Exemple #1
0
    def test_caching(self):
        deadlines = {
            CourseKey.from_string("edX/DemoX/Fall"): datetime.now(pytz.UTC),
            CourseKey.from_string("edX/DemoX/Spring"): datetime.now(pytz.UTC) + timedelta(days=1)
        }
        course_keys = deadlines.keys()

        # Initially, no deadlines are set
        with self.assertNumQueries(1):
            all_deadlines = VerificationDeadline.deadlines_for_courses(course_keys)
            self.assertEqual(all_deadlines, {})

        # Create the deadlines
        for course_key, deadline in deadlines.iteritems():
            VerificationDeadline.objects.create(
                course_key=course_key,
                deadline=deadline,
            )

        # Warm the cache
        with self.assertNumQueries(1):
            VerificationDeadline.deadlines_for_courses(course_keys)

        # Load the deadlines from the cache
        with self.assertNumQueries(0):
            all_deadlines = VerificationDeadline.deadlines_for_courses(course_keys)
            self.assertEqual(all_deadlines, deadlines)

        # Delete the deadlines
        VerificationDeadline.objects.all().delete()

        # Verify that the deadlines are updated correctly
        with self.assertNumQueries(1):
            all_deadlines = VerificationDeadline.deadlines_for_courses(course_keys)
            self.assertEqual(all_deadlines, {})
Exemple #2
0
    def test_update(self):
        """ Verify the view supports updating a course. """
        # Sanity check: Ensure no verification deadline is set
        self.assertIsNone(
            VerificationDeadline.deadline_for_course(self.course.id))

        # Generate the expected data
        verification_deadline = datetime(year=2020,
                                         month=12,
                                         day=31,
                                         tzinfo=pytz.utc)
        expiration_datetime = datetime.now(pytz.utc)
        response, expected = self._get_update_response_and_expected_data(
            expiration_datetime, verification_deadline)

        # Sanity check: The API should return HTTP status 200 for updates
        self.assertEqual(response.status_code, 200)

        # Verify the course and modes are returned as JSON
        actual = json.loads(response.content)
        self.assertEqual(actual, expected)

        # Verify the verification deadline is updated
        self.assertEqual(
            VerificationDeadline.deadline_for_course(self.course.id),
            verification_deadline)
Exemple #3
0
    def test_disable_verification_deadline(self):
        # Configure a verification deadline for the course
        VerificationDeadline.set_deadline(self.course.id, self.VERIFICATION_DEADLINE)

        # Create the course mode Django admin form
        form = self._admin_form("verified", upgrade_deadline=self.UPGRADE_DEADLINE)

        # Use the form to disable the verification deadline
        self._set_form_verification_deadline(form, None)
        form.save()

        # Check that the deadline was disabled
        self.assertIs(VerificationDeadline.deadline_for_course(self.course.id), None)
Exemple #4
0
    def save(self, *args, **kwargs):  # pylint: disable=unused-argument
        """ Save the CourseMode objects to the database. """

        # Update the verification deadline for the course (not the individual modes)
        VerificationDeadline.set_deadline(self.id, self.verification_deadline)

        for mode in self.modes:
            mode.course_id = self.id
            mode.mode_display_name = self.get_mode_display_name(mode)
            mode.save()

        deleted_mode_ids = [mode.id for mode in self._deleted_modes]
        CourseMode.objects.filter(id__in=deleted_mode_ids).delete()
        self._deleted_modes = []
Exemple #5
0
    def _configure(self, mode, upgrade_deadline=None, verification_deadline=None):
        """Configure course modes and deadlines. """
        course_mode = CourseMode.objects.create(
            mode_slug=mode,
            mode_display_name=mode,
        )

        if upgrade_deadline is not None:
            course_mode.upgrade_deadline = upgrade_deadline
            course_mode.save()

        VerificationDeadline.set_deadline(self.course.id, verification_deadline)

        return CourseModeForm(instance=course_mode)
Exemple #6
0
    def save(self, *args, **kwargs):  # pylint: disable=unused-argument
        """ Save the CourseMode objects to the database. """

        # Update the verification deadline for the course (not the individual modes)
        VerificationDeadline.set_deadline(self.id, self.verification_deadline)

        for mode in self.modes:
            mode.course_id = self.id
            mode.mode_display_name = self.get_mode_display_name(mode)
            mode.save()

        deleted_mode_ids = [mode.id for mode in self._deleted_modes]
        CourseMode.objects.filter(id__in=deleted_mode_ids).delete()
        self._deleted_modes = []
