Beispiel #1
0
class MasteryLog(BaseLogModel):
    """
    This model provides a summary of a user's engagement with an assessment within a mastery level
    """
    # Morango syncing settings
    morango_model_name = "masterylog"

    user = models.ForeignKey(FacilityUser)
    # Every MasteryLog is related to the single summary log for the user/content pair
    summarylog = models.ForeignKey(ContentSummaryLog,
                                   related_name="masterylogs")
    # The MasteryLog records the mastery criterion that has been specified for the user.
    # It is recorded here to prevent this changing in the middle of a user's engagement
    # with an assessment.
    mastery_criterion = JSONField(default={})
    start_timestamp = DateTimeTzField()
    end_timestamp = DateTimeTzField(blank=True, null=True)
    completion_timestamp = DateTimeTzField(blank=True, null=True)
    # The integer mastery level that this log is tracking.
    mastery_level = models.IntegerField(
        validators=[MinValueValidator(1),
                    MaxValueValidator(10)])
    # Has this mastery level been completed?
    complete = models.BooleanField(default=False)

    def infer_dataset(self, *args, **kwargs):
        return self.user.dataset

    def calculate_source_id(self):
        return "{summarylog_id}:{mastery_level}".format(
            summarylog_id=self.summarylog_id, mastery_level=self.mastery_level)
Beispiel #2
0
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)
Beispiel #3
0
class ReportsDataOffline(models.Model):
    """
    The top layer of the contentDB schema, defines the most common properties that are shared across all different contents.
    Things it can represent are, for example, video, exercise, audio or document...
    """
    class_id = UUIDField(db_index=True)
    student_id = UUIDField(db_index=True)
    number_of_attempt = models.IntegerField()
    course_id = UUIDField(db_index=True)
    unit_id = UUIDField(db_index=True)
    lesson_id = UUIDField(db_index=True)
    collection_id = UUIDField(db_index=True)
    collection_type = models.CharField(max_length=50, null=True, blank=True)
    content_id = UUIDField(db_index=True)  #Collection/Assesment Id
    content_type = models.CharField(max_length=50, null=True, blank=True)
    time_spent = models.FloatField(help_text="(in seconds)",
                                   default=0.0,
                                   validators=[MinValueValidator(0)])
    reaction = models.IntegerField(null=True, blank=True)
    student_response = JSONField(default=[], blank=True)
    attended = models.BooleanField(default=False)
    score = models.BooleanField(default=False)
    created_by = models.ForeignKey(FacilityUser,
                                   related_name='created_user',
                                   blank=False,
                                   null=False)
    created_date = DateTimeTzField(default=local_now, editable=False)
    modified_by = models.ForeignKey(FacilityUser, blank=False, null=False)
    modified_date = DateTimeTzField(default=local_now, editable=False)
Beispiel #4
0
class BaseAttemptLog(BaseLogModel):
    """
    This is an abstract model that provides a summary of a user's engagement within a particular
    interaction with an item/question in an assessment
    """
    # Unique identifier within the relevant assessment for the particular question/item
    # that this attemptlog is a record of an interaction with.
    item = models.CharField(max_length=200)
    start_timestamp = DateTimeTzField()
    end_timestamp = DateTimeTzField()
    completion_timestamp = DateTimeTzField(blank=True, null=True)
    time_spent = models.FloatField(help_text="(in seconds)",
                                   default=0.0,
                                   validators=[MinValueValidator(0)])
    complete = models.BooleanField(default=False)
    # How correct was their answer? In simple cases, just 0 or 1.
    correct = models.FloatField(
        validators=[MinValueValidator(0),
                    MaxValueValidator(1)])
    hinted = models.BooleanField(default=False)
    # JSON blob that would allow the learner's answer to be rerendered in the frontend interface
    answer = JSONField(default={}, null=True, blank=True)
    # A human readable answer that could be rendered directly in coach reports, can be blank.
    simple_answer = models.CharField(max_length=200, blank=True)
    # A JSON Array with a sequence of JSON objects that describe the history of interaction of the user
    # with this assessment item in this attempt.
    interaction_history = JSONField(default=[], blank=True)
    user = models.ForeignKey(FacilityUser, blank=True, null=True)

    class Meta:
        abstract = True
