Skip to content
Snippets Groups Projects
models.py 52.8 KiB
Newer Older
    def deadlines_for_courses(cls, course_keys):
        """
        Retrieve verification deadlines for particular courses.

        Arguments:
            course_keys (list): List of `CourseKey`s.

        Returns:
            dict: Map of course keys to datetimes (verification deadlines)

        """
        all_deadlines = cache.get(cls.ALL_DEADLINES_CACHE_KEY)
        if all_deadlines is None:
            all_deadlines = {
                deadline.course_key: deadline.deadline
                for deadline in VerificationDeadline.objects.all()
            }
            cache.set(cls.ALL_DEADLINES_CACHE_KEY, all_deadlines)

        return {
            course_key: all_deadlines[course_key]
            for course_key in course_keys
            if course_key in all_deadlines
        }

    @classmethod
    def deadline_for_course(cls, course_key):
        """
        Retrieve the verification deadline for a particular course.

        Arguments:
            course_key (CourseKey): The identifier for the course.

        Returns:
            datetime or None

        """
        try:
            deadline = cls.objects.get(course_key=course_key)
            return deadline.deadline
        except cls.DoesNotExist:
            return None


@receiver(models.signals.post_save, sender=VerificationDeadline)
@receiver(models.signals.post_delete, sender=VerificationDeadline)
def invalidate_deadline_caches(sender, **kwargs):  # pylint: disable=unused-argument
    """Invalidate the cached verification deadline information. """
    cache.delete(VerificationDeadline.ALL_DEADLINES_CACHE_KEY)