Exemple #7
0
    def _setup_mode_and_enrollment(self, deadline, enrollment_mode):
        """Create a course mode and enrollment.

        Arguments:
            deadline (datetime): The deadline for submitting your verification.
            enrollment_mode (str): The mode of the enrollment.

        """
        CourseModeFactory(course_id=self.course.id,
                          mode_slug="verified",
                          expiration_datetime=deadline)
        CourseEnrollmentFactory(course_id=self.course.id,
                                user=self.user,
                                mode=enrollment_mode)
        VerificationDeadline.set_deadline(self.course.id, deadline)
Exemple #8
0
    def test_set_verification_deadline(self, course_mode):
        # Configure a verification deadline for the course
        VerificationDeadline.set_deadline(self.course.id, self.VERIFICATION_DEADLINE)

        # Create the course mode Django admin form
        form = self._admin_form(course_mode)

        # Update the verification deadline form data
        # We need to set the date and time fields separately, since they're
        # displayed as separate widgets in the form.
        new_deadline = (self.VERIFICATION_DEADLINE + timedelta(days=1)).replace(microsecond=0)
        self._set_form_verification_deadline(form, new_deadline)
        form.save()

        # Check that the deadline was updated
        updated_deadline = VerificationDeadline.deadline_for_course(self.course.id)
        self.assertEqual(updated_deadline, new_deadline)
Exemple #9
0
    def test_load_verification_deadline(self, mode, expect_deadline):
        # Configure a verification deadline for the course
        VerificationDeadline.set_deadline(self.course.id, self.VERIFICATION_DEADLINE)

        # Configure a course mode with both an upgrade and verification deadline
        # and load the form to edit it.
        deadline = self.UPGRADE_DEADLINE if mode == "verified" else None
        form = self._admin_form(mode, upgrade_deadline=deadline)

        # Check that the verification deadline is loaded,
        # but ONLY for verified modes.
        loaded_deadline = form.initial.get("verification_deadline")
        if expect_deadline:
            self.assertEqual(
                loaded_deadline.replace(tzinfo=None),
                self.VERIFICATION_DEADLINE.replace(tzinfo=None)
            )
        else:
            self.assertIs(loaded_deadline, None)
Exemple #10
0
    def test_update_verification_deadline_without_expiring_modes(self):
        """ Verify verification deadline can be set if no course modes expire.

         This accounts for the verified professional mode, which requires verification but should never expire.
        """
        verification_deadline = datetime(year=1915, month=5, day=7, tzinfo=pytz.utc)
        response, __ = self._get_update_response_and_expected_data(None, verification_deadline)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(VerificationDeadline.deadline_for_course(self.course.id), verification_deadline)
    def _setup_mode_and_enrollment(self, deadline, enrollment_mode):
        """Create a course mode and enrollment.

        Arguments:
            deadline (datetime): The deadline for submitting your verification.
            enrollment_mode (str): The mode of the enrollment.

        """
        CourseModeFactory(
            course_id=self.course.id,
            mode_slug="verified",
            expiration_datetime=deadline
        )
        CourseEnrollmentFactory(
            course_id=self.course.id,
            user=self.user,
            mode=enrollment_mode
        )
        VerificationDeadline.set_deadline(self.course.id, deadline)
Exemple #12
0
    def test_update(self):
        """ Verify the view supports updating a course. """
        # Sanity check: Ensure no verification deadline is set
        self.assertIsNone(VerificationDeadline.deadline_for_course(self.course.id))

        # Generate the expected data
        verification_deadline = datetime(year=2020, month=12, day=31, tzinfo=pytz.utc)
        expiration_datetime = datetime.now(pytz.utc)
        response, expected = self._get_update_response_and_expected_data(expiration_datetime, verification_deadline)

        # Sanity check: The API should return HTTP status 200 for updates
        self.assertEqual(response.status_code, 200)

        # Verify the course and modes are returned as JSON
        actual = json.loads(response.content)
        self.assertEqual(actual, expected)

        # Verify the verification deadline is updated
        self.assertEqual(VerificationDeadline.deadline_for_course(self.course.id), verification_deadline)
Exemple #13
0
    def _serialize_course(cls, course, modes=None, verification_deadline=None):
        """ Serializes a course to a Python dict. """
        modes = modes or []
        verification_deadline = verification_deadline or VerificationDeadline.deadline_for_course(course.id)

        return {
            u"id": unicode(course.id),
            u"name": unicode(course.display_name),
            u"verification_deadline": cls._serialize_datetime(verification_deadline),
            u"modes": [cls._serialize_course_mode(mode) for mode in modes],
        }
