AA-274: sending credit requirement information and added tests
This commit is contained in:
@@ -66,18 +66,43 @@ class CertificateDataSerializer(serializers.Serializer):
|
||||
return cert_data.cert_status == CertificateStatuses.requesting and cert_data.request_cert_url is not None
|
||||
|
||||
|
||||
class CreditCourseRequirementsSerializer(serializers.Serializer):
|
||||
class CreditRequirementSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for credit_course_requirements
|
||||
Serializer for credit requirement objects
|
||||
"""
|
||||
display_name = serializers.CharField()
|
||||
namespace = serializers.CharField()
|
||||
min_grade = serializers.SerializerMethodField()
|
||||
status = serializers.CharField()
|
||||
status_date = serializers.DateTimeField()
|
||||
|
||||
def get_min_grade(self, req):
|
||||
return req['criteria']['min_grade'] * 100
|
||||
def get_min_grade(self, requirement):
|
||||
if requirement['namespace'] == 'grade':
|
||||
return requirement['criteria']['min_grade'] * 100
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class CreditCourseRequirementsSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for credit_course_requirements
|
||||
"""
|
||||
dashboard_url = serializers.SerializerMethodField()
|
||||
eligibility_status = serializers.CharField()
|
||||
requirements = CreditRequirementSerializer(many=True)
|
||||
|
||||
def get_dashboard_url(self, _):
|
||||
relative_path = reverse('dashboard')
|
||||
request = self.context['request']
|
||||
return request.build_absolute_uri(relative_path)
|
||||
|
||||
|
||||
class VerificationDataSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for verification data object
|
||||
"""
|
||||
link = serializers.URLField()
|
||||
status = serializers.CharField()
|
||||
status_date = serializers.DateTimeField()
|
||||
|
||||
|
||||
class ProgressTabSerializer(serializers.Serializer):
|
||||
@@ -85,7 +110,10 @@ class ProgressTabSerializer(serializers.Serializer):
|
||||
Serializer for progress tab
|
||||
"""
|
||||
certificate_data = CertificateDataSerializer()
|
||||
credit_course_requirements = CreditCourseRequirementsSerializer()
|
||||
credit_support_url = serializers.URLField()
|
||||
courseware_summary = ChapterSerializer(many=True)
|
||||
enrollment_mode = serializers.CharField()
|
||||
studio_url = serializers.CharField()
|
||||
user_timezone = serializers.CharField()
|
||||
verification_data = VerificationDataSerializer()
|
||||
|
||||
@@ -9,11 +9,14 @@ from django.urls import reverse
|
||||
from course_modes.models import CourseMode
|
||||
from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests
|
||||
from lms.djangoapps.course_home_api.toggles import COURSE_HOME_MICROFRONTEND
|
||||
from lms.djangoapps.verify_student.models import ManualVerification
|
||||
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
CREDIT_SUPPORT_URL = 'https://support.edx.org/hc/en-us/sections/115004154688-Purchasing-Academic-Credit'
|
||||
|
||||
|
||||
@override_waffle_flag(COURSE_HOME_MICROFRONTEND, active=True)
|
||||
@ddt.ddt
|
||||
@@ -36,6 +39,13 @@ class ProgressTabTestViews(BaseCourseHomeTests):
|
||||
for chapter in response.data['courseware_summary']:
|
||||
self.assertIsNotNone(chapter)
|
||||
self.assertIn('settings/grading/' + str(self.course.id), response.data['studio_url'])
|
||||
self.assertEqual(response.data['credit_support_url'], CREDIT_SUPPORT_URL)
|
||||
self.assertIsNotNone(response.data['verification_data'])
|
||||
self.assertEqual(response.data['verification_data']['status'], 'none')
|
||||
if enrollment_mode == CourseMode.VERIFIED:
|
||||
ManualVerification.objects.create(user=self.user, status='approved')
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.data['verification_data']['status'], 'approved')
|
||||
|
||||
def test_get_authenticated_user_not_enrolled(self):
|
||||
response = self.client.get(self.url)
|
||||
|
||||
@@ -17,13 +17,15 @@ from lms.djangoapps.courseware.context_processor import user_timezone_locale_pre
|
||||
from lms.djangoapps.courseware.courses import get_course_with_access, get_studio_url
|
||||
from lms.djangoapps.courseware.masquerade import setup_masquerade
|
||||
from lms.djangoapps.courseware.access import has_access
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
import lms.djangoapps.course_blocks.api as course_blocks_api
|
||||
from lms.djangoapps.courseware.views.views import credit_course_requirements, get_cert_data
|
||||
from lms.djangoapps.grades.api import CourseGradeFactory
|
||||
from lms.djangoapps.verify_student.services import IDVerificationService
|
||||
from openedx.core.djangoapps.content.block_structure.transformers import BlockStructureTransformers
|
||||
|
||||
CREDIT_SUPPORT_URL = 'https://support.edx.org/hc/en-us/sections/115004154688-Purchasing-Academic-Credit'
|
||||
|
||||
|
||||
class ProgressTabView(RetrieveAPIView):
|
||||
"""
|
||||
@@ -46,6 +48,16 @@ class ProgressTabView(RetrieveAPIView):
|
||||
is_requestable: (bool) true if status is requesting and request_cert_url is not None
|
||||
msg: (str) message for the certificate status
|
||||
title: (str) title of the certificate status
|
||||
credit_course_requirements: An object containing the following fields
|
||||
dashboard_url: (str) the url to the user's dashboard
|
||||
eligibility_status: (str) the user's eligibility to receive a course credit
|
||||
requirements: object containing the following fields
|
||||
display_name: (str) the name of the requirement that should be displayed
|
||||
namespace: (str) the type that the requirement is
|
||||
min_grade: (float) the percentage formatted minimum grade needed for this requirement
|
||||
status: (str) the status of the requirement
|
||||
status_date: (str) the date the status was set
|
||||
credit_support_url: (str) the url to the support docs for purchasing a credit
|
||||
courseware_summary: List of serialized Chapters. each Chapter has the following fields:
|
||||
display_name: (str) a str of what the name of the Chapter is for displaying on the site
|
||||
subsections: List of serialized Subsections, each has the following fields:
|
||||
@@ -63,6 +75,10 @@ class ProgressTabView(RetrieveAPIView):
|
||||
enrollment_mode: (str) a str representing the enrollment the user has ('audit', 'verified', ...)
|
||||
studio_url: (str) a str of the link to the grading in studio for the course
|
||||
user_timezone: (str) The user's preferred timezone
|
||||
verification_data: an object containing
|
||||
link: (str) the link to either start or retry verification
|
||||
status: (str) the status of the verification
|
||||
status_date: (str) the date time string of when the verification status was set
|
||||
|
||||
|
||||
|
||||
@@ -80,7 +96,6 @@ class ProgressTabView(RetrieveAPIView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
course_key_string = kwargs.get('course_key_string')
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
course_usage_key = modulestore().make_course_usage_key(course_key)
|
||||
|
||||
# Enable NR tracing for this view based on course
|
||||
monitoring_utils.set_custom_metric('course_id', course_key_string)
|
||||
@@ -109,12 +124,29 @@ class ProgressTabView(RetrieveAPIView):
|
||||
course_grade = CourseGradeFactory().read(request.user, course)
|
||||
courseware_summary = course_grade.chapter_grades.values()
|
||||
|
||||
verification_status = IDVerificationService.user_status(request.user)
|
||||
verification_link = None
|
||||
if verification_status['status'] is None or verification_status['status'] == 'expired':
|
||||
verification_link = IDVerificationService.get_verify_location('verify_student_verify_now',
|
||||
course_id=course_key)
|
||||
elif verification_status['status'] == 'must_reverify':
|
||||
verification_link = IDVerificationService.get_verify_location('verify_student_reverify',
|
||||
course_id=course_key)
|
||||
verification_data = {
|
||||
'link': verification_link,
|
||||
'status': verification_status['status'],
|
||||
'status_date': verification_status['status_date'],
|
||||
}
|
||||
|
||||
data = {
|
||||
'certificate_data': get_cert_data(request.user, course, enrollment_mode, course_grade),
|
||||
'courseware_summary': courseware_summary,
|
||||
'credit_course_requirements': credit_course_requirements(course_key, request.user),
|
||||
'credit_support_url': CREDIT_SUPPORT_URL,
|
||||
'enrollment_mode': enrollment_mode,
|
||||
'studio_url': get_studio_url(course, 'settings/grading'),
|
||||
'user_timezone': user_timezone,
|
||||
'verification_data': verification_data,
|
||||
}
|
||||
context = self.get_serializer_context()
|
||||
context['staff_access'] = bool(has_access(request.user, 'staff', course))
|
||||
|
||||
@@ -173,6 +173,7 @@ class IDVerificationService(object):
|
||||
'status': 'none',
|
||||
'error': '',
|
||||
'should_display': True,
|
||||
'status_date': '',
|
||||
'verification_expiry': '',
|
||||
}
|
||||
|
||||
@@ -188,6 +189,7 @@ class IDVerificationService(object):
|
||||
manual_id_verifications,
|
||||
'updated_at'
|
||||
)
|
||||
|
||||
except IndexError:
|
||||
# The user has no verification attempts, return the default set of data.
|
||||
return user_status
|
||||
@@ -218,6 +220,7 @@ class IDVerificationService(object):
|
||||
expiration_datetime = cls.get_expiration_datetime(user, ['approved'])
|
||||
if getattr(attempt, 'expiry_date', None) and is_verification_expiring_soon(expiration_datetime):
|
||||
user_status['verification_expiry'] = attempt.expiry_date.date().strftime("%m/%d/%Y")
|
||||
user_status['status_date'] = attempt.status_changed
|
||||
|
||||
elif attempt.status in ['submitted', 'approved', 'must_retry']:
|
||||
# user_has_valid_or_pending does include 'approved', but if we are
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
Tests for the service classes in verify_student.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from mock import patch
|
||||
from freezegun import freeze_time
|
||||
from pytz import utc
|
||||
|
||||
from lms.djangoapps.verify_student.models import ManualVerification, SoftwareSecurePhotoVerification, SSOVerification
|
||||
from lms.djangoapps.verify_student.services import IDVerificationService
|
||||
@@ -66,47 +69,62 @@ class TestIDVerificationService(ModuleStoreTestCase):
|
||||
self.assertTrue(IDVerificationService.user_has_valid_or_pending(user), status)
|
||||
|
||||
def test_user_status(self):
|
||||
# each part of the test is in it's own 'frozen time' block because the status is dependent on recency of
|
||||
# verifications and in order to control the recency, we just put everything inside of a frozen time
|
||||
|
||||
# test for correct status when no error returned
|
||||
user = UserFactory.create()
|
||||
status = IDVerificationService.user_status(user)
|
||||
expected_status = {'status': 'none', 'error': '', 'should_display': True, 'verification_expiry': ''}
|
||||
self.assertDictEqual(status, expected_status)
|
||||
with freeze_time('2014-12-12'):
|
||||
status = IDVerificationService.user_status(user)
|
||||
expected_status = {'status': 'none', 'error': '', 'should_display': True, 'verification_expiry': '',
|
||||
'status_date': ''}
|
||||
self.assertDictEqual(status, expected_status)
|
||||
|
||||
# test for when photo verification has been created
|
||||
SoftwareSecurePhotoVerification.objects.create(user=user, status='approved')
|
||||
status = IDVerificationService.user_status(user)
|
||||
expected_status = {'status': 'approved', 'error': '', 'should_display': True, 'verification_expiry': ''}
|
||||
self.assertDictEqual(status, expected_status)
|
||||
with freeze_time('2015-01-02'):
|
||||
# test for when photo verification has been created
|
||||
SoftwareSecurePhotoVerification.objects.create(user=user, status='approved')
|
||||
status = IDVerificationService.user_status(user)
|
||||
expected_status = {'status': 'approved', 'error': '', 'should_display': True, 'verification_expiry': '',
|
||||
'status_date': datetime.now(utc)}
|
||||
self.assertDictEqual(status, expected_status)
|
||||
|
||||
# create another photo verification for the same user, make sure the denial
|
||||
# is handled properly
|
||||
SoftwareSecurePhotoVerification.objects.create(
|
||||
user=user, status='denied', error_msg='[{"photoIdReasons": ["Not provided"]}]'
|
||||
)
|
||||
status = IDVerificationService.user_status(user)
|
||||
expected_status = {
|
||||
'status': 'must_reverify', 'error': ['id_image_missing'], 'should_display': True, 'verification_expiry': ''
|
||||
}
|
||||
self.assertDictEqual(status, expected_status)
|
||||
with freeze_time('2015-02-02'):
|
||||
# create another photo verification for the same user, make sure the denial
|
||||
# is handled properly
|
||||
SoftwareSecurePhotoVerification.objects.create(
|
||||
user=user, status='denied', error_msg='[{"photoIdReasons": ["Not provided"]}]'
|
||||
)
|
||||
status = IDVerificationService.user_status(user)
|
||||
expected_status = {
|
||||
'status': 'must_reverify', 'error': ['id_image_missing'], 'should_display': True, 'verification_expiry': '',
|
||||
'status_date': '',
|
||||
}
|
||||
self.assertDictEqual(status, expected_status)
|
||||
|
||||
# test for when sso verification has been created
|
||||
SSOVerification.objects.create(user=user, status='approved')
|
||||
status = IDVerificationService.user_status(user)
|
||||
expected_status = {'status': 'approved', 'error': '', 'should_display': False, 'verification_expiry': ''}
|
||||
self.assertDictEqual(status, expected_status)
|
||||
with freeze_time('2015-03-02'):
|
||||
# test for when sso verification has been created
|
||||
SSOVerification.objects.create(user=user, status='approved')
|
||||
status = IDVerificationService.user_status(user)
|
||||
expected_status = {'status': 'approved', 'error': '', 'should_display': False, 'verification_expiry': '',
|
||||
'status_date': datetime.now(utc)}
|
||||
self.assertDictEqual(status, expected_status)
|
||||
|
||||
# create another sso verification for the same user, make sure the denial
|
||||
# is handled properly
|
||||
SSOVerification.objects.create(user=user, status='denied')
|
||||
status = IDVerificationService.user_status(user)
|
||||
expected_status = {'status': 'must_reverify', 'error': '', 'should_display': False, 'verification_expiry': ''}
|
||||
self.assertDictEqual(status, expected_status)
|
||||
with freeze_time('2015-04-02'):
|
||||
# create another sso verification for the same user, make sure the denial
|
||||
# is handled properly
|
||||
SSOVerification.objects.create(user=user, status='denied')
|
||||
status = IDVerificationService.user_status(user)
|
||||
expected_status = {'status': 'must_reverify', 'error': '', 'should_display': False, 'verification_expiry': '',
|
||||
'status_date': ''}
|
||||
self.assertDictEqual(status, expected_status)
|
||||
|
||||
# test for when manual verification has been created
|
||||
ManualVerification.objects.create(user=user, status='approved')
|
||||
status = IDVerificationService.user_status(user)
|
||||
expected_status = {'status': 'approved', 'error': '', 'should_display': False, 'verification_expiry': ''}
|
||||
self.assertDictEqual(status, expected_status)
|
||||
with freeze_time('2015-05-02'):
|
||||
# test for when manual verification has been created
|
||||
ManualVerification.objects.create(user=user, status='approved')
|
||||
status = IDVerificationService.user_status(user)
|
||||
expected_status = {'status': 'approved', 'error': '', 'should_display': False, 'verification_expiry': '',
|
||||
'status_date': datetime.now(utc)}
|
||||
self.assertDictEqual(status, expected_status)
|
||||
|
||||
@ddt.unpack
|
||||
@ddt.data(
|
||||
@@ -125,7 +143,6 @@ class TestIDVerificationService(ModuleStoreTestCase):
|
||||
with patch(
|
||||
'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified'
|
||||
) as mock_verification:
|
||||
|
||||
mock_verification.return_value = status
|
||||
|
||||
status = IDVerificationService.verification_status_for_user(user, enrollment_mode)
|
||||
|
||||
Reference in New Issue
Block a user