Beispiel #5
0
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)
Beispiel #6
0
class UserSessionLog(BaseLogModel):
    """
    This model provides a record of a user session in Kolibri.
    """
    # Morango syncing settings
    morango_model_name = "usersessionlog"

    user = models.ForeignKey(FacilityUser)
    channels = models.TextField(blank=True)
    start_timestamp = DateTimeTzField(default=local_now)
    last_interaction_timestamp = DateTimeTzField(null=True, blank=True)
    pages = models.TextField(blank=True)

    @classmethod
    def update_log(cls, user):
        """
        Update the current UserSessionLog for a particular user.
        """
        if user and isinstance(user, FacilityUser):
            try:
                user_session_log = cls.objects.filter(
                    user=user).latest('last_interaction_timestamp')
            except ObjectDoesNotExist:
                user_session_log = None

            if not user_session_log or timezone.now(
            ) - user_session_log.last_interaction_timestamp > timedelta(
                    minutes=5):
                user_session_log = cls(user=user)
            user_session_log.last_interaction_timestamp = local_now()
            user_session_log.save()
Beispiel #7
0
class UserSessionLog(BaseLogModel):
    """
    This model provides a record of a user session in Kolibri.
    """

    # Morango syncing settings
    morango_model_name = "usersessionlog"

    user = models.ForeignKey(FacilityUser)
    channels = models.TextField(blank=True)
    start_timestamp = DateTimeTzField(default=local_now)
    last_interaction_timestamp = DateTimeTzField(null=True, blank=True)
    pages = models.TextField(blank=True)
    device_info = models.CharField(null=True, blank=True, max_length=100)

    @classmethod
    def update_log(cls, user, user_agent):
        """
        Update the current UserSessionLog for a particular user.

        ua_parser never defaults the setting of os.family and user_agent.family
        It uses the value 'other' whenever the values are not recognized or the parsing
        fails. The code depends on this behaviour.
        """
        if user and isinstance(user, FacilityUser):
            try:
                user_session_log = cls.objects.filter(user=user).latest(
                    "last_interaction_timestamp"
                )
            except ObjectDoesNotExist:
                user_session_log = None

            if (
                not user_session_log
                or timezone.now() - user_session_log.last_interaction_timestamp
                > timedelta(minutes=5)
            ):
                parsed_string = user_agent_parser.Parse(user_agent)
                device_info = (
                    "{os_family},{os_major}/{browser_family},{browser_major}".format(
                        os_family=parsed_string["os"].get("family", ""),
                        os_major=parsed_string["os"].get("major", ""),
                        browser_family=parsed_string["user_agent"].get("family", ""),
                        browser_major=parsed_string["user_agent"].get("major", ""),
                    )
                )
                user_session_log = cls(user=user, device_info=device_info)
            user_session_log.last_interaction_timestamp = local_now()
            user_session_log.save()
Beispiel #8
0
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 Admin:
        pass

    def __str__(self):
        return self.name

    def delete_content_tree_and_files(self):
        # Use Django ORM to ensure cascading delete:
        self.root.delete()
        ContentCacheKey.update_cache_key()
Beispiel #9
0
class ExamLog(BaseLogModel):
    """
    This model provides a summary of a user's interaction with a particular exam, and serves as
    an aggregation point for individual attempts on that exam.
    """

    morango_model_name = 'examlog'

    # Identifies the exam that this is for.
    exam = models.ForeignKey(Exam,
                             related_name="examlogs",
                             blank=False,
                             null=False)
    # Identifies which user this log summarizes interactions for.
    user = models.ForeignKey(FacilityUser)
    # Is this exam open for engagement, or is it closed?
    # Used to end user engagement with an exam when it has been deactivated.
    closed = models.BooleanField(default=False)
    # when was this exam finished?
    completion_timestamp = DateTimeTzField(blank=True, null=True)

    def calculate_source_id(self):
        return "{exam_id}:{user_id}".format(exam_id=self.exam_id,
                                            user_id=self.user_id)

    def calculate_partition(self):
        return self.dataset_id