Exemple #14
0
    def get(cls, course_id):
        """ Retrieve a single course. """
        try:
            course_id = CourseKey.from_string(unicode(course_id))
        except InvalidKeyError:
            log.debug('[%s] is not a valid course key.', course_id)
            raise ValueError

        course_modes = CourseMode.objects.filter(course_id=course_id)

        if course_modes:
            verification_deadline = VerificationDeadline.deadline_for_course(course_id)
            return cls(course_id, list(course_modes), verification_deadline=verification_deadline)

        return None
Exemple #15
0
    def _serialize_course(cls, course, modes=None, verification_deadline=None):
        """ Serializes a course to a Python dict. """
        modes = modes or []
        verification_deadline = verification_deadline or VerificationDeadline.deadline_for_course(
            course.id)

        return {
            u'id':
            unicode(course.id),
            u'name':
            unicode(course.display_name),
            u'verification_deadline':
            cls._serialize_datetime(verification_deadline),
            u'modes': [cls._serialize_course_mode(mode) for mode in modes]
        }
Exemple #16
0
    def test_update_verification_deadline_without_expiring_modes(self):
        """ Verify verification deadline can be set if no course modes expire.

         This accounts for the verified professional mode, which requires verification but should never expire.
        """
        verification_deadline = datetime(year=1915,
                                         month=5,
                                         day=7,
                                         tzinfo=pytz.utc)
        response, __ = self._get_update_response_and_expected_data(
            None, verification_deadline)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(
            VerificationDeadline.deadline_for_course(self.course.id),
            verification_deadline)
Exemple #17
0
    def get(cls, course_id):
        """ Retrieve a single course. """
        try:
            course_id = CourseKey.from_string(unicode(course_id))
        except InvalidKeyError:
            log.debug('[%s] is not a valid course key.', course_id)
            raise ValueError

        course_modes = CourseMode.objects.filter(course_id=course_id)

        if course_modes:
            verification_deadline = VerificationDeadline.deadline_for_course(
                course_id)
            return cls(course_id,
                       list(course_modes),
                       verification_deadline=verification_deadline)

        return None
