Exemple #1
0
class Role(RoleJsonSerializer, SyncableDocument):
    name = db.StringField(max_length=80, unique=True)

    description = db.StringField()

    def __unicode__(self):
        return self.name
Exemple #2
0
class UnresolvedReference(JsonSerializer, db.Document):

    document = db.GenericReferenceField()

    field_path = db.StringField()

    class_name = db.StringField()

    central_id = db.ObjectIdField()

    def resolve(self):
        print "Trying to resolve %s" % unicode(self)
        try:
            from MookAPI.helpers import get_service_for_class
            service = get_service_for_class(self.class_name)
            local_document = service.get(central_id=self.central_id)
            self.document.set_value_for_field_path(local_document,
                                                   self.field_path)
            self.document.clean()
            self.document.save(validate=False)  # FIXME MongoEngine bug
            self.delete()
            print "==> Success!"
            return True
        except Exception as e:
            print "==> Failed!"
            return False

    def __unicode__(self):
        try:
            return "Reference to %s document %s in document %s at field path %s" \
                   % (self.class_name, str(self.central_id), self.document, self.field_path)
        except:
            return "Reference to %s document %s in %s document at field path %s" \
                   % (self.class_name, str(self.central_id), self.document.__class__.__name__, self.field_path)
Exemple #3
0
class ExternalVideoResourceContent(ExternalVideoResourceContentJsonSerializer,
                                   ResourceContent):

    _SOURCES = ('youtube', )
    source = db.StringField(required=True, choices=_SOURCES)
    """The website where the video is hosted."""

    ## Video unique id on the source website
    video_id = db.StringField(required=True)
    """A unique identifier of the video on the `source` website."""
Exemple #4
0
class ExerciseQuestion(ExerciseQuestionJsonSerializer, db.EmbeddedDocument):
    """
    Generic collection, every question type will inherit from this.
    Subclasses should override method "without_correct_answer" in order to define the version sent to clients.
    Subclasses of questions depending on presentation parameters should also override
    method "with_computed_correct_answer".
    """

    meta = {'allow_inheritance': True, 'abstract': True}

    @property
    def id(self):
        return self._id

    ## Object Id
    _id = db.ObjectIdField(default=bson.ObjectId)

    ## Question text
    question_heading = db.StringField()

    ## Question image
    question_image = db.ImageField()

    @property
    def question_image_url(self, _external=True):
        if not self.question_image:
            return None

        if not hasattr(self, '_instance'):
            return None

        return url_for("resources.get_exercise_question_image",
                       resource_id=str(self._instance._instance.id),
                       question_id=str(self._id),
                       filename=self.question_image.filename,
                       _external=_external)

    ## Answer feedback (explanation of the right answer)
    answer_feedback = db.StringField()

    def without_correct_answer(self):
        son = self.to_json()
        son.pop('answer_feedback', None)
        return son

    def with_computed_correct_answer(self, parameters):
        son = self.to_json()
        return son

    def answer_with_data(self, data):
        return ExerciseQuestionAnswer.init_with_data(data)
Exemple #5
0
class VideoResourceContent(VideoResourceContentJsonSerializer, LinkedFileResourceContent):

    ##FIXME: Override content_file to specify accepted extensions/mimetypes.

    _SOURCES = (
        '',
        'youtube',
    )
    source = db.StringField(choices=_SOURCES)
    """The website where the video is hosted."""

    ## Video unique id on the source website
    video_id = db.StringField()
    """A unique identifier of the video on the `source` website."""
Exemple #6
0
class DropdownExerciseQuestion(DropdownExerciseQuestionJsonSerializer,
                               ExerciseQuestion):
    """question where blanks need to be filled with word chosen from a dropdown list."""

    ## Object Id
    _id = db.ObjectIdField(default=ObjectId)

    ## the whole text, where blanks are tagged with [%N%] where N if Nth blank of the text
    text = db.StringField()

    ## Propositions
    dropdowns = db.ListField(
        db.EmbeddedDocumentField(DropdownExerciseQuestionDropdown))

    def without_correct_answer(self):
        son = super(DropdownExerciseQuestion, self).without_correct_answer()
        for dropdown in son['dropdowns']:
            for proposition in dropdown['propositions']:
                proposition.pop('is_correct_answer', None)
        return son

    def answer_with_data(self, data):
        return DropdownExerciseQuestionAnswer.init_with_data(data)

    def get_propositions_by_id(self, propositionsId):
        result = []
        for dropdown in self.dropdowns:
            for proposition in dropdown.propositions:
                if proposition._id in propositionsId:
                    result.append(proposition)
        return result
Exemple #7
0
class OrderingExerciseQuestionItem(OrderingExerciseQuestionItemJsonSerializer, db.EmbeddedDocument):
    """Stores an item for the overall ordering question."""

    ## Object Id
    _id = db.ObjectIdField(default=ObjectId)

    ## Text
    text = db.StringField()
Exemple #8
0
class CategorizeExerciseQuestionItem(CategorizeExerciseQuestionItem,
                                     db.EmbeddedDocument):
    """Stores an item that belongs to one category."""

    ## Object Id
    _id = db.ObjectIdField(default=ObjectId)

    ## Text
    text = db.StringField()
