class Role(RoleJsonSerializer, SyncableDocument): name = db.StringField(max_length=80, unique=True) description = db.StringField() def __unicode__(self): return self.name
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)
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."""
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)
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."""
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 OrderingExerciseQuestionItem(OrderingExerciseQuestionItemJsonSerializer, db.EmbeddedDocument): """Stores an item for the overall ordering question.""" ## Object Id _id = db.ObjectIdField(default=ObjectId) ## Text text = db.StringField()
class CategorizeExerciseQuestionItem(CategorizeExerciseQuestionItem, db.EmbeddedDocument): """Stores an item that belongs to one category.""" ## Object Id _id = db.ObjectIdField(default=ObjectId) ## Text text = db.StringField()
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 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 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()
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 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)
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)
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 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)
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( )
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)
class RichTextResourceContent(RichTextResourceContentJsonSerializer, ResourceContent): html = db.StringField(required=True) """An HTML string containing the rich text."""
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' ]
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)
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 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