class LearnerProgressNotification(models.Model): id = (models.AutoField(auto_created=True, primary_key=True, serialize=True, verbose_name="ID"), ) notification_object = models.CharField( max_length=200, choices=NotificationObjectType.choices(), blank=True) notification_event = models.CharField( max_length=200, choices=NotificationEventType.choices(), blank=True) user_id = UUIDField() classroom_id = UUIDField() # This is a Classroom id assignment_collections = JSONField(null=True, default=[]) contentnode_id = UUIDField(null=True) lesson_id = UUIDField(null=True) quiz_id = UUIDField(null=True) quiz_num_correct = models.IntegerField(null=True) quiz_num_answered = models.IntegerField(null=True) reason = models.CharField(max_length=200, choices=HelpReason.choices(), blank=True) timestamp = DateTimeTzField(default=local_now) def __str__(self): return "{object} - {event}".format(object=self.notification_object, event=self.notification_event) class Meta: app_label = "notifications"
class ContentSummaryLog(BaseLogModel): """ This model provides an aggregate summary of all recorded interactions a user has had with a content item over time. """ # Morango syncing settings morango_model_name = "contentsummarylog" user = models.ForeignKey(FacilityUser) content_id = UUIDField(db_index=True) channel_id = UUIDField() start_timestamp = DateTimeTzField() end_timestamp = DateTimeTzField(blank=True, null=True) completion_timestamp = DateTimeTzField(blank=True, null=True) time_spent = models.FloatField(help_text="(in seconds)", default=0.0, validators=[MinValueValidator(0)]) progress = models.FloatField( default=0, validators=[MinValueValidator(0), MaxValueValidator(1.01)]) kind = models.CharField(max_length=200) extra_fields = JSONField(default={}, blank=True) def calculate_source_id(self): return self.content_id def save(self, *args, **kwargs): if self.progress < 0 or self.progress > 1.01: raise ValidationError( "Content summary progress out of range (0-1)") super(ContentSummaryLog, self).save(*args, **kwargs)
class ContentSessionLog(BaseLogModel): """ This model provides a record of interactions with a content item within a single visit to that content page. """ # Morango syncing settings morango_model_name = "contentsessionlog" user = models.ForeignKey(FacilityUser, blank=True, null=True) content_id = UUIDField(db_index=True) visitor_id = models.UUIDField(blank=True, null=True) channel_id = UUIDField() start_timestamp = DateTimeTzField() end_timestamp = DateTimeTzField(blank=True, null=True) time_spent = models.FloatField(help_text="(in seconds)", default=0.0, validators=[MinValueValidator(0)]) progress = models.FloatField(default=0, validators=[MinValueValidator(0)]) kind = models.CharField(max_length=200) extra_fields = JSONField(default={}, blank=True) def save(self, *args, **kwargs): if self.progress < 0: raise ValidationError("Progress out of range (<0)") super(ContentSessionLog, self).save(*args, **kwargs)
class ContentNode(MPTTModel): """ The primary object type in a content database. Defines the properties that are shared across all content types. It represents videos, exercises, audio, documents, and other 'content items' that exist as nodes in content channels. """ id = UUIDField(primary_key=True) parent = TreeForeignKey("self", null=True, blank=True, related_name="children", db_index=True) license_name = models.CharField(max_length=50, null=True, blank=True) license_description = models.TextField(null=True, blank=True) has_prerequisite = models.ManyToManyField("self", related_name="prerequisite_for", symmetrical=False, blank=True) related = models.ManyToManyField("self", symmetrical=True, blank=True) tags = models.ManyToManyField("ContentTag", symmetrical=False, related_name="tagged_content", blank=True) title = models.CharField(max_length=200) coach_content = models.BooleanField(default=False) # the content_id is used for tracking a user's interaction with a piece of # content, in the face of possibly many copies of that content. When a user # interacts with a piece of content, all substantially similar pieces of # content should be marked as such as well. We track these "substantially # similar" types of content by having them have the same content_id. content_id = UUIDField(db_index=True) channel_id = UUIDField(db_index=True) description = models.TextField(blank=True, null=True) sort_order = models.FloatField(blank=True, null=True) license_owner = models.CharField(max_length=200, blank=True) author = models.CharField(max_length=200, blank=True) kind = models.CharField(max_length=200, choices=content_kinds.choices, blank=True) available = models.BooleanField(default=False) lang = models.ForeignKey("Language", blank=True, null=True) class Meta: abstract = True
class AssessmentMetaData(models.Model): """ A model to describe additional metadata that characterizes assessment behaviour in Kolibri. This model contains additional fields that are only revelant to content nodes that probe a user's state of knowledge and allow them to practice to Mastery. ContentNodes with this metadata may also be able to be used within quizzes and exams. """ id = UUIDField(primary_key=True) contentnode = models.ForeignKey("ContentNode", related_name="assessmentmetadata") # A JSON blob containing a serialized list of ids for questions that the assessment can present. assessment_item_ids = JSONField(default=[]) # Length of the above assessment_item_ids for a convenience lookup. number_of_assessments = models.IntegerField() # A JSON blob describing the mastery model that is used to set this assessment as mastered. mastery_model = JSONField(default={}) # Should the questions listed in assessment_item_ids be presented in a random order? randomize = models.BooleanField(default=False) # Is this assessment compatible with being previewed and answer filled for display in coach reports # and use in summative and formative tests? is_manipulable = models.BooleanField(default=False) class Meta: abstract = True
class DeviceAppKey(models.Model): """ This class stores a key that is checked to make sure that a webview is making requests from a privileged device (i.e. from inside an app-wrapper webview) """ key = UUIDField(default=uuid4) def save(self, *args, **kwargs): self.pk = 1 super(DeviceAppKey, self).save(*args, **kwargs) @classmethod def update_app_key(cls): app_key, created = cls.objects.get_or_create() app_key.key = uuid4().hex app_key.save() cache.set(APP_KEY_CACHE_KEY, app_key.key, 5000) return app_key @classmethod def get_app_key(cls): key = cache.get(APP_KEY_CACHE_KEY) if key is None: try: app_key = cls.objects.get() except cls.DoesNotExist: app_key = cls.update_app_key() key = app_key.key cache.set(APP_KEY_CACHE_KEY, key, 5000) return key
class NotificationsLog(models.Model): id = (models.AutoField(auto_created=True, primary_key=True, serialize=True, verbose_name="ID"), ) coach_id = UUIDField() timestamp = DateTimeTzField(default=local_now) def __str__(self): return self.coach_id class Meta: app_label = "notifications"
class Bookmark(AbstractFacilityDataModel): content_id = UUIDField(blank=True, null=True) channel_id = UUIDField(blank=True, null=True) contentnode_id = UUIDField() user = models.ForeignKey(FacilityUser, blank=False) morango_model_name = "bookmark" permissions = IsOwn() def infer_dataset(self, *args, **kwargs): if self.user_id: return self.cached_related_dataset_lookup("user") elif self.dataset_id: # confirm that there exists a facility with that dataset_id try: return Facility.objects.get( dataset_id=self.dataset_id).dataset_id except Facility.DoesNotExist: pass # if no user or matching facility, infer dataset from the default facility facility = Facility.get_default_facility() assert facility, "Before you can save bookmarks, you must have a facility" return facility.dataset_id def calculate_partition(self): return "{dataset_id}:user-rw:{user_id}".format( dataset_id=self.dataset_id, user_id=self.user.id) class Meta: # Ensures that we do not save duplicates, otherwise raises a # django.db.utils.IntegrityError unique_together = ( "user", "contentnode_id", )
class ChannelMetadata(models.Model): """ Holds metadata about all existing content databases that exist locally. """ id = UUIDField(primary_key=True) name = models.CharField(max_length=200) description = models.CharField(max_length=400, blank=True) author = models.CharField(max_length=400, blank=True) version = models.IntegerField(default=0) thumbnail = models.TextField(blank=True) last_updated = DateTimeTzField(null=True) # Minimum version of Kolibri that this content database is compatible with min_schema_version = models.CharField(max_length=50) root = models.ForeignKey("ContentNode") class Meta: abstract = True
class ExamAttemptLog(BaseAttemptLog): """ This model provides a summary of a user's interactions with a question in an exam """ morango_model_name = "examattemptlog" examlog = models.ForeignKey( ExamLog, related_name="attemptlogs", blank=False, null=False ) # We have no session logs associated with ExamLogs, so we need to record the channel and content # ids here content_id = UUIDField() def infer_dataset(self, *args, **kwargs): return self.cached_related_dataset_lookup("examlog") def calculate_partition(self): return self.dataset_id
class File(models.Model): """ The second to bottom layer of the contentDB schema, defines the basic building brick for content. Things it can represent are, for example, mp4, avi, mov, html, css, jpeg, pdf, mp3... """ id = UUIDField(primary_key=True) # The foreign key mapping happens here as many File objects can map onto a single local file local_file = models.ForeignKey("LocalFile", related_name="files") contentnode = models.ForeignKey("ContentNode", related_name="files") preset = models.CharField(max_length=150, choices=format_presets.choices, blank=True) lang = models.ForeignKey("Language", blank=True, null=True) supplementary = models.BooleanField(default=False) thumbnail = models.BooleanField(default=False) priority = models.IntegerField(blank=True, null=True, db_index=True) class Meta: abstract = True
class SyncQueue(models.Model): """ This class maintains the queue of the devices that try to sync with this server """ id = UUIDField(primary_key=True, default=uuid4) user = models.ForeignKey(FacilityUser, on_delete=models.CASCADE, null=False) datetime = models.DateTimeField(auto_now_add=True) updated = models.FloatField(default=time.time) # polling interval is 5 seconds by default keep_alive = models.FloatField(default=5.0) @classmethod def clean_stale(cls, expire=180.0): """ This method will delete all the devices from the queue with the expire time (in seconds) exhausted """ staled_time = time.time() - expire cls.objects.filter(updated__lte=staled_time).delete()
class ContentTag(models.Model): id = UUIDField(primary_key=True) tag_name = models.CharField(max_length=30, blank=True) class Meta: abstract = True
if offset > 0: in_clause_elements.append(" OR ") in_clause_elements.append("%s IN (" % lhs) params.extend(lhs_params) sqls_params = tuple() param_group = ("(" + ",".join( "'{}'".format(p) for p in rhs_params[offset:offset + max_in_list_size]) + ")") in_clause_elements.append(param_group) in_clause_elements.append(")") params.extend(sqls_params) in_clause_elements.append(")") return "".join(in_clause_elements), params UUIDField.register_lookup(UUIDIn) CharField.register_lookup(UUIDIn) ForeignKey.register_lookup(UUIDIn) class FilterByUUIDQuerysetMixin(object): """ As a workaround to the SQLITE_MAX_VARIABLE_NUMBER, so we can avoid having to chunk our queries, we pass in the list of ids (after being validated) as an inline query statement. """ def filter_by_uuids(self, ids, validate=True): id_field = self.model._meta.pk.attname return self._by_uuids(ids, validate, id_field, True) def exclude_by_uuids(self, ids, validate=True): id_field = self.model._meta.pk.attname