Exemple #9
0
class MultipleAnswerMCQExerciseQuestionProposition(MultipleAnswerMCQExerciseQuestionPropositionJsonSerializer, db.EmbeddedDocument):
    """Stores a proposition to a multiple-answer MCQ."""

    ## Object Id
    _id = db.ObjectIdField(default=ObjectId)

    ## Text
    text = db.StringField()

    ## Is correct answer
    is_correct_answer = db.BooleanField(default=False)
Exemple #10
0
class TutoringRelation(TutoringRelationJsonSerializer, SyncableDocument):
    tutor = db.ReferenceField('User')
    student = db.ReferenceField('User')
    accepted = db.BooleanField(default=False)
    acknowledged = db.BooleanField(default=False)  # has the accepted notification been seen by the initiator
    INITIATORS = ('tutor, student')
    initiated_by = db.StringField(choices=INITIATORS)

    @property
    def url(self, _external=False):
        return url_for("tutoring.get_relation", relation_id=self.id, _external=_external)
Exemple #11
0
class LinkedFileResourceContent(LinkedFileResourceContentJsonSerializer,
                                ResourceContent):

    content_file = db.StringField()
    """
    The URL or the name of the file to download.
    If an absolute URL (containing "://" or starting with "//" is given, that URL will be used as the source.
    Otherwise, the string will be interpreted as a path from the "static" folder.
    """
    def clean(self):
        super(LinkedFileResourceContent, self).clean()
        self.content_file = self.content_file.strip()
Exemple #12
0
class DropdownExerciseQuestionProposition(
        DropdownExerciseQuestionPropositionJsonSerializer,
        db.EmbeddedDocument):
    """Stores a proposition to a blank field."""

    ## Object Id
    _id = db.ObjectIdField(default=ObjectId)

    ## Text
    text = db.StringField()

    ## Is correct answer
    is_correct_answer = db.BooleanField(default=False)
Exemple #13
0
class CategorizeExerciseQuestionCategory(
        CategorizeExerciseQuestionCategoryJsonSerializer, db.EmbeddedDocument):
    """Stores a category for the categorize question."""

    ## Object Id
    _id = db.ObjectIdField(default=ObjectId)

    ## Text
    title = db.StringField()

    ## items that belong to this category
    items = db.ListField(
        db.EmbeddedDocumentField(CategorizeExerciseQuestionItem))
class RightOrWrongExerciseQuestionProposition(
        RightOrWrongExerciseQuestionPropositionJsonSerializer,
        db.EmbeddedDocument):
    """Stores a proposition to a right or wrong question."""

    ## Object Id
    _id = db.ObjectIdField(default=ObjectId)

    ## Text
    text = db.StringField()

    ## Is correct answer
    is_correct_answer = db.BooleanField(default=False)
Exemple #15
0
class StaticPage(StaticPageJsonSerializer, SyncableDocument):

    page_id = db.StringField(unique=True, required=True)
    """An unique id used to reference this page"""

    footer_link_text = db.StringField(required=True)
    """The text that appears on the link towards this page"""

    html_content = db.StringField()
    """An HTML string containing the text of the page."""

    external_link = db.StringField()
    """if the link should redirect to an external page"""

    order = db.IntField()

    ### VIRTUAL PROPERTIES

    @property
    def url(self, _external=False):
        return url_for("static_page.get_static_page",
                       page_id=self.page_id,
                       _external=_external)