Beispiel #10
0
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"
Beispiel #11
0
class ChannelMetadataCache(ChannelMetadataAbstractBase):
    """
    This class stores the channel metadata cached/denormed into the primary database.
    """

    last_updated = DateTimeTzField(null=True)

    class Admin:
        pass
Beispiel #12
0
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"
Beispiel #13
0
class ContentSummaryLog(BaseLogModel):
    """
    This model provides a summary of all interactions a user has had with a content item.
    """
    # 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)])
    kind = models.CharField(max_length=200)
    extra_fields = JSONField(default={}, blank=True)

    def calculate_source_id(self):
        return self.content_id
Beispiel #14
0
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
Beispiel #15
0
class Lesson(AbstractFacilityDataModel):
    """
    A Lesson is a collection of non-topic ContentNodes that is linked to
    a Classroom and LearnerGroups within that Classroom.
    """

    permissions = RoleBasedPermissions(
        target_field="collection",
        can_be_created_by=(role_kinds.ADMIN, role_kinds.COACH),
        can_be_read_by=(role_kinds.ADMIN, role_kinds.COACH),
        can_be_updated_by=(role_kinds.ADMIN, role_kinds.COACH),
        can_be_deleted_by=(role_kinds.ADMIN, role_kinds.COACH),
    )

    title = models.CharField(max_length=50)
    description = models.CharField(default="", blank=True, max_length=200)
    """
    Like Exams, we store an array of objects with the following form:
    {
      contentnode_id: string,
      content_id: string,
      channel_id: string
    }
    """
    resources = JSONField(default=[], blank=True)
    # If True, then the Lesson should be viewable by Learners
    is_active = models.BooleanField(default=False)

    # The Classroom-type Collection for which the Lesson is created
    collection = models.ForeignKey(Collection,
                                   related_name="lessons",
                                   blank=False,
                                   null=False)

    created_by = models.ForeignKey(FacilityUser,
                                   related_name="lessons_created",
                                   blank=False,
                                   null=False)
    date_created = DateTimeTzField(default=local_now, editable=False)

    def get_all_learners(self):
        """
        Get all Learners that are somehow assigned to this Lesson
        """
        assignments = self.lesson_assignments.all()
        learners = FacilityUser.objects.none()
        for a in assignments:
            learners = learners.union(a.collection.get_members())
        return learners

    def __str__(self):
        return "Lesson {} for Classroom {}".format(self.title,
                                                   self.collection.name)

    def delete(self, using=None, keep_parents=False):
        """
        We delete all notifications objects whose lesson is this lesson id.
        """
        LearnerProgressNotification.objects.filter(lesson_id=self.id).delete()
        super(Lesson, self).delete(using, keep_parents)

    # Morango fields
    morango_model_name = "lesson"

    def infer_dataset(self, *args, **kwargs):
        return self.created_by.dataset_id

    def calculate_partition(self):
        return self.dataset_id