class VerificationCheckpoint(models.Model):
    """Represents a point at which a user is asked to re-verify his/her
    identity.
    Each checkpoint is uniquely identified by a
    (course_id, checkpoint_location) tuple.
    """
    course_id = CourseKeyField(max_length=255, db_index=True)
    checkpoint_location = models.CharField(max_length=255)
    photo_verification = models.ManyToManyField(SoftwareSecurePhotoVerification)

    class Meta(object):
        app_label = "verify_student"
        unique_together = ('course_id', 'checkpoint_location')
    def __unicode__(self):
        return u"{checkpoint} in {course}".format(
            checkpoint=self.checkpoint_name,
            course=self.course_id
        )

    @lazy
    def checkpoint_name(self):
        """Lazy method for getting checkpoint name of reverification block.

        Return location of the checkpoint if no related assessment found in
        database.
        """
        checkpoint_key = UsageKey.from_string(self.checkpoint_location)
        try:
            checkpoint_name = modulestore().get_item(checkpoint_key).related_assessment
        except ItemNotFoundError:
            log.warning(
                u"Verification checkpoint block with location '%s' and course id '%s' "
                u"not found in database.", self.checkpoint_location, unicode(self.course_id)
            )
            checkpoint_name = self.checkpoint_location

        return checkpoint_name

    def add_verification_attempt(self, verification_attempt):
        """Add the verification attempt in M2M relation of photo_verification.

        Arguments:
            verification_attempt(object): SoftwareSecurePhotoVerification object
        self.photo_verification.add(verification_attempt)   # pylint: disable=no-member
    def get_user_latest_status(self, user_id):
        """Get the status of the latest checkpoint attempt of the given user.

        Args:
            user_id(str): Id of user

        Returns:
            VerificationStatus object if found any else None
        """
        try:
            return self.checkpoint_status.filter(user_id=user_id).latest()
    @classmethod
    def get_or_create_verification_checkpoint(cls, course_id, checkpoint_location):
        """
        Get or create the verification checkpoint for given 'course_id' and

        Arguments:
            course_id (CourseKey): CourseKey
            checkpoint_location (str): Verification checkpoint location
        Raises:
            IntegrityError if create fails due to concurrent create.

        Returns:
            VerificationCheckpoint object if exists otherwise None
        """
        with transaction.atomic():
            checkpoint, __ = cls.objects.get_or_create(course_id=course_id, checkpoint_location=checkpoint_location)
            return checkpoint


class VerificationStatus(models.Model):
    """This model is an append-only table that represents user status changes
    during the verification process.
    A verification status represents a user’s progress through the verification
    process for a particular checkpoint.
    """
    SUBMITTED_STATUS = "submitted"
    APPROVED_STATUS = "approved"
    DENIED_STATUS = "denied"
    ERROR_STATUS = "error"

    VERIFICATION_STATUS_CHOICES = (
        (SUBMITTED_STATUS, SUBMITTED_STATUS),
        (APPROVED_STATUS, APPROVED_STATUS),
        (DENIED_STATUS, DENIED_STATUS),
        (ERROR_STATUS, ERROR_STATUS)
    checkpoint = models.ForeignKey(VerificationCheckpoint, related_name="checkpoint_status")
    user = models.ForeignKey(User)
    status = models.CharField(choices=VERIFICATION_STATUS_CHOICES, db_index=True, max_length=32)
    timestamp = models.DateTimeField(auto_now_add=True)
    response = models.TextField(null=True, blank=True)
    error = models.TextField(null=True, blank=True)

    class Meta(object):
        app_label = "verify_student"
        verbose_name = "Verification Status"
        verbose_name_plural = "Verification Statuses"
    @classmethod
    def add_verification_status(cls, checkpoint, user, status):
        """Create new verification status object.

        Arguments:
            checkpoint(VerificationCheckpoint): VerificationCheckpoint object
            user(User): user object
            status(str): Status from VERIFICATION_STATUS_CHOICES

        Returns:
            None
        """
        cls.objects.create(checkpoint=checkpoint, user=user, status=status)

    @classmethod
    def add_status_from_checkpoints(cls, checkpoints, user, status):
        """Create new verification status objects for a user against the given
        checkpoints.

        Arguments:
            checkpoints(list): list of VerificationCheckpoint objects
            user(User): user object
            status(str): Status from VERIFICATION_STATUS_CHOICES

        Returns:
            None
        """
        for checkpoint in checkpoints:
            cls.objects.create(checkpoint=checkpoint, user=user, status=status)
    @classmethod
    def get_user_status_at_checkpoint(cls, user, course_key, location):
        """
        Get the user's latest status at the checkpoint.

        Arguments:
            user (User): The user whose status we are retrieving.
            course_key (CourseKey): The identifier for the course.
            location (UsageKey): The location of the checkpoint in the course.

        Returns:
            unicode or None

        """
        try:
            return cls.objects.filter(
                user=user,
                checkpoint__course_id=course_key,
                checkpoint__checkpoint_location=unicode(location),
            ).latest().status
        except cls.DoesNotExist:
            return None

    def get_user_attempts(cls, user_id, course_key, checkpoint_location):
        """
        Get re-verification attempts against a user for a given 'checkpoint'
        and 'course_id'.

        Arguments:
            user_id (str): User Id string
            course_key (str): A CourseKey of a course
            checkpoint_location (str): Verification checkpoint location
        """

        return cls.objects.filter(
            user_id=user_id,
            checkpoint__course_id=course_key,
            checkpoint__checkpoint_location=checkpoint_location,
            status=cls.SUBMITTED_STATUS
    @classmethod
    def get_location_id(cls, photo_verification):
        """Get the location ID of reverification XBlock.
            photo_verification(object): SoftwareSecurePhotoVerification object
            Location Id of XBlock if any else empty string
            verification_status = cls.objects.filter(checkpoint__photo_verification=photo_verification).latest()
            return verification_status.checkpoint.checkpoint_location
    @classmethod
    def get_all_checkpoints(cls, user_id, course_key):
        """Return dict of all the checkpoints with their status.
        Args:
            user_id(int): Id of user.
            course_key(unicode): Unicode of course key

        Returns:
            dict: {checkpoint:status}
        """
        all_checks_points = cls.objects.filter(
            user_id=user_id, checkpoint__course_id=course_key
        )
        check_points = {}
        for check in all_checks_points:
            check_points[check.checkpoint.checkpoint_location] = check.status

        return check_points

    @classmethod
    def cache_key_name(cls, user_id, course_key):
        """Return the name of the key to use to cache the current configuration
        Args:
            user_id(int): Id of user.
            course_key(unicode): Unicode of course key

        Returns:
            Unicode cache key
        """
        return u"verification.{}.{}".format(user_id, unicode(course_key))