Exemple #16
0
class Resource(ResourceJsonSerializer, SyncableDocument):
    """
    .. _Resource:
    
    Any elementary pedagogical resource.
    Contains the metadata and a ResourceContent_ embedded document.
    Resource_ objects are organized by Lesson_ objects, *i.e.* each Resource_ references a parent Lesson_.
    """

    meta = {'allow_inheritance': True}

    ### PROPERTIES

    is_published = db.BooleanField(default=True)
    """Whether the resource should appear on the platform"""

    is_additional = db.BooleanField(default=False)
    """True if the resource is an additional resource, i.e. has a Resource parent (instead of a Lesson)"""

    title = db.StringField(required=True)
    """The title of the Resource_."""

    slug = db.StringField(unique=True)
    """A human-readable unique identifier for the Resource_."""

    ## Will be implemented later
    # creator = db.ReferenceField('User')
    # """The user who created the resource."""

    description = db.StringField()
    """A text describing the Resource_."""

    order = db.IntField()
    """The order of the Resource_ in the Lesson_."""

    keywords = db.ListField(db.StringField())
    """A list of keywords to index the Resource_."""

    date = db.DateTimeField(default=datetime.datetime.now, required=True)
    """The date the Resource_ was created."""

    parent = db.ReferenceField('Lesson')
    """The parent hierarchy object (usually Lesson, but can be overridden)."""

    parent_resource = db.ReferenceField('Resource')
    """The parent hierarchy object (usually Lesson, but can be overridden)."""

    resource_content = db.EmbeddedDocumentField(ResourceContent)
    """The actual content of the Resource_, stored in a ResourceContent_ embedded document."""

    ### VIRTUAL PROPERTIES

    @property
    def url(self, _external=False):
        return url_for("resources.get_resource",
                       resource_id=self.id,
                       _external=_external)

    def is_validated_by_user(self, user):
        """Whether the current user (if any) has validated this Resource_."""
        from MookAPI.services import completed_resources

        return completed_resources.find(resource=self, user=user).count() > 0

    @property
    def is_validated(self):
        try:
            verify_jwt()
        except:
            pass
        if not current_user:
            return None
        return self.is_validated_by_user(current_user.user)

    @property
    def skill(self):
        """Shorthand virtual property to the parent Skill_ of the parent Lesson_."""
        if self.parent_resource:
            return self.parent_resource.skill if not isinstance(
                self.parent_resource, DBRef) else None
        return self.parent.skill if not isinstance(self.parent,
                                                   DBRef) else None

    @property
    def track(self):
        """Shorthand virtual property to the parent Track_ of the parent Skill_ of the parent Lesson_."""
        if self.skill and not isinstance(self.skill, DBRef):
            return self.skill.track
        return None

    @property
    def additional_resources(self):
        """A queryset of the Resources_ objects that are additional resources to the current Resource_."""
        from MookAPI.services import resources

        return resources.find(parent_resource=self).order_by('order', 'title')

    @property
    def additional_resources_refs(self):
        return [
            additional_resource.to_json_dbref()
            for additional_resource in self.additional_resources
        ]

    ### METHODS

    def siblings(self):
        """A queryset of Resource_ objects in the same Lesson_, including the current Resource_."""
        return Resource.objects.order_by('order',
                                         'title').filter(parent=self.parent)

    def siblings_strict(self):
        """A queryset of Resource_ objects in the same Lesson_, excluding the current Resource_."""
        return Resource.objects.order_by('order',
                                         'title').filter(parent=self.parent,
                                                         id__ne=self.id)

    def aunts(self):
        """A queryset of Lesson_ objects in the same Skill_, including the current Lesson_."""
        return self.parent.siblings()

    def aunts_strict(self):
        """A queryset of Lesson_ objects in the same Skill_, excluding the current Lesson_."""
        return self.parent.siblings_strict()

    def cousins(self):
        """A queryset of Resource_ objects in the same Skill_, including the current Resource_."""
        return Resource.objects.order_by(
            'parent', 'order', 'title').filter(parent__in=self.aunts())

    def cousins_strict(self):
        """A queryset of Resource_ objects in the same Skill_, excluding the current Resource_."""
        return Resource.objects.order_by(
            'parent', 'order', 'title').filter(parent__in=self.aunts_strict())

    def _set_slug(self):
        """Sets a slug for the hierarchy level based on the title."""

        slug = self.slug or slugify(self.title)

        def alternate_slug(text, k=1):
            return text if k <= 1 else "{text}-{k}".format(text=text, k=k)

        k = 0
        kmax = 10**4
        while k < kmax:
            if self.id is None:
                req = self.__class__.objects(slug=alternate_slug(slug, k))
            else:
                req = self.__class__.objects(slug=alternate_slug(slug, k),
                                             id__ne=self.id)
            if len(req) > 0:
                k = k + 1
                continue
            else:
                break
        self.slug = alternate_slug(slug, k) if k <= kmax else None

    def clean(self):
        super(Resource, self).clean()
        self._set_slug()

    @property
    def bg_color(self):
        return self.track.bg_color

    def user_analytics(self, user):
        from MookAPI.services import visited_resources
        return dict(
            nb_visits=visited_resources.find(user=user, resource=self).count())

    def user_info(self, user, analytics=False):
        info = dict(is_validated=self.is_validated_by_user(user))
        if analytics:
            info['analytics'] = self.user_analytics(user)
        return info

    @property
    def hierarchy(self):
        """
        Returns an array of the breadcrumbs up until the current object: [Track_, Skill_, Lesson_, Resource_]
        """
        rv = []

        if self.is_additional:
            rv.extend([
                self.parent_resource.track.to_json_dbref(),
                self.parent_resource.skill.to_json_dbref(),
                self.parent_resource.parent.to_json_dbref(),
                self.parent_resource.to_json_dbref(),
                self.to_json_dbref()
            ])
        else:
            rv.extend([
                self.track.to_json_dbref(),
                self.skill.to_json_dbref(),
                self.parent.to_json_dbref(),
                self.to_json_dbref()
            ])

        return rv

    def __unicode__(self):
        return "%s [%s]" % (self.title, self.__class__.__name__)

    def top_level_syncable_document(self):
        return self.track

    def all_synced_documents(self, local_server=None):
        items = super(Resource,
                      self).all_synced_documents(local_server=local_server)

        for additional_resource in self.additional_resources:
            items.extend(
                additional_resource.all_synced_documents(
                    local_server=local_server))

        return items
