class OrderingExerciseQuestion(OrderingExerciseQuestionOrderingExerciseQuestionItemJsonSerializer, ExerciseQuestion): """A list of items that need to be ordered. May be horizontal or vertical""" ## Object Id _id = db.ObjectIdField(default=ObjectId) ## Propositions items = db.ListField(db.EmbeddedDocumentField(OrderingExerciseQuestionItem)) def without_correct_answer(self): son = super(OrderingExerciseQuestion, self).without_correct_answer() shuffle(son['items']) return son def answer_with_data(self, data): return OrderingExerciseQuestionAnswer.init_with_data(data) def get_items_by_id(self, itemsId): result = [] for itemId in itemsId: print(itemId) result.append(self.get_item_by_id(itemId)) return result def get_item_by_id(self, itemId): for item in self.items: if str(item._id) == itemId: return item return None
class DropdownExerciseQuestion(DropdownExerciseQuestionJsonSerializer, ExerciseQuestion): """question where blanks need to be filled with word chosen from a dropdown list.""" ## Object Id _id = db.ObjectIdField(default=ObjectId) ## the whole text, where blanks are tagged with [%N%] where N if Nth blank of the text text = db.StringField() ## Propositions dropdowns = db.ListField( db.EmbeddedDocumentField(DropdownExerciseQuestionDropdown)) def without_correct_answer(self): son = super(DropdownExerciseQuestion, self).without_correct_answer() for dropdown in son['dropdowns']: for proposition in dropdown['propositions']: proposition.pop('is_correct_answer', None) return son def answer_with_data(self, data): return DropdownExerciseQuestionAnswer.init_with_data(data) def get_propositions_by_id(self, propositionsId): result = [] for dropdown in self.dropdowns: for proposition in dropdown.propositions: if proposition._id in propositionsId: result.append(proposition) return result
class RightOrWrongExerciseQuestion(RightOrWrongExerciseQuestionJsonSerializer, ExerciseQuestion): """Question with a right or wrong answer""" ## Object Id _id = db.ObjectIdField(default=ObjectId) ## Propositions propositions = db.ListField( db.EmbeddedDocumentField(RightOrWrongExerciseQuestionProposition)) def without_correct_answer(self): son = super(RightOrWrongExerciseQuestion, self).without_correct_answer() for proposition in son['propositions']: proposition.pop('is_correct_answer', None) return son def answer_with_data(self, data): return RightOrWrongExerciseQuestionAnswer.init_with_data(data) def get_proposition_by_id(self, propositionId): result = None for proposition in self.propositions: if proposition._id == propositionId: result = proposition return result
class MultipleAnswerMCQExerciseQuestion(MultipleAnswerMCQExerciseQuestionJsonSerializer, ExerciseQuestion): """Multiple choice question with several possible answers.""" ## Object Id _id = db.ObjectIdField(default=ObjectId) ## Propositions propositions = db.ListField(db.EmbeddedDocumentField(MultipleAnswerMCQExerciseQuestionProposition)) def without_correct_answer(self): son = super(MultipleAnswerMCQExerciseQuestion, self).without_correct_answer() for proposition in son['propositions']: proposition.pop('is_correct_answer', None) return son def answer_with_data(self, data): return MultipleAnswerMCQExerciseQuestionAnswer.init_with_data(data) def get_propositions_by_id(self, propositions_id): result = [] for proposition in self.propositions: print(str(proposition._id)) if proposition._id in propositions_id: result.append(proposition) return result
class DropdownExerciseQuestionAnswer( DropdownExerciseQuestionAnswerJsonSerializer, ExerciseQuestionAnswer): """Answers given for a dropdown question""" ## The list of chosen propositions, identified by their ObjectIds given_propositions = db.ListField(db.ObjectIdField()) @classmethod def init_with_data(cls, data): obj = cls() obj.given_propositions = [] import re for key in data: match = re.match(r"dropdown_(\w+)", key) if match: obj.given_propositions.append(ObjectId(data[key])) return obj def is_correct(self, question, parameters): propositions = question.get_propositions_by_id(self.given_propositions) all_question_propositions = [] for dropdown in question.dropdowns: all_question_propositions.extend(dropdown.propositions) correct_propositions = filter( lambda proposition: proposition.is_correct_answer, all_question_propositions) return set(propositions) == set(correct_propositions)
class UniqueAnswerMCQExerciseQuestion( UniqueAnswerMCQExerciseQuestionJsonSerializer, ExerciseQuestion): """Multiple choice question with one possible answer only.""" ## Object Id _id = db.ObjectIdField(default=ObjectId) ## Propositions propositions = db.ListField( db.EmbeddedDocumentField(UniqueAnswerMCQExerciseQuestionProposition)) def without_correct_answer(self): son = super(UniqueAnswerMCQExerciseQuestion, self).without_correct_answer() for proposition in son['propositions']: proposition.pop('is_correct_answer', None) return son def answer_with_data(self, data): return UniqueAnswerMCQExerciseQuestionAnswer.init_with_data(data) def get_proposition_by_id(self, propositionId): result = None for proposition in self.propositions: if proposition._id == propositionId: result = proposition return result
class CategorizeExerciseQuestion(CategorizeExerciseQuestionJsonSerializer, ExerciseQuestion): """A list of items that need to be categorized.""" ## Object Id _id = db.ObjectIdField(default=ObjectId) ## categories categories = db.ListField( db.EmbeddedDocumentField(CategorizeExerciseQuestionCategory)) def without_correct_answer(self): son = super(CategorizeExerciseQuestion, self).without_correct_answer() all_items = [] for category in son['categories']: all_items.extend(category.pop('items')) shuffle(all_items) son['items'] = all_items return son def answer_with_data(self, data): return CategorizeExerciseQuestionAnswer.init_with_data(data) def get_category_by_id(self, category_id): for category in self.categories: if category._id == category_id: return category def get_items_in_category_by_id(self, category_id, items_id): result = [] category = self.get_category_by_id(category_id) for item in category.items: if item._id in items_id: result.append(item) return result
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 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 DropdownExerciseQuestionDropdown( DropdownExerciseQuestionDropdownJsonSerializer, db.EmbeddedDocument): """Stores a list of propositions to a blank field in a text.""" ## Object Id _id = db.ObjectIdField(default=ObjectId) ## Text propositions = db.ListField( db.EmbeddedDocumentField(DropdownExerciseQuestionProposition))
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 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 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 CategorizeExerciseQuestionAnswer( CategorizeExerciseQuestionAnswerJsonSerializer, ExerciseQuestionAnswer): """categorized items given for this Categorize question.""" ## The categories sent by the client, identified by their ObjectIds. given_categories = db.ListField(db.ObjectIdField()) ## The categorized items, identified by their ObjectIds, in the requested categories. ## The first level order is the same as the given_categories given_categorized_items = db.ListField(db.ListField(db.ObjectIdField())) @classmethod def init_with_data(cls, data): obj = cls() obj.given_categories = [] obj.given_categorized_items = [] for category, items in data['categorized_items'].iteritems(): obj.given_categories.append(ObjectId(category)) categorized_items = [] for given_item in items: categorized_items.append(ObjectId(given_item)) obj.given_categorized_items.append(categorized_items) return obj def is_correct(self, question, parameters): answer_categories_items = self.given_categorized_items result = True for i in range(0, len(answer_categories_items)): category_items = question.get_items_in_category_by_id( self.given_categories[i], self.given_categorized_items[i]) correct_category = question.get_category_by_id( self.given_categories[i]) if set(category_items) != set(correct_category.items): result = False break return result
class OrderingExerciseQuestionAnswer(OrderingExerciseQuestionAnswerJsonSerializer, ExerciseQuestionAnswer): """Ordered items given for this ordering question.""" ## The given propositions, identified by their ObjectIds given_ordered_items = db.ListField(db.ObjectIdField()) @classmethod def init_with_data(cls, data): obj = cls() obj.given_ordered_items = data['ordered_items'] return obj def is_correct(self, question, parameters): ordered_items = question.get_items_by_id(self.given_ordered_items) correct_ordered_items = question.items return ordered_items == correct_ordered_items
class MultipleAnswerMCQExerciseQuestionAnswer(MultipleAnswerMCQExerciseQuestionAnswerJsonSerializer, ExerciseQuestionAnswer): """Answers given to a multiple-answer MCQ.""" ## The list of chosen propositions, identified by their ObjectIds given_propositions = db.ListField(db.ObjectIdField()) @classmethod def init_with_data(cls, data): obj = cls() obj.given_propositions = [] for proposition in data['propositions']: obj.given_propositions.append(ObjectId(proposition)) return obj def is_correct(self, question, parameters): propositions = question.get_propositions_by_id(self.given_propositions) correct_propositions = filter(lambda proposition: proposition.is_correct_answer, question.propositions) return set(propositions) == set(correct_propositions)
class UniqueAnswerMCQExerciseQuestionAnswer( UniqueAnswerMCQExerciseQuestionAnswerJsonSerializer, ExerciseQuestionAnswer): """Answer given to a unique-answer MCQ.""" ## The chosen propositions, identified by its ObjectId given_proposition = db.ObjectIdField() @classmethod def init_with_data(cls, data): obj = cls() obj.given_proposition = data['proposition'] return obj def is_correct(self, question, parameters): proposition = question.get_proposition_by_id( ObjectId(self.given_proposition)) if (proposition != None): return proposition.is_correct_answer return False
class SyncableDocument(SyncableDocumentJsonSerializer, db.Document): """ .. _SyncableDocument: An abstract class for any document that needs to be synced between the central server and a local server. """ meta = {'allow_inheritance': True, 'abstract': True} ## Last modification last_modification = db.DateTimeField() """ The date of the last modification on the document. Used to determine whether the document has changed since the last synchronization of a local server. """ ## Id of the document on the central server central_id = db.ObjectIdField() """The id of the document on the central server.""" # A placeholder for unresolved references that need to be saved after the document is saved unresolved_references = [] @property def url(self, _external=False): """ The URL where a JSON representation of the document based on MongoCoderMixin_'s encode_mongo_ method can be found. .. warning:: Subclasses of MongoCoderDocument should implement this method. """ raise exceptions.NotImplementedError( "The single-object URL of this document class is not defined.") def top_level_syncable_document(self): """ If a ``SyncableDocument`` has child documents, this function returns the top-level parent document. Defaults to ``self``. .. note:: Override this method if the document is a child of a ``SyncableDocument``. """ return self def save(self, *args, **kwargs): self.last_modification = datetime.datetime.now() rv = super(SyncableDocument, self).save(*args, **kwargs) for ref in self.unresolved_references: ref.document = self ref.save() return rv def delete(self, *args, **kwargs): reference = DeletedSyncableDocument() reference.document = self reference.top_level_document = self.top_level_syncable_document() reference.save() return super(SyncableDocument, self).delete(*args, **kwargs) def all_synced_documents(self, local_server=None): """ Returns the list of references to atomic documents that should be looked at when syncing this document. Defaults to a one-element list containing a reference to self. .. note:: Override this method if this document has children documents. Children documents who reference this document should be inserted AFTER self. Children documents referenced from this document should be inserted BEFORE self. """ return [self] def items_to_update(self, local_server): """ .. _items_to_update: Returns the list of references to atomic documents that have changed since the last synchronization. Defaults to a one-element list containing a reference to self. .. note:: Override this method if this document has children documents. """ items = [] last_sync = local_server.last_sync for item in self.all_synced_documents(local_server=local_server): if last_sync is None or item.last_modification is None or last_sync < item.last_modification: items.append(item) return items def items_to_delete(self, local_server): """ .. _items_to_delete: Returns the list of references to atomic documents that have been deleted since the last synchronization. This method will also automatically check for any deleted children documents (no need to override as long as ``top_level_document`` is overridden). """ items = [] last_sync = local_server.last_sync for obj in DeletedSyncableDocument.objects.no_dereference().filter( top_level_document=self.top_level_syncable_document()): if last_sync is None or obj.date is None or last_sync < obj.date: items.append(obj.document) return items def items_to_sync(self, local_server=None): """ Returns a dictionary ``dict`` with two keys: * ``dict['update']`` contains the results of the items_to_update_ method; * ``dict['delete']`` contains the results of the items_to_delete_ method. .. todo:: Remove items that are in the ``delete`` list from the ``update`` list. """ items = {} items['update'] = self.items_to_update(local_server=local_server) items['delete'] = self.items_to_delete(local_server=local_server) ## We should do some cleanup at this point, in particular remove deletable items from 'update' list. return items def __unicode__(self): return "Document with class %s and id %s" % (self.__class__.__name__, str(self.id))
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