Example #1
0
class MultipleAnswerMCQExerciseQuestion(MultipleAnswerMCQExerciseQuestionJsonSerializer, ExerciseQuestion):
    """Multiple choice question with several possible answers."""

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

    ## Propositions
    propositions = db.ListField(db.EmbeddedDocumentField(MultipleAnswerMCQExerciseQuestionProposition))

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

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

    def get_propositions_by_id(self, propositions_id):
        result = []
        for proposition in self.propositions:
            print(str(proposition._id))
            if proposition._id in propositions_id:
                result.append(proposition)
        return result
Example #2
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
class RightOrWrongExerciseQuestion(RightOrWrongExerciseQuestionJsonSerializer,
                                   ExerciseQuestion):
    """Question with a right or wrong answer"""

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

    ## Propositions
    propositions = db.ListField(
        db.EmbeddedDocumentField(RightOrWrongExerciseQuestionProposition))

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

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

    def get_proposition_by_id(self, propositionId):
        result = None
        for proposition in self.propositions:
            if proposition._id == propositionId:
                result = proposition
        return result
Example #4
0
class CategorizeExerciseQuestion(CategorizeExerciseQuestionJsonSerializer,
                                 ExerciseQuestion):
    """A list of items that need to be categorized."""

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

    ## categories
    categories = db.ListField(
        db.EmbeddedDocumentField(CategorizeExerciseQuestionCategory))

    def without_correct_answer(self):
        son = super(CategorizeExerciseQuestion, self).without_correct_answer()
        all_items = []
        for category in son['categories']:
            all_items.extend(category.pop('items'))
        shuffle(all_items)
        son['items'] = all_items
        return son

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

    def get_category_by_id(self, category_id):
        for category in self.categories:
            if category._id == category_id:
                return category

    def get_items_in_category_by_id(self, category_id, items_id):
        result = []
        category = self.get_category_by_id(category_id)
        for item in category.items:
            if item._id in items_id:
                result.append(item)
        return result
Example #5
0
class UniqueAnswerMCQExerciseQuestion(
        UniqueAnswerMCQExerciseQuestionJsonSerializer, ExerciseQuestion):
    """Multiple choice question with one possible answer only."""

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

    ## Propositions
    propositions = db.ListField(
        db.EmbeddedDocumentField(UniqueAnswerMCQExerciseQuestionProposition))

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

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

    def get_proposition_by_id(self, propositionId):
        result = None
        for proposition in self.propositions:
            if proposition._id == propositionId:
                result = proposition
        return result
Example #6
0
class OrderingExerciseQuestion(OrderingExerciseQuestionOrderingExerciseQuestionItemJsonSerializer, ExerciseQuestion):
    """A list of items that need to be ordered. May be horizontal or vertical"""

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

    ## Propositions
    items = db.ListField(db.EmbeddedDocumentField(OrderingExerciseQuestionItem))

    def without_correct_answer(self):
        son = super(OrderingExerciseQuestion, self).without_correct_answer()
        shuffle(son['items'])
        return son

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

    def get_items_by_id(self, itemsId):
        result = []
        for itemId in itemsId:
            print(itemId)
            result.append(self.get_item_by_id(itemId))
        return result

    def get_item_by_id(self, itemId):
        for item in self.items:
            if str(item._id) == itemId:
                return item
        return None
Example #7
0
class DropdownExerciseQuestionDropdown(
        DropdownExerciseQuestionDropdownJsonSerializer, db.EmbeddedDocument):
    """Stores a list of propositions to a blank field in a text."""

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

    ## Text
    propositions = db.ListField(
        db.EmbeddedDocumentField(DropdownExerciseQuestionProposition))
Example #8
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))
Example #9
0
class ExternalVideoResource(ExternalVideoResourceJsonSerializer, Resource):
    """References a video from the Internet."""

    resource_content = db.EmbeddedDocumentField(ExternalVideoResourceContent)
Example #10
0
class VideoResource(VideoResourceJsonSerializer, LinkedFileResource):
    """Stores a video file in the database."""

    resource_content = db.EmbeddedDocumentField(VideoResourceContent)