Exemple #17
0
class LocalServer(LocalServerJsonSerializer, SyncableDocument):
    """
    .. _LocalServer:

    This collection contains the list of all central servers connected to the current central server.
    """

    ### PROPERTIES

    name = db.StringField()

    key = db.StringField(unique=True)

    secret = db.StringField()

    synced_tracks = db.ListField(
        db.ReferenceField('Track', reverse_delete_rule=PULL))

    @property
    def synced_users(self):
        from MookAPI.services import user_credentials
        return [
            creds.user for creds in user_credentials.find(local_server=self)
        ]

    @property
    def synced_documents(self):
        from MookAPI.services import static_pages
        documents = []
        documents.extend(self.synced_tracks)
        documents.extend(self.synced_users)
        documents.extend(static_pages.all())
        return documents

    last_sync = db.DateTimeField()

    @property
    def url(self, _external=False):
        return url_for("local_servers.get_local_server",
                       local_server_id=self.id,
                       _external=_external)

    def syncs_document(self, document):
        top_level_document = document.top_level_syncable_document()
        return top_level_document in self.synced_documents

    def get_sync_list(self):
        updates = []
        deletes = []

        updates.extend(self.items_to_update(local_server=self))

        for document in self.synced_documents:
            updates.extend(document.items_to_update(local_server=self))
            deletes.extend(document.items_to_delete(local_server=self))

        return dict(
            updates=unique(updates),
            deletes=
            deletes  # 'deletes' contains (unhashable) DBRef objects, can't apply 'unique' to it
        )

    def reset(self):
        self.last_sync = None

    def __unicode__(self):
        return "%s [%s]" % (self.name, self.key)

    @staticmethod
    def hash_secret(secret):
        """
        Return the md5 hash of the secret+salt
        """
        return bcrypt.encrypt(secret)

    def verify_secret(self, secret):
        return bcrypt.verify(secret, self.secret)
Exemple #18
0
class User(UserJsonSerializer, CsvSerializer, SyncableDocument):

    full_name = db.StringField(unique=False, required=True)

    email = db.EmailField(unique=False)

    country = db.StringField()

    occupation = db.StringField()

    organization = db.StringField()

    active = db.BooleanField(default=True)

    accept_cgu = db.BooleanField(required=True, default=False)

    roles = db.ListField(db.ReferenceField(Role))

    @property
    def inscription_time(self):
        from MookAPI.services import misc_activities
        creation_activity = misc_activities.first(user=self,
                                                  type="register_user_attempt",
                                                  object_title="success")
        return creation_activity.date if creation_activity else "#NO INSCRIPTION DATE FOUND#"

    @property
    def inscription_date(self):
        inscription_date = self.inscription_time
        if isinstance(inscription_date, datetime):
            inscription_date = inscription_date.strftime("%Y-%m-%d")
        return inscription_date

    @property
    def tracks(self):
        from MookAPI.services import completed_tracks
        completedTracks = completed_tracks.__model__.objects(
            user=self).only('track').distinct('track')
        return "|".join(track.title for track in completedTracks)

    @property
    def skills(self):
        from MookAPI.services import completed_skills
        completedSkills = completed_skills.__model__.objects(
            user=self).only('skill').distinct('skill')
        return "|".join(skill.title for skill in completedSkills)

    def courses_info(self, analytics=False):
        info = dict(tracks=dict(), skills=dict(), resources=dict())

        from MookAPI.services import tracks
        for track in tracks.all():
            info['tracks'][str(track.id)] = track.user_info(
                user=self, analytics=analytics)
            for skill in track.skills:
                info['skills'][str(skill.id)] = skill.user_info(
                    user=self, analytics=analytics)
                for lesson in skill.lessons:
                    for resource in lesson.resources:
                        info['resources'][str(
                            resource.id)] = resource.user_info(
                                user=self, analytics=analytics)

        return info

    @property
    def tutors(self):
        from MookAPI.services import tutoring_relations
        relations = tutoring_relations.find(student=self, accepted=True)
        return [relation.tutor for relation in relations]

    @property
    def students(self):
        from MookAPI.services import tutoring_relations
        relations = tutoring_relations.find(tutor=self, accepted=True)
        return [relation.student for relation in relations]

    ## Awaiting: the current user was requested
    @property
    def awaiting_tutor_requests(self):
        from MookAPI.services import tutoring_relations
        relations = tutoring_relations.find(tutor=self,
                                            initiated_by='student',
                                            accepted=False)
        return [relation.student for relation in relations]

    @property
    def awaiting_student_requests(self):
        from MookAPI.services import tutoring_relations
        relations = tutoring_relations.find(student=self,
                                            initiated_by='tutor',
                                            accepted=False)
        return [relation.tutor for relation in relations]

    ## Not acknowledged: Requests initiated by the current user have been accepted
    @property
    def not_acknowledged_tutors(self):
        from MookAPI.services import tutoring_relations
        relations = tutoring_relations.find(student=self,
                                            initiated_by='student',
                                            accepted=True,
                                            acknowledged=False)
        return [relation.tutor for relation in relations]

    @property
    def not_acknowledged_students(self):
        from MookAPI.services import tutoring_relations
        relations = tutoring_relations.find(tutor=self,
                                            initiated_by='tutor',
                                            accepted=True,
                                            acknowledged=False)
        return [relation.student for relation in relations]

    ## Pending: the current user made the request
    @property
    def pending_tutors(self):
        from MookAPI.services import tutoring_relations
        relations = tutoring_relations.find(student=self,
                                            initiated_by='student',
                                            accepted=False)
        return [relation.tutor for relation in relations]

    @property
    def pending_students(self):
        from MookAPI.services import tutoring_relations
        relations = tutoring_relations.find(tutor=self,
                                            initiated_by='tutor',
                                            accepted=False)
        return [relation.student for relation in relations]

    phagocyted_by = db.ReferenceField('self', required=False)

    def is_track_test_available_and_never_attempted(self, track):
        # FIXME Make more efficient search using Service
        from MookAPI.services import unlocked_track_tests
        if unlocked_track_tests.find(user=self, track=track).count() > 0:
            from MookAPI.services import track_validation_attempts
            attempts = track_validation_attempts.find(user=self)
            return all(attempt.track != track for attempt in attempts)

        return False

    def update_progress(self, self_credentials):
        from MookAPI.services import \
            completed_resources, \
            unlocked_track_tests
        skills = set()
        tracks = set()
        for activity in completed_resources.find(user=self):
            skills.add(activity.resource.skill)
        for skill in skills:
            tracks.add(skill.track)
        for track in tracks:
            track_progress = track.user_progress(self)
            if unlocked_track_tests.find(user=self, track=track).count(
            ) == 0 and track_progress['current'] >= track_progress['max']:
                self_credentials.unlock_track_validation_test(track)

    @property
    def url(self, _external=False):
        return url_for("users.get_user_info",
                       user_id=self.id,
                       _external=_external)

    @property
    def username(self):
        credentials = self.all_credentials
        if not credentials:
            return "#NO USERNAME FOUND#"
        return credentials[0].username

    @property
    def all_credentials(self):
        from MookAPI.services import user_credentials
        return user_credentials.find(user=self)

    def credentials(self, local_server=None):
        from MookAPI.services import user_credentials
        return user_credentials.find(user=self, local_server=local_server)

    def all_synced_documents(self, local_server=None):
        items = super(User, self).all_synced_documents()

        from MookAPI.services import activities, tutoring_relations
        for creds in self.credentials(local_server=local_server):
            items.extend(creds.all_synced_documents(local_server=local_server))
        for activity in activities.find(user=self):
            items.extend(
                activity.all_synced_documents(local_server=local_server))
        for student_relation in tutoring_relations.find(tutor=self):
            items.extend(
                student_relation.all_synced_documents(
                    local_server=local_server))
        for tutor_relation in tutoring_relations.find(student=self):
            items.extend(
                tutor_relation.all_synced_documents(local_server=local_server))

        return items

    def __unicode__(self):
        return self.full_name or self.email or self.id

    def phagocyte(self, other, self_credentials):
        if other == self:
            self.update_progress(self_credentials)
            self.save()
            return self

        from MookAPI.services import \
            activities, \
            tutoring_relations, \
            user_credentials
        for creds in user_credentials.find(user=other):
            creds.user = self
            creds.save(validate=False)
        for activity in activities.find(user=other):
            activity.user = self
            activity.save()
        for student_relation in tutoring_relations.find(tutor=other):
            student_relation.tutor = self
            student_relation.save()
        for tutor_relation in tutoring_relations.find(student=other):
            tutor_relation.student = self
            tutor_relation.save()

        other.active = False
        other.phagocyted_by = self
        other.save()

        self.update_progress(self_credentials)
        self.save()

        return self

    @classmethod
    def field_names_header_for_csv(cls):
        return [
            'Full name', 'Username', 'Email', 'Country', 'Occupation',
            'Organization', 'Inscription date', 'Tracks', 'Skills'
        ]

    def get_field_names_for_csv(cls):
        return 'full_name username email country occupation organization inscription_date tracks skills'.split(
        )