@receiver(models.signals.post_save, sender=VerificationStatus)
@receiver(models.signals.post_delete, sender=VerificationStatus)
def invalidate_verification_status_cache(sender, instance, **kwargs):  # pylint: disable=unused-argument, invalid-name
    """Invalidate the cache of VerificationStatus model. """

    cache_key = VerificationStatus.cache_key_name(
        instance.user.id,
        unicode(instance.checkpoint.course_id)
    )
    cache.delete(cache_key)

# DEPRECATED: this feature has been permanently enabled.
# Once the application code has been updated in production,
# this table can be safely deleted.
class InCourseReverificationConfiguration(ConfigurationModel):
    """Configure in-course re-verification.

    Enable or disable in-course re-verification feature.
    When this flag is disabled, the "in-course re-verification" feature
    will be disabled.

    When the flag is enabled, the "in-course re-verification" feature
    will be enabled.
    """
    pass
class IcrvStatusEmailsConfiguration(ConfigurationModel):
    """Toggle in-course reverification (ICRV) status emails

    Disabled by default. When disabled, ICRV status emails will not be sent.
    When enabled, ICRV status emails are sent.
    """
    pass


class SkippedReverification(models.Model):
    """Model for tracking skipped Reverification of a user against a specific

    If a user skipped a Reverification checkpoint for a specific course then in
    future that user cannot see the reverification link.
    """
    user = models.ForeignKey(User)
    course_id = CourseKeyField(max_length=255, db_index=True)
    checkpoint = models.ForeignKey(VerificationCheckpoint, related_name="skipped_checkpoint")
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta(object):
        app_label = "verify_student"
        unique_together = (('user', 'course_id'),)
    @transaction.atomic
    def add_skipped_reverification_attempt(cls, checkpoint, user_id, course_id):
        """Create skipped reverification object.

        Arguments:
            checkpoint(VerificationCheckpoint): VerificationCheckpoint object
            user_id(str): User Id of currently logged in user
            course_id(CourseKey): CourseKey
        Returns:
            None
        """
        cls.objects.create(checkpoint=checkpoint, user_id=user_id, course_id=course_id)

    @classmethod
    def check_user_skipped_reverification_exists(cls, user_id, course_id):
        """Check existence of a user's skipped re-verification attempt for a
        specific course.
            user_id(str): user id
            course_id(CourseKey): CourseKey
        has_skipped = cls.objects.filter(user_id=user_id, course_id=course_id).exists()
        return has_skipped

    @classmethod
    def cache_key_name(cls, user_id, course_key):
        """Return the name of the key to use to cache the current configuration
        Arguments:
            user(User): user object
            course_key(CourseKey): CourseKey

        Returns:
            string: cache key name
        """
        return u"skipped_reverification.{}.{}".format(user_id, unicode(course_key))


@receiver(models.signals.post_save, sender=SkippedReverification)
@receiver(models.signals.post_delete, sender=SkippedReverification)
def invalidate_skipped_verification_cache(sender, instance, **kwargs):  # pylint: disable=unused-argument, invalid-name
    """Invalidate the cache of skipped verification model. """

    cache_key = SkippedReverification.cache_key_name(
        instance.user.id,
        unicode(instance.course_id)
    )
    cache.delete(cache_key)