Example #11
0
class ExerciseResource(ExerciseResourceJsonSerializer, Resource):
    """An exercise with a list of questions."""

    resource_content = db.EmbeddedDocumentField(ExerciseResourceContent)

    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)

    @property
    def questions(self):
        """A shorthand getter for the list of questions in the resource content."""

        questions = self.resource_content.questions
        return self._add_instance(questions)

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

        question = self.resource_content.question(question_id)
        return self._add_instance(question)

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

        questions = self.resource_content.random_questions(number)
        return self._add_instance(questions)

    def all_synced_documents(self, local_server=None):
        items = []
        if self.resource_content.fail_linked_resource:
            items.append(self.resource_content.fail_linked_resource)
        items.extend(
            super(ExerciseResource,
                  self).all_synced_documents(local_server=local_server))
        return items

    def user_analytics(self, user):
        rv = super(ExerciseResource, self).user_analytics(user)

        from MookAPI.services import exercise_attempts
        exercise_attempts = exercise_attempts.find(
            user=user, exercise=self).order_by('-date')
        rv['last_attempts_scores'] = map(
            lambda a: {
                "date": a.date,
                "nb_questions": a.nb_questions,
                "score": a.nb_right_answers
            }, exercise_attempts[:5])

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

        rv['nb_attempts'] = exercise_attempts.count()

        return rv
Example #12
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)
Example #13
0
class AudioResource(AudioResourceJsonSerializer, LinkedFileResource):
    """Stores an audio file in the database."""

    resource_content = db.EmbeddedDocumentField(AudioResourceContent)
Example #14
0
class RichTextResource(RichTextResourceJsonSerializer, Resource):
    """Store rich text content."""

    resource_content = db.EmbeddedDocumentField(RichTextResourceContent)
class SkillValidationAttempt(SkillValidationAttemptJsonSerializer, Activity):
    """
    Records any attempt at an exercise.
    """

    ### PROPERTIES

    ## Skill
    skill = db.ReferenceField('Skill')

    @property
    def object(self):
        return self.skill

    ## Question answers
    question_answers = db.ListField(
        db.EmbeddedDocumentField(ExerciseAttemptQuestionAnswer))

    is_validated = db.BooleanField(default=False)
    """ Is exercise validated """

    end_date = db.DateTimeField()
    """ The date when the attempt is ended """

    @property
    def max_mistakes(self):
        if self.skill and not isinstance(self.skill, DBRef):
            return self.skill.validation_exercise.max_mistakes
        return None

    @property
    def nb_right_answers(self):
        return len(
            filter(lambda qa: qa.is_answered_correctly, self.question_answers))

    @property
    def nb_questions(self):
        return len(self.question_answers)

    @property
    def duration(self):
        if not self.end_date:
            return None
        else:
            return self.end_date - self.date

    ### METHODS

    @classmethod
    def init_with_skill(cls, skill):
        """Initiate an validation attempt for a given skill."""
        obj = cls()
        obj.skill = skill

        questions = skill.random_questions()
        obj.question_answers = map(
            lambda q: ExerciseAttemptQuestionAnswer.init_with_question(q),
            questions)

        return obj

    def clean(self):
        super(SkillValidationAttempt, self).clean()
        self.type = "exercise_attempt"

    def __unicode__(self):
        if self.skill is not None:
            return self.skill.title
        return self.id

    def question_answer(self, question_id):
        oid = ObjectId(question_id)
        for question_answer in self.question_answers:
            if question_answer.question_id == oid:
                return question_answer
        raise KeyError("Question not found.")

    def set_question_answer(self, question_id, question_answer):
        oid = ObjectId(question_id)
        for (index, qa) in enumerate(self.question_answers):
            if qa.question_id == oid:
                self.question_answers[index] = question_answer
                return question_answer
        raise KeyError("Question not found.")

    def save_answer(self, question_id, data):
        """
        Saves an answer (ExerciseQuestionAnswer) to a question (referenced by its ObjectId).
        """

        question = self.skill.question(question_id)
        attempt_question_answer = self.question_answer(question_id)
        question_answer = question.answer_with_data(data)
        attempt_question_answer.given_answer = question_answer
        attempt_question_answer.is_answered_correctly = question_answer.is_correct(
            question, attempt_question_answer.parameters)
        self.set_question_answer(question_id, attempt_question_answer)

    def start_question(self, question_id):
        """
        Records the datetime at which the given question has been started
        :param question_id: the id of the question which has just been started
        """
        attempt_question_answer = self.question_answer(question_id)
        attempt_question_answer.asked_date = datetime.datetime.now

    def end(self):
        """
        Records the "now" date as the date when the user has finished the attempt
        """
        self.end_date = datetime.datetime.now

    def is_skill_validation_validated(self):
        nb_total_questions = len(self.question_answers)
        answered_questions = filter(lambda a: a.given_answer is not None,
                                    self.question_answers)
        return len(answered_questions) >= nb_total_questions

    def is_attempt_completed(self):
        nb_total_questions = len(self.question_answers)
        nb_max_mistakes = self.skill.validation_exercise.max_mistakes
        answered_questions = filter(lambda a: a.given_answer is not None,
                                    self.question_answers)
        if len(answered_questions) >= nb_total_questions:
            right_answers = filter(lambda a: a.is_answered_correctly,
                                   answered_questions)
            if len(right_answers) >= nb_total_questions - nb_max_mistakes:
                return True

        return False