Exemple #19
0
class Skill(SkillJsonSerializer, ResourceHierarchy):
    """
    .. _Skill:

    Second level of Resource_ hierarchy.
    Their ascendants are Track_ objects.
    Their descendants are Lesson_ objects.
    """

    ### PROPERTIES

    ## Parent track
    track = db.ReferenceField('Track')
    """The parent Track_."""

    ## icon image
    icon = db.ImageField()
    """An icon to illustrate the Skill_."""

    ## short description
    short_description = db.StringField()
    """The short description of the skill, to appear where there is not enough space for the long one."""

    ## skill validation test
    validation_exercise = db.EmbeddedDocumentField(SkillValidationExercise)
    """The exercise that the user might take to validate the skill."""

    ### VIRTUAL PROPERTIES

    @property
    def icon_url(self, _external=True):
        """The URL where the skill icon can be downloaded."""
        return url_for("hierarchy.get_skill_icon",
                       skill_id=self.id,
                       _external=_external)

    @property
    def url(self, _external=False):
        return url_for("hierarchy.get_skill",
                       skill_id=self.id,
                       _external=_external)

    @property
    def lessons(self):
        """A queryset of the Lesson_ objects that belong to the current Skill_."""
        from MookAPI.services import lessons

        return lessons.find(skill=self).order_by('order', 'title')

    @property
    def lessons_refs(self):
        return [lesson.to_json_dbref() for lesson in self.lessons]

    def is_validated_by_user(self, user):
        """Whether the current_user validated the hierarchy level based on their activity."""
        from MookAPI.services import completed_skills

        return completed_skills.find(
            skill=self, user=user, is_validated_through_test=True).count() > 0

    def user_progress(self, user):
        current = 0
        nb_resources = 0
        for lesson in self.lessons:
            for resource in lesson.resources:
                nb_resources += 1
                if resource.is_validated_by_user(user):
                    current += 1
        return {'current': current, 'max': nb_resources}

    @property
    def bg_color(self):
        return self.track.bg_color

    @property
    def hierarchy(self):
        return [self.track.to_json_dbref(), self.to_json_dbref()]

    ### METHODS

    def _add_instance(self, obj):
        """This is a hack to provide the ``_instance`` property to the shorthand question-getters."""
        def _add_instance_single_object(obj):
            obj._instance = self
            return obj

        if isinstance(obj, list):
            return map(_add_instance_single_object, obj)
        else:
            return _add_instance_single_object(obj)

    def user_analytics(self, user):
        analytics = super(Skill, self).user_analytics(user)

        from MookAPI.services import skill_validation_attempts, visited_skills

        skill_validation_attempts = skill_validation_attempts.find(
            user=user).order_by('-date')
        analytics['last_attempts_scores'] = map(
            lambda a: {
                "date": a.date,
                "nb_questions": a.nb_questions,
                "score": a.nb_right_answers
            }, skill_validation_attempts[:5])

        nb_finished_attempts = 0
        total_duration = datetime.timedelta(0)
        for attempt in skill_validation_attempts:
            if attempt.duration:
                nb_finished_attempts += 1
                total_duration += attempt.duration
        if nb_finished_attempts > 0:
            analytics['average_time_on_exercise'] = math.floor(
                (total_duration / nb_finished_attempts).total_seconds())
        else:
            analytics['average_time_on_exercise'] = 0

        analytics['nb_attempts'] = skill_validation_attempts.count()
        analytics['nb_visits'] = visited_skills.find(user=user,
                                                     skill=self).count()

        return analytics

    def top_level_syncable_document(self):
        return self.track

    def all_synced_documents(self, local_server=None):
        items = super(Skill,
                      self).all_synced_documents(local_server=local_server)

        for lesson in self.lessons:
            items.extend(
                lesson.all_synced_documents(local_server=local_server))

        return items

    @property
    def questions(self):
        """A list of all children exercises' questions, whatever their type."""

        from MookAPI.services import exercise_resources

        questions = []
        for l in self.lessons:
            for r in l.resources:
                if exercise_resources._isinstance(r):
                    questions.extend(r.questions)

        return questions

    def question(self, question_id):
        """A shorthand getter for a question with a known `_id`."""

        from MookAPI.services import exercise_resources

        oid = bson.ObjectId(question_id)
        for l in self.lessons:
            for r in l.resources:
                if exercise_resources._isinstance(r):
                    for q in r.questions:
                        if q._id == oid:
                            return r._add_instance(q)
        return None

    def random_questions(self, number=None):
        """
        A shorthand getter for a list of random questions.
        See the documentation of `SkillValidationExercise.random_questions`.
        """

        questions = self.validation_exercise.random_questions(self, number)
        return self._add_instance(questions)