Beispiel #16
0
class KolibriAbstractBaseUser(AbstractBaseUser):
    """
    Our custom user type, derived from ``AbstractBaseUser`` as described in the Django docs.
    Draws liberally from ``django.contrib.auth.AbstractUser``, except we exclude some fields
    we don't care about, like email.

    This model is an abstract model, and is inherited by ``FacilityUser``.
    """
    class Meta:
        abstract = True

    USERNAME_FIELD = "username"

    username = models.CharField(
        _('username'),
        max_length=30,
        help_text=_(
            'Required. 30 characters or fewer. Letters and digits only'),
        validators=[
            validators.RegexValidator(
                r'^\w+$',
                _('Enter a valid username. This value can contain only letters, numbers, and underscores.'
                  )),
        ],
    )
    full_name = models.CharField(_('full name'), max_length=120, blank=True)
    date_joined = DateTimeTzField(_('date joined'),
                                  default=local_now,
                                  editable=False)

    is_staff = False
    is_superuser = False
    is_facility_user = False

    can_manage_content = False

    def get_short_name(self):
        return self.full_name.split(' ', 1)[0]

    def is_member_of(self, coll):
        """
        Determine whether this user is a member of the specified ``Collection``.

        :param coll: The ``Collection`` for which we are checking this user's membership.
        :return: ``True`` if this user is a member of the specified ``Collection``, otherwise False.
        :rtype: bool
        """
        raise NotImplementedError(
            "Subclasses of KolibriAbstractBaseUser must override the `is_member_of` method."
        )

    def get_roles_for_user(self, user):
        """
        Determine all the roles this user has in relation to the target user, and return a set containing the kinds of roles.

        :param user: The target user for which this user has the roles.
        :return: The kinds of roles this user has with respect to the target user.
        :rtype: set of ``kolibri.auth.constants.role_kinds.*`` strings
        """
        raise NotImplementedError(
            "Subclasses of KolibriAbstractBaseUser must override the `get_roles_for_user` method."
        )

    def get_roles_for_collection(self, coll):
        """
        Determine all the roles this user has in relation to the specified ``Collection``, and return a set containing the kinds of roles.

        :param coll: The target ``Collection`` for which this user has the roles.
        :return: The kinds of roles this user has with respect to the specified ``Collection``.
        :rtype: set of ``kolibri.auth.constants.role_kinds.*`` strings
        """
        raise NotImplementedError(
            "Subclasses of KolibriAbstractBaseUser must override the `get_roles_for_collection` method."
        )

    def has_role_for_user(self, kinds, user):
        """
        Determine whether this user has (at least one of) the specified role kind(s) in relation to the specified user.

        :param user: The user that is the target of the role (for which this user has the roles).
        :param kinds: The kind (or kinds) of role to check for, as a string or iterable.
        :type kinds: string from ``kolibri.auth.constants.role_kinds.*``
        :return: ``True`` if this user has the specified role kind with respect to the target user, otherwise ``False``.
        :rtype: bool
        """
        raise NotImplementedError(
            "Subclasses of KolibriAbstractBaseUser must override the `has_role_for_user` method."
        )

    def has_role_for_collection(self, kinds, coll):
        """
        Determine whether this user has (at least one of) the specified role kind(s) in relation to the specified ``Collection``.

        :param kinds: The kind (or kinds) of role to check for, as a string or iterable.
        :type kinds: string from kolibri.auth.constants.role_kinds.*
        :param coll: The target ``Collection`` for which this user has the roles.
        :return: ``True`` if this user has the specified role kind with respect to the target ``Collection``, otherwise ``False``.
        :rtype: bool
        """
        raise NotImplementedError(
            "Subclasses of KolibriAbstractBaseUser must override the `has_role_for_collection` method."
        )

    def can_create_instance(self, obj):
        """
        Checks whether this user (self) has permission to create a particular model instance (obj).

        This method should be overridden by classes that inherit from ``KolibriAbstractBaseUser``.

        In general, unless an instance has already been initialized, this method should not be called directly;
        instead, it should be preferred to call ``can_create``.

        :param obj: An (unsaved) instance of a Django model, to check permissions for.
        :return: ``True`` if this user should have permission to create the object, otherwise ``False``.
        :rtype: bool
        """
        raise NotImplementedError(
            "Subclasses of KolibriAbstractBaseUser must override the `can_create_instance` method."
        )

    def can_create(self, Model, data):
        """
        Checks whether this user (self) has permission to create an instance of Model with the specified attributes (data).

        This method defers to the ``can_create_instance`` method, and in most cases should not itself be overridden.

        :param Model: A subclass of ``django.db.models.Model``
        :param data: A ``dict`` of data to be used in creating an instance of the Model
        :return: ``True`` if this user should have permission to create an instance of Model with the specified data, else ``False``.
        :rtype: bool
        """
        try:
            instance = Model(**data)
            instance.clean_fields(exclude=getattr(
                Model, "FIELDS_TO_EXCLUDE_FROM_VALIDATION", None))
            instance.clean()
        except TypeError as e:
            logging.error(
                "TypeError while validating model before checking permissions: {}"
                .format(e.args))
            return False  # if the data provided does not fit the Model, don't continue checking
        except ValidationError as e:
            return False  # if the data does not validate, don't continue checking
        # now that we have an instance, defer to the permission-checking method that works with instances
        return self.can_create_instance(instance)

    def can_read(self, obj):
        """
        Checks whether this user (self) has permission to read a particular model instance (obj).

        This method should be overridden by classes that inherit from ``KolibriAbstractBaseUser``.

        :param obj: An instance of a Django model, to check permissions for.
        :return: ``True`` if this user should have permission to read the object, otherwise ``False``.
        :rtype: bool
        """
        raise NotImplementedError(
            "Subclasses of KolibriAbstractBaseUser must override the `can_read` method."
        )

    def can_update(self, obj):
        """
        Checks whether this user (self) has permission to update a particular model instance (obj).

        This method should be overridden by classes that inherit from KolibriAbstractBaseUser.

        :param obj: An instance of a Django model, to check permissions for.
        :return: ``True`` if this user should have permission to update the object, otherwise ``False``.
        :rtype: bool
        """
        raise NotImplementedError(
            "Subclasses of KolibriAbstractBaseUser must override the `can_update` method."
        )

    def can_delete(self, obj):
        """
        Checks whether this user (self) has permission to delete a particular model instance (obj).

        This method should be overridden by classes that inherit from KolibriAbstractBaseUser.

        :param obj: An instance of a Django model, to check permissions for.
        :return: ``True`` if this user should have permission to delete the object, otherwise ``False``.
        :rtype: bool
        """
        raise NotImplementedError(
            "Subclasses of KolibriAbstractBaseUser must override the `can_delete` method."
        )

    def get_roles_for(self, obj):
        """
        Helper function that defers to ``get_roles_for_user`` or ``get_roles_for_collection`` based on the type of object passed in.
        """
        if isinstance(obj, KolibriAbstractBaseUser):
            return self.get_roles_for_user(obj)
        elif isinstance(obj, Collection):
            return self.get_roles_for_collection(obj)
        else:
            raise ValueError(
                "The `obj` argument to `get_roles_for` must be either an instance of KolibriAbstractBaseUser or Collection."
            )

    def has_role_for(self, kinds, obj):
        """
        Helper function that defers to ``has_role_for_user`` or ``has_role_for_collection`` based on the type of object passed in.
        """
        if isinstance(obj, KolibriAbstractBaseUser):
            return self.has_role_for_user(kinds, obj)
        elif isinstance(obj, Collection):
            return self.has_role_for_collection(kinds, obj)
        else:
            raise ValueError(
                "The `obj` argument to `has_role_for` must be either an instance of KolibriAbstractBaseUser or Collection."
            )

    def filter_readable(self, queryset):
        """
        Filters a queryset down to only the elements that this user should have permission to read.

        :param queryset: A ``QuerySet`` instance that the filtering should be applied to.
        :return: Filtered ``QuerySet`` including only elements that are readable by this user.
        """
        raise NotImplementedError(
            "Subclasses of KolibriAbstractBaseUser must override the `can_delete` method."
        )
class DateTimeTzModel(models.Model):
    timestamp = DateTimeTzField(null=True)
    default_timestamp = DateTimeTzField(default=aware_datetime)