Exemple #18
0
def check_verify_status_by_course(user, course_enrollments):
    """
    Determine the per-course verification statuses for a given user.

    The possible statuses are:
        * VERIFY_STATUS_NEED_TO_VERIFY: The student has not yet submitted photos for verification.
        * VERIFY_STATUS_SUBMITTED: The student has submitted photos for verification,
          but has have not yet been approved.
        * VERIFY_STATUS_APPROVED: The student has been successfully verified.
        * VERIFY_STATUS_MISSED_DEADLINE: The student did not submit photos within the course's deadline.
        * VERIFY_STATUS_NEED_TO_REVERIFY: The student has an active verification, but it is
            set to expire before the verification deadline for the course.

    It is is also possible that a course does NOT have a verification status if:
        * The user is not enrolled in a verified mode, meaning that the user didn't pay.
        * The course does not offer a verified mode.
        * The user submitted photos but an error occurred while verifying them.
        * The user submitted photos but the verification was denied.

    In the last two cases, we rely on messages in the sidebar rather than displaying
    messages for each course.

    Arguments:
        user (User): The currently logged-in user.
        course_enrollments (list[CourseEnrollment]): The courses the user is enrolled in.

    Returns:
        dict: Mapping of course keys verification status dictionaries.
            If no verification status is applicable to a course, it will not
            be included in the dictionary.
            The dictionaries have these keys:
                * status (str): One of the enumerated status codes.
                * days_until_deadline (int): Number of days until the verification deadline.
                * verification_good_until (str): Date string for the verification expiration date.

    """
    status_by_course = {}

    # Retrieve all verifications for the user, sorted in descending
    # order by submission datetime
    verifications = SoftwareSecurePhotoVerification.objects.filter(user=user)

    # Check whether the user has an active or pending verification attempt
    # To avoid another database hit, we re-use the queryset we have already retrieved.
    has_active_or_pending = SoftwareSecurePhotoVerification.user_has_valid_or_pending(
        user, queryset=verifications)

    # Retrieve verification deadlines for the enrolled courses
    enrolled_course_keys = [
        enrollment.course_id for enrollment in course_enrollments
    ]
    course_deadlines = VerificationDeadline.deadlines_for_courses(
        enrolled_course_keys)

    recent_verification_datetime = None

    for enrollment in course_enrollments:

        # If the user hasn't enrolled as verified, then the course
        # won't display state related to its verification status.
        if enrollment.mode in CourseMode.VERIFIED_MODES:

            # Retrieve the verification deadline associated with the course.
            # This could be None if the course doesn't have a deadline.
            deadline = course_deadlines.get(enrollment.course_id)

            relevant_verification = SoftwareSecurePhotoVerification.verification_for_datetime(
                deadline, verifications)

            # Picking the max verification datetime on each iteration only with approved status
            if relevant_verification is not None and relevant_verification.status == "approved":
                recent_verification_datetime = max(
                    recent_verification_datetime
                    if recent_verification_datetime is not None else
                    relevant_verification.expiration_datetime,
                    relevant_verification.expiration_datetime)

            # By default, don't show any status related to verification
            status = None

            # Check whether the user was approved or is awaiting approval
            if relevant_verification is not None:
                if relevant_verification.status == "approved":
                    status = VERIFY_STATUS_APPROVED
                elif relevant_verification.status == "submitted":
                    status = VERIFY_STATUS_SUBMITTED

            # If the user didn't submit at all, then tell them they need to verify
            # If the deadline has already passed, then tell them they missed it.
            # If they submitted but something went wrong (error or denied),
            # then don't show any messaging next to the course, since we already
            # show messages related to this on the left sidebar.
            submitted = (relevant_verification is not None
                         and relevant_verification.status
                         not in ["created", "ready"])
            if status is None and not submitted:
                if deadline is None or deadline > datetime.now(UTC):
                    if has_active_or_pending:
                        # The user has an active verification, but the verification
                        # is set to expire before the deadline.  Tell the student
                        # to reverify.
                        status = VERIFY_STATUS_NEED_TO_REVERIFY
                    else:
                        status = VERIFY_STATUS_NEED_TO_VERIFY
                else:
                    # If a user currently has an active or pending verification,
                    # then they may have submitted an additional attempt after
                    # the verification deadline passed.  This can occur,
                    # for example, when the support team asks a student
                    # to reverify after the deadline so they can receive
                    # a verified certificate.
                    # In this case, we still want to show them as "verified"
                    # on the dashboard.
                    if has_active_or_pending:
                        status = VERIFY_STATUS_APPROVED

                    # Otherwise, the student missed the deadline, so show
                    # them as "honor" (the kind of certificate they will receive).
                    else:
                        status = VERIFY_STATUS_MISSED_DEADLINE

            # Set the status for the course only if we're displaying some kind of message
            # Otherwise, leave the course out of the dictionary.
            if status is not None:
                days_until_deadline = None

                now = datetime.now(UTC)
                if deadline is not None and deadline > now:
                    days_until_deadline = (deadline - now).days

                status_by_course[enrollment.course_id] = {
                    'status': status,
                    'days_until_deadline': days_until_deadline
                }

    if recent_verification_datetime:
        for key, value in status_by_course.iteritems():  # pylint: disable=unused-variable
            status_by_course[key][
                'verification_good_until'] = recent_verification_datetime.strftime(
                    "%m/%d/%Y")

    return status_by_course
