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, {})
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)
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)
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 = []
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)
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)
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)
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)
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)
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)
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], }
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
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] }
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 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
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
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
def date(self): return VerificationDeadline.deadline_for_course(self.course.id)