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)
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)
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)
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)
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
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
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()
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( )