Exemple #19
0
def check_verify_status_by_course(user, course_enrollments):
    """
    Determine the per-course verification statuses for a given user.

    The possible statuses are:
        * VERIFY_STATUS_NEED_TO_VERIFY: The student has not yet submitted photos for verification.
        * VERIFY_STATUS_SUBMITTED: The student has submitted photos for verification,
          but has have not yet been approved.
        * VERIFY_STATUS_APPROVED: The student has been successfully verified.
        * VERIFY_STATUS_MISSED_DEADLINE: The student did not submit photos within the course's deadline.
        * VERIFY_STATUS_NEED_TO_REVERIFY: The student has an active verification, but it is
            set to expire before the verification deadline for the course.

    It is is also possible that a course does NOT have a verification status if:
        * The user is not enrolled in a verified mode, meaning that the user didn't pay.
        * The course does not offer a verified mode.
        * The user submitted photos but an error occurred while verifying them.
        * The user submitted photos but the verification was denied.

    In the last two cases, we rely on messages in the sidebar rather than displaying
    messages for each course.

    Arguments:
        user (User): The currently logged-in user.
        course_enrollments (list[CourseEnrollment]): The courses the user is enrolled in.

    Returns:
        dict: Mapping of course keys verification status dictionaries.
            If no verification status is applicable to a course, it will not
            be included in the dictionary.
            The dictionaries have these keys:
                * status (str): One of the enumerated status codes.
                * days_until_deadline (int): Number of days until the verification deadline.
                * verification_good_until (str): Date string for the verification expiration date.

    """
    status_by_course = {}

    # Retrieve all verifications for the user, sorted in descending
    # order by submission datetime
    verifications = SoftwareSecurePhotoVerification.objects.filter(user=user)

    # Check whether the user has an active or pending verification attempt
    # To avoid another database hit, we re-use the queryset we have already retrieved.
    has_active_or_pending = SoftwareSecurePhotoVerification.user_has_valid_or_pending(
        user, queryset=verifications
    )

    # Retrieve verification deadlines for the enrolled courses
    enrolled_course_keys = [enrollment.course_id for enrollment in course_enrollments]
    course_deadlines = VerificationDeadline.deadlines_for_courses(enrolled_course_keys)

    recent_verification_datetime = None

    for enrollment in course_enrollments:

        # If the user hasn't enrolled as verified, then the course
        # won't display state related to its verification status.
        if enrollment.mode in CourseMode.VERIFIED_MODES:

            # Retrieve the verification deadline associated with the course.
            # This could be None if the course doesn't have a deadline.
            deadline = course_deadlines.get(enrollment.course_id)

            relevant_verification = SoftwareSecurePhotoVerification.verification_for_datetime(deadline, verifications)

            # Picking the max verification datetime on each iteration only with approved status
            if relevant_verification is not None and relevant_verification.status == "approved":
                recent_verification_datetime = max(
                    recent_verification_datetime if recent_verification_datetime is not None
                    else relevant_verification.expiration_datetime,
                    relevant_verification.expiration_datetime
                )

            # By default, don't show any status related to verification
            status = None

            # Check whether the user was approved or is awaiting approval
            if relevant_verification is not None:
                if relevant_verification.status == "approved":
                    status = VERIFY_STATUS_APPROVED
                elif relevant_verification.status == "submitted":
                    status = VERIFY_STATUS_SUBMITTED

            # If the user didn't submit at all, then tell them they need to verify
            # If the deadline has already passed, then tell them they missed it.
            # If they submitted but something went wrong (error or denied),
            # then don't show any messaging next to the course, since we already
            # show messages related to this on the left sidebar.
            submitted = (
                relevant_verification is not None and
                relevant_verification.status not in ["created", "ready"]
            )
            if status is None and not submitted:
                if deadline is None or deadline > datetime.now(UTC):
                    if has_active_or_pending:
                        # The user has an active verification, but the verification
                        # is set to expire before the deadline.  Tell the student
                        # to reverify.
                        status = VERIFY_STATUS_NEED_TO_REVERIFY
                    else:
                        status = VERIFY_STATUS_NEED_TO_VERIFY
                else:
                    # If a user currently has an active or pending verification,
                    # then they may have submitted an additional attempt after
                    # the verification deadline passed.  This can occur,
                    # for example, when the support team asks a student
                    # to reverify after the deadline so they can receive
                    # a verified certificate.
                    # In this case, we still want to show them as "verified"
                    # on the dashboard.
                    if has_active_or_pending:
                        status = VERIFY_STATUS_APPROVED

                    # Otherwise, the student missed the deadline, so show
                    # them as "honor" (the kind of certificate they will receive).
                    else:
                        status = VERIFY_STATUS_MISSED_DEADLINE

            # Set the status for the course only if we're displaying some kind of message
            # Otherwise, leave the course out of the dictionary.
            if status is not None:
                days_until_deadline = None

                now = datetime.now(UTC)
                if deadline is not None and deadline > now:
                    days_until_deadline = (deadline - now).days

                status_by_course[enrollment.course_id] = {
                    'status': status,
                    'days_until_deadline': days_until_deadline
                }

    if recent_verification_datetime:
        for key, value in status_by_course.iteritems():  # pylint: disable=unused-variable
            status_by_course[key]['verification_good_until'] = recent_verification_datetime.strftime("%m/%d/%Y")

    return status_by_course
Exemple #20
0
 def date(self):
     return VerificationDeadline.deadline_for_course(self.course.id)
Exemple #21
0
 def date(self):
     return VerificationDeadline.deadline_for_course(self.course.id)