Ejemplo n.º 1
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)
Ejemplo n.º 2
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)
Ejemplo n.º 3
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)
Ejemplo n.º 4
0
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)
Ejemplo n.º 5
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 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
Ejemplo n.º 7
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
Ejemplo n.º 8
0
class Track(TrackJsonSerializer, ResourceHierarchy):
    """
    .. _Track:

    Top level of Resource_ hierarchy. Their descendants are Skill_ objects.
    """

    is_active = db.BooleanField(default=True)

    icon = db.ImageField()
    """An icon to illustrate the Track_."""
    @property
    def icon_url(self, _external=True):
        """The URL where the track icon can be downloaded."""
        return url_for("hierarchy.get_track_icon",
                       track_id=self.id,
                       _external=_external)

    ### VIRTUAL PROPERTIES

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

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

        return skills.find(track=self).order_by('order', 'title')

    @property
    def skills_refs(self):
        return [skill.to_json_dbref() for skill in self.skills]

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

        return completed_tracks.find(track=self, user=user).count() > 0

    def user_progress(self, user):
        current = 0
        for skill in self.skills:
            if skill.is_validated_by_user(user):
                current += 1
        return {'current': current, 'max': len(self.skills)}

    def is_started_by_user(self, user):
        from MookAPI.services import started_tracks

        return started_tracks.find(track=self, user=user).count() > 0

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

    def test_is_unlocked_by_user(self, user):
        from MookAPI.services import unlocked_track_tests

        return unlocked_track_tests.find(track=self, user=user).count() > 0

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

    @property
    def track_validation_tests(self):
        """A queryset of the TrackValidationResource_ objects that belong to the current Track_."""
        from MookAPI.services import track_validation_resources

        return track_validation_resources.find(parent=self).order_by(
            'order', 'title')

    @property
    def validation_test(self):
        number_of_tests = len(self.track_validation_tests)
        if len(self.track_validation_tests) > 0:
            i = random.randrange(0, number_of_tests)
            return self.track_validation_tests[i].to_json_dbref()
        return None

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

    ### METHODS

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

        from MookAPI.services import track_validation_attempts, visited_tracks

        track_validation_attempts = track_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
            }, track_validation_attempts[:5])

        nb_finished_attempts = 0
        total_duration = datetime.timedelta(0)
        for attempt in track_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'] = track_validation_attempts.count()
        analytics['nb_visits'] = visited_tracks.find(user=user,
                                                     track=self).count()

        return analytics

    def user_info(self, user, analytics=False):
        rv = super(Track, self).user_info(user=user, analytics=analytics)

        rv['is_started'] = self.is_started_by_user(user)
        rv['test_is_unlocked'] = self.test_is_unlocked_by_user(user)

        return rv

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

        for skill in self.skills:
            items.extend(skill.all_synced_documents(local_server=local_server))

        for test in self.track_validation_tests:
            items.extend(test.all_synced_documents(local_server=local_server))

        return items

    @classmethod
    def pre_delete(cls, sender, document, **kwargs):
        from MookAPI.services import local_servers
        for local_server in local_servers.find(synced_tracks=document):
            # The track is automatically pulled from the local server's sync list
            # thanks to the "reverse_delete_rule=PULL" option.
            # We just need to save the LS in order to update its LS last modification date
            local_server.save()
Ejemplo n.º 9
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(
        )