Example #16
0
class LinkedFileResource(Resource):
    """Stores a file in the database."""

    resource_content = db.EmbeddedDocumentField(LinkedFileResourceContent)
Example #17
0
class ExerciseResourceContent(ExerciseResourceContentJsonSerializer,
                              ResourceContent):
    unique_answer_mcq_questions = db.ListField(
        db.EmbeddedDocumentField(UniqueAnswerMCQExerciseQuestion))
    """A (possibly empty) list of unique-answer multiple-choice questions (`UniqueAnswerMCQExerciseQuestion`)."""

    multiple_answer_mcq_questions = db.ListField(
        db.EmbeddedDocumentField(MultipleAnswerMCQExerciseQuestion))
    """A (possibly empty) list of multiple-answer multiple-choice questions (`MultipleAnswerMCQExerciseQuestion`)."""

    right_or_wrong_questions = db.ListField(
        db.EmbeddedDocumentField(RightOrWrongExerciseQuestion))
    """A (possibly empty) list of multiple-answer multiple-choice questions (`RightOrWrongExerciseQuestion`)."""

    dropdown_questions = db.ListField(
        db.EmbeddedDocumentField(DropdownExerciseQuestion))
    """A (possibly empty) list of dropdown questions (`DropdownExerciseQuestion`)."""

    ordering_questions = db.ListField(
        db.EmbeddedDocumentField(OrderingExerciseQuestion))
    """A (possibly empty) list of ordering questions (`OrderingExerciseQuestion`)."""

    categorize_questions = db.ListField(
        db.EmbeddedDocumentField(CategorizeExerciseQuestion))
    """A (possibly empty) list of categorizing questions (`CategorizeExerciseQuestion`)."""
    @property
    def questions(self):
        """A list of all questions, whatever their type."""

        questions = []
        questions.extend(self.unique_answer_mcq_questions)
        questions.extend(self.multiple_answer_mcq_questions)
        questions.extend(self.right_or_wrong_questions)
        questions.extend(self.dropdown_questions)
        questions.extend(self.ordering_questions)
        questions.extend(self.categorize_questions)

        return questions

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

        oid = bson.ObjectId(question_id)
        for question in self.questions:
            if question._id == oid:
                return question

        raise exceptions.KeyError("Question not found.")

    def random_questions(self, number=None):
        """
        A list of random questions.
        If `number` is not specified, it will be set to the exercise's `number_of_questions` property.
        The list will contain `number` questions, or all questions if there are not enough questions in the exercise.
        """

        if not number:
            number = self.number_of_questions or len(self.questions)

        all_questions = self.questions
        random.shuffle(all_questions)
        return all_questions[:number]

    number_of_questions = db.IntField()
    """The number of questions to ask from this exercise."""

    max_mistakes = db.IntField()
    """The number of mistakes authorized before failing the exercise."""

    fail_linked_resource = db.ReferenceField(Resource)
    """A resource to look again when failing the exercise."""

    def clean(self):
        super(ExerciseResourceContent, self).clean()
        # FIXME This should be done in validate and raise an error. Do that when MongoEngine is fixed.
        if self.fail_linked_resource:
            if self.fail_linked_resource.track != self._instance.track:
                self.fail_linked_resource = None
Example #18
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
class DownloadableFileResource(DownloadableFileResourceJsonSerializer,
                               LinkedFileResource):
    """Stores a downloadable file in the database."""

    resource_content = db.EmbeddedDocumentField(
        DownloadableFileResourceContent)