Exemple #20
0
class RichTextResourceContent(RichTextResourceContentJsonSerializer,
                              ResourceContent):

    html = db.StringField(required=True)
    """An HTML string containing the rich text."""
Exemple #21
0
class Activity(ActivityJsonSerializer, CsvSerializer, SyncableDocument):
    """Describes any kind of user activity."""

    meta = {'allow_inheritance': True, 'indexes': ['+date', 'user', 'type']}

    ### PROPERTIES

    credentials = db.ReferenceField('UserCredentials')
    """The credentials under which the user was logged when they performed the action."""

    user = db.ReferenceField('User')
    """The user performing the activity."""

    user_username = db.StringField(default="")
    """The username (= unique id) of the user who has performed the activity"""

    user_name = db.StringField(default="")
    """The full name of the user who has performed the activity"""
    # FIXME this has to be set using the credentials provided

    date = db.DateTimeField(default=datetime.datetime.now, required=True)
    """The date at which the activity was performed."""

    local_server = db.ReferenceField('LocalServer')
    """The local server on which the activity has been performed"""

    local_server_name = db.StringField()
    """The name of the local server on which the activity has been performed"""

    type = db.StringField()
    """ The type of the activity, so we can group the activities by type to better analyse them.
    This is supposed to be defaulted/initialized in each subclass"""
    @property
    def object(self):
        return None

    object_title = db.StringField()
    """ The title of the object associated to this activity. It allows a better comprehension of the activity than the activity_id.
    This is supposed to be defaulted/initialized in each subclass """

    @property
    def url(self, _external=False):
        print("Activity (%s) url: %s" % (self._cls, self.id))
        return url_for("activity.get_activity",
                       activity_id=self.id,
                       _external=_external)

    @property
    def local_server_id(self):
        return self.local_server.id if self.local_server is not None else ""

    @property
    def user_id(self):
        return self.user.id if self.user is not None else ""

    def clean(self):
        super(Activity, self).clean()
        if self.object and not isinstance(self.object, DBRef):
            self.object_title = getattr(self.object, 'title', None)
        if self.credentials and not isinstance(self.credentials, DBRef):
            self.user = self.credentials.user
            self.user_username = self.credentials.username
            self.local_server = self.credentials.local_server
        if self.user and not isinstance(self.user, DBRef):
            self.user_name = self.user.full_name
        if self.local_server and not isinstance(self.local_server, DBRef):
            self.local_server_name = self.local_server.name

    @classmethod
    def field_names_header_for_csv(cls):
        return [
            'Koombook id', 'Koombook name', 'User id', 'User username',
            'User full name', 'Date - Time', 'Action type', 'Object id',
            'Object title', 'Object-specific data'
        ]

    def top_level_syncable_document(self):
        return self.user

    def all_synced_documents(self, local_server=None):
        if self.object and not isinstance(self.object, DBRef):
            if not local_server.syncs_document(self.object):
                return []
        return super(Activity,
                     self).all_synced_documents(local_server=local_server)

    def __unicode__(self):
        return "Activity with type %s for user %s" % (self.type, self.user)

    def get_field_names_for_csv(self):
        """ this method gives the fields to export as csv row, in a chosen order """
        return [
            'local_server_id', 'local_server_name', 'user_id', 'user_username',
            'user_name', 'date', 'type', 'object', 'object_title'
        ]
Exemple #22
0
class UserCredentials(UserCredentialsJsonSerializer, SyncableDocument,
                      UserMixin):

    user = db.ReferenceField(User, required=True)

    local_server = db.ReferenceField('LocalServer', required=False)

    @property
    def local_server_name(self):
        if self.local_server:
            return self.local_server.name
        return None

    username = db.StringField(unique_with='local_server', required=True)

    password = db.StringField()

    @property
    def is_active(self):
        return self.user.active

    def has_role(self, rolename):
        return rolename in [r.name for r in self.user.roles]

    def add_visited_resource(self, resource):
        achievements = []
        from MookAPI.services import exercise_resources, video_resources, external_video_resources, visited_resources
        visited_resource = visited_resources.create(credentials=self,
                                                    resource=resource)
        if not resource.is_additional:
            track_achievements = self.add_started_track(resource.track)
            achievements.extend(track_achievements)
            if not exercise_resources._isinstance(
                    resource) and not video_resources._isinstance(
                        resource) and not external_video_resources._isinstance(
                            resource):
                resource_achievements = self.add_completed_resource(resource)
                achievements.extend(resource_achievements)
        return visited_resource, achievements

    def add_completed_resource(self, resource):
        achievements = []
        from MookAPI.services import completed_resources
        if completed_resources.find(user=self.user,
                                    resource=resource).count() == 0:
            completed_resource = completed_resources.create(credentials=self,
                                                            resource=resource)
            achievements.append(completed_resource)
        return achievements

    def add_completed_skill(self, skill):
        achievements = []
        from MookAPI.services import completed_skills
        if completed_skills.find(user=self.user,
                                 skill=skill,
                                 is_validated_through_test=True).count() == 0:
            completed_skill = completed_skills.create(
                credentials=self, skill=skill, is_validated_through_test=True)
            achievements.append(completed_skill)
            track = skill.track
            track_progress = track.user_progress(self.user)
            from MookAPI.services import unlocked_track_tests
            if unlocked_track_tests.find(user=self.user, track=track).count(
            ) == 0 and track_progress['current'] >= track_progress['max']:
                track_achievements = self.unlock_track_validation_test(
                    track=track)
                achievements.extend(track_achievements)
        return achievements

    def add_started_track(self, track):
        achievements = []
        from MookAPI.services import started_tracks
        if started_tracks.find(user=self.user, track=track).count() == 0:
            started_track = started_tracks.create(credentials=self,
                                                  track=track)
            achievements.append(started_track)
        return achievements

    def unlock_track_validation_test(self, track):
        achievements = []
        from MookAPI.services import unlocked_track_tests
        if unlocked_track_tests.find(user=self.user, track=track).count() == 0:
            unlocked_track_test = unlocked_track_tests.create(credentials=self,
                                                              track=track)
            achievements.append(unlocked_track_test)
        return achievements

    def add_completed_track(self, track):
        achievements = []
        from MookAPI.services import completed_tracks
        if completed_tracks.find(user=self.user, track=track).count() == 0:
            completed_track = completed_tracks.create(credentials=self,
                                                      track=track)
            achievements.append(completed_track)
        return achievements

    def is_track_test_available_and_never_attempted(self, track):
        return self.user.is_track_test_available_and_never_attempted(track)

    @property
    def url(self, _external=False):
        return url_for("users.get_user_credentials",
                       credentials_id=self.id,
                       _external=_external)

    @staticmethod
    def hash_pass(password):
        """
        Return the md5 hash of the password+salt
        """
        return bcrypt.encrypt(password)

    def verify_pass(self, password):
        return bcrypt.verify(password, self.password)
Exemple #23
0
class ResourceHierarchy(ResourceHierarchyJsonSerializer, SyncableDocument):
    """
    .. _ResourceHierarchy:
    
    An abstract class that can describe a Lesson_, a Skill_ or a Track_.
    """

    meta = {'allow_inheritance': True, 'abstract': True}

    ### PROPERTIES

    is_published = db.BooleanField(default=True)
    """Whether the resource should appear on the platform"""

    title = db.StringField(required=True)
    """The title of the hierarchy level."""

    slug = db.StringField(unique=True)
    """A human-readable unique identifier for the hierarchy level."""

    description = db.StringField()
    """A text describing the content of the resources in this hierarchy level."""

    order = db.IntField()
    """The order of the hierarchy amongst its siblings."""

    date = db.DateTimeField(default=datetime.datetime.now, required=True)
    """The date the hierarchy level was created."""
    def is_validated_by_user(self, user):
        """Whether the user validated the hierarchy level based on their activity."""
        ## Override this method in each subclass
        return False

    @property
    def is_validated(self):
        try:
            verify_jwt()
        except:
            pass
        if not current_user:
            return None
        return self.is_validated_by_user(current_user.user)

    def user_progress(self, user):
        """
        How many sub-units in this level have been validated (current) and how many are there in total (max).
        Returns a dictionary with format: {'current': Int, 'max': Int}
        """
        ## Override this method in each subclass
        return {'current': 0, 'max': 0}

    @property
    def progress(self):
        try:
            verify_jwt()
        except:
            pass
        if not current_user:
            return None
        return self.user_progress(current_user.user)

    ### METHODS

    def _set_slug(self):
        """Sets a slug for the hierarchy level based on the title."""

        slug = self.slug or slugify(self.title)

        def alternate_slug(text, k=1):
            return text if k <= 1 else "{text}-{k}".format(text=text, k=k)

        k = 0
        kmax = 10**4
        while k < kmax:
            if self.id is None:
                req = self.__class__.objects(slug=alternate_slug(slug, k))
            else:
                req = self.__class__.objects(slug=alternate_slug(slug, k),
                                             id__ne=self.id)
            if len(req) > 0:
                k = k + 1
                continue
            else:
                break
        self.slug = alternate_slug(slug, k) if k <= kmax else None

    def clean(self):
        self._set_slug()
        super(ResourceHierarchy, self).clean()

    def __unicode__(self):
        return self.title

    @property
    def hierarchy(self):
        """Returns an array of the breadcrumbs up until the current object."""
        return []

    def user_analytics(self, user):
        return dict()

    def user_info(self, user, analytics=False):
        info = dict(is_validated=self.is_validated_by_user(user),
                    progress=self.user_progress(user))
        if analytics:
            info['analytics'] = self.user_analytics(user)
        return info
Exemple #24
0
class SyncTask(db.Document):
    """A document that describes a sync operation to perform on the local server."""

    ### PROPERTIES

    queue_position = db.SequenceField()
    """An auto-incrementing counter determining the order of the operations to perform."""

    type = db.StringField(choices=('update', 'delete'))
    """The operation to perform (``update`` or ``delete``)."""

    central_id = db.ObjectIdField()
    """The ``id`` of the ``SyncableDocument`` on the central server."""

    class_name = db.StringField()
    """The class of the ``SyncableDocument`` affected by this operation."""

    url = db.StringField()
    """The URL at which the information of the document can be downloaded. Null if ``type`` is ``delete``."""

    errors = db.ListField(db.StringField())
    """The list of the errors that occurred while trying to perform the operation."""
    def __unicode__(self):
        return "%s %s document with central id %s" % (
            self.type, self.class_name, self.central_id)

    _service = None

    @property
    def service(self):
        if not self._service:
            from MookAPI.helpers import get_service_for_class
            self._service = get_service_for_class(self.class_name)
        return self._service

    @property
    def model(self):
        return self.service.__model__

    def _fetch_update(self, connector, local_document):
        r = connector.get(self.url)

        if r.status_code == 200:
            son = json_util.loads(r.text)
            document = self.model.from_json(
                son['data'],
                from_central=True,
                overwrite_document=local_document,
                upload_path=connector.local_files_path)
            document.clean()
            document.save(
                validate=False
            )  # FIXME MongoEngine bug, hopefully fixed in next version
            return document

        else:
            raise Exception("Could not fetch info, got status code %d" %
                            r.status_code)

    def _update_local_server(self, connector, local_document):
        tracks_before = set(local_document.synced_tracks)
        users_before = set(local_document.synced_users)

        document = self._fetch_update(connector, local_document)

        if document:
            from . import sync_tasks_service

            tracks_after = set(document.synced_tracks)
            users_after = set(document.synced_users)

            for new_track in tracks_after - tracks_before:
                sync_tasks_service.fetch_tasks_whole_track(
                    new_track.id, connector)
            for new_user in users_after - users_before:
                sync_tasks_service.fetch_tasks_whole_user(
                    new_user.id, connector)
            for obsolete_track in tracks_before - tracks_after:
                sync_tasks_service.create_delete_task_existing_document(
                    obsolete_track)
            for obsolete_user in users_before - users_after:
                sync_tasks_service.create_delete_task_existing_document(
                    obsolete_user)

        return document

    def _depile_update(self, connector):
        try:
            local_document = self.service.get(central_id=self.central_id)
        except Exception as e:  # TODO Distinguish between not found and found >1 results
            local_document = None

        if local_document:
            from MookAPI.services import local_servers
            if local_servers._isinstance(local_document):
                return self._update_local_server(connector, local_document)

        return self._fetch_update(connector, local_document)

    def _depile_delete(self):
        try:
            local_document = self.service.get(central_id=self.central_id)
        except Exception as e:
            pass  # FIXME What should we do in that case?
        else:
            for document in reversed(local_document.all_synced_documents()):
                document.delete()

        return True

    def depile(self, connector=None):

        rv = False

        try:
            if self.type == 'update':
                rv = self._depile_update(connector=connector)
            elif self.type == 'delete':
                rv = self._depile_delete()
            else:
                self.errors.append("Unrecognized task type")
                self.save()
                return False
        except Exception as e:
            self.errors.append(str(e.message) or str(e.strerror))
            self.save()
            rv = False
        else:
            self.delete()

        return rv