diff --git a/common/djangoapps/student/tests/factories.py b/common/djangoapps/student/tests/factories.py index f07dc57860..0a57b95aec 100644 --- a/common/djangoapps/student/tests/factories.py +++ b/common/djangoapps/student/tests/factories.py @@ -41,6 +41,7 @@ class UserProfileFactory(DjangoModelFactory): gender = u'm' mailing_address = None goals = u'Learn a lot' + allow_certificate = True class CourseModeFactory(DjangoModelFactory): diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index 2ee7079f7f..d994d249d5 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -187,6 +187,33 @@ def certificate_status_for_student(student, course_id): return {'status': CertificateStatuses.unavailable, 'mode': GeneratedCertificate.MODES.honor} +def certificate_info_for_user(user, course_id, grade, user_is_whitelisted=None): + """ + Returns the certificate info for a user for grade report. + """ + if user_is_whitelisted is None: + user_is_whitelisted = CertificateWhitelist.objects.filter( + user=user, course_id=course_id, whitelist=True + ).exists() + + eligible_for_certificate = (user_is_whitelisted or grade is not None) and user.profile.allow_certificate + + if eligible_for_certificate: + user_is_eligible = 'Y' + + certificate_status = certificate_status_for_student(user, course_id) + certificate_generated = certificate_status['status'] == CertificateStatuses.downloadable + certificate_is_delivered = 'Y' if certificate_generated else 'N' + + certificate_type = certificate_status['mode'] if certificate_generated else 'N/A' + else: + user_is_eligible = 'N' + certificate_is_delivered = 'N' + certificate_type = 'N/A' + + return [user_is_eligible, certificate_is_delivered, certificate_type] + + class ExampleCertificateSet(TimeStampedModel): """A set of example certificates. diff --git a/lms/djangoapps/certificates/tests/factories.py b/lms/djangoapps/certificates/tests/factories.py index 917be240ee..49b5290c79 100644 --- a/lms/djangoapps/certificates/tests/factories.py +++ b/lms/djangoapps/certificates/tests/factories.py @@ -1,6 +1,8 @@ from factory.django import DjangoModelFactory -from certificates.models import GeneratedCertificate, CertificateStatuses, CertificateHtmlViewConfiguration +from certificates.models import ( + GeneratedCertificate, CertificateStatuses, CertificateHtmlViewConfiguration, CertificateWhitelist +) # Factories are self documenting @@ -15,6 +17,14 @@ class GeneratedCertificateFactory(DjangoModelFactory): name = '' +class CertificateWhitelistFactory(DjangoModelFactory): + + FACTORY_FOR = CertificateWhitelist + + course_id = None + whitelist = True + + class CertificateHtmlViewConfigurationFactory(DjangoModelFactory): FACTORY_FOR = CertificateHtmlViewConfiguration diff --git a/lms/djangoapps/certificates/tests/tests.py b/lms/djangoapps/certificates/tests/tests.py index 25b63663ef..ff6af69e1d 100644 --- a/lms/djangoapps/certificates/tests/tests.py +++ b/lms/djangoapps/certificates/tests/tests.py @@ -1,7 +1,7 @@ """ Tests for the certificates models. """ - +from ddt import ddt, data, unpack from mock import patch from django.conf import settings @@ -9,7 +9,12 @@ from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from student.tests.factories import UserFactory -from certificates.models import CertificateStatuses, GeneratedCertificate, certificate_status_for_student +from certificates.models import ( + CertificateStatuses, + GeneratedCertificate, + certificate_status_for_student, + certificate_info_for_user +) from certificates.tests.factories import GeneratedCertificateFactory from util.milestones_helpers import ( @@ -19,6 +24,7 @@ from util.milestones_helpers import ( ) +@ddt class CertificatesModelTest(ModuleStoreTestCase): """ Tests for the GeneratedCertificate model @@ -32,6 +38,26 @@ class CertificatesModelTest(ModuleStoreTestCase): self.assertEqual(certificate_status['status'], CertificateStatuses.unavailable) self.assertEqual(certificate_status['mode'], GeneratedCertificate.MODES.honor) + @unpack + @data( + {'allow_certificate': False, 'whitelisted': False, 'grade': None, 'output': ['N', 'N', 'N/A']}, + {'allow_certificate': True, 'whitelisted': True, 'grade': None, 'output': ['Y', 'N', 'N/A']}, + {'allow_certificate': True, 'whitelisted': False, 'grade': 0.9, 'output': ['Y', 'N', 'N/A']}, + {'allow_certificate': False, 'whitelisted': True, 'grade': 0.8, 'output': ['N', 'N', 'N/A']}, + {'allow_certificate': False, 'whitelisted': None, 'grade': 0.8, 'output': ['N', 'N', 'N/A']} + ) + def test_certificate_info_for_user(self, allow_certificate, whitelisted, grade, output): + """ + Verify that certificate_info_for_user works. + """ + student = UserFactory() + course = CourseFactory.create(org='edx', number='verified', display_name='Verified Course') + student.profile.allow_certificate = allow_certificate + student.profile.save() + + certificate_info = certificate_info_for_user(student, course.id, grade, whitelisted) + self.assertEqual(certificate_info, output) + @patch.dict(settings.FEATURES, {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True}) def test_course_milestone_collected(self): seed_milestone_relationship_types() diff --git a/lms/djangoapps/instructor_task/tasks_helper.py b/lms/djangoapps/instructor_task/tasks_helper.py index 92087df572..7dcc967394 100644 --- a/lms/djangoapps/instructor_task/tasks_helper.py +++ b/lms/djangoapps/instructor_task/tasks_helper.py @@ -22,6 +22,7 @@ from util.file import course_filename_prefix_generator, UniversalNewlineIterator from xmodule.modulestore.django import modulestore from xmodule.split_test_module import get_split_user_partitions +from certificates.models import CertificateWhitelist, certificate_info_for_user from courseware.courses import get_course_by_id, get_problems_in_section from courseware.grades import iterate_grades_for from courseware.models import StudentModule @@ -36,6 +37,7 @@ from openedx.core.djangoapps.course_groups.models import CourseUserGroup from opaque_keys.edx.keys import UsageKey from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort, is_course_cohorted from student.models import CourseEnrollment +from verify_student.models import SoftwareSecurePhotoVerification # define different loggers for use within tasks and on client side @@ -549,7 +551,7 @@ def upload_csv_to_report_store(rows, csv_name, course_id, timestamp): ) -def upload_grades_csv(_xmodule_instance_args, _entry_id, course_id, _task_input, action_name): +def upload_grades_csv(_xmodule_instance_args, _entry_id, course_id, _task_input, action_name): # pylint: disable=too-many-statements """ For a given `course_id`, generate a grades CSV file for all students that are enrolled, and store using a `ReportStore`. Once created, the files can @@ -584,6 +586,10 @@ def upload_grades_csv(_xmodule_instance_args, _entry_id, course_id, _task_input, experiment_partitions = get_split_user_partitions(course.user_partitions) group_configs_header = [u'Experiment Group ({})'.format(partition.name) for partition in experiment_partitions] + certificate_info_header = ['Certificate Eligible', 'Certificate Delivered', 'Certificate Type'] + certificate_whitelist = CertificateWhitelist.objects.filter(course_id=course_id, whitelist=True) + whitelisted_user_ids = [entry.user_id for entry in certificate_whitelist] + # Loop over all our students and build our CSV lists in memory header = None rows = [] @@ -623,7 +629,8 @@ def upload_grades_csv(_xmodule_instance_args, _entry_id, course_id, _task_input, if not header: header = [section['label'] for section in gradeset[u'section_breakdown']] rows.append( - ["id", "email", "username", "grade"] + header + cohorts_header + group_configs_header + ["id", "email", "username", "grade"] + header + cohorts_header + + group_configs_header + ['Enrollment Track', 'Verification Status'] + certificate_info_header ) percents = { @@ -642,6 +649,19 @@ def upload_grades_csv(_xmodule_instance_args, _entry_id, course_id, _task_input, group = LmsPartitionService(student, course_id).get_group(partition, assign=False) group_configs_group_names.append(group.name if group else '') + enrollment_mode = CourseEnrollment.enrollment_mode_for_user(student, course_id)[0] + verification_status = SoftwareSecurePhotoVerification.verification_status_for_user( + student, + course_id, + enrollment_mode + ) + certificate_info = certificate_info_for_user( + student, + course_id, + gradeset['grade'], + student.id in whitelisted_user_ids + ) + # Not everybody has the same gradable items. If the item is not # found in the user's gradeset, just assume it's a 0. The aggregated # grades for their sections and overall course will be calculated @@ -651,7 +671,8 @@ def upload_grades_csv(_xmodule_instance_args, _entry_id, course_id, _task_input, row_percents = [percents.get(label, 0.0) for label in header] rows.append( [student.id, student.email, student.username, gradeset['percent']] + - row_percents + cohorts_group_name + group_configs_group_names + row_percents + cohorts_group_name + group_configs_group_names + + [enrollment_mode] + [verification_status] + certificate_info ) else: # An empty gradeset means we failed to grade a student. diff --git a/lms/djangoapps/instructor_task/tests/test_base.py b/lms/djangoapps/instructor_task/tests/test_base.py index 762b4c8f79..f83d79a7c3 100644 --- a/lms/djangoapps/instructor_task/tests/test_base.py +++ b/lms/djangoapps/instructor_task/tests/test_base.py @@ -10,9 +10,11 @@ import unicodecsv from uuid import uuid4 from celery.states import SUCCESS, FAILURE +from django.core.urlresolvers import reverse from django.conf import settings from django.test.testcases import TestCase from django.contrib.auth.models import User +from lms.djangoapps.lms_xblock.runtime import quote_slashes from opaque_keys.edx.locations import Location, SlashSeparatedCourseKey from capa.tests.response_xml_factory import OptionResponseXMLFactory @@ -147,21 +149,21 @@ class InstructorTaskCourseTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase) self.login(InstructorTaskCourseTestCase.get_user_email(username), "test") self.current_user = username - def _create_user(self, username, email=None, is_staff=False): + def _create_user(self, username, email=None, is_staff=False, mode='honor'): """Creates a user and enrolls them in the test course.""" if email is None: email = InstructorTaskCourseTestCase.get_user_email(username) thisuser = UserFactory.create(username=username, email=email, is_staff=is_staff) - CourseEnrollmentFactory.create(user=thisuser, course_id=self.course.id) + CourseEnrollmentFactory.create(user=thisuser, course_id=self.course.id, mode=mode) return thisuser def create_instructor(self, username, email=None): """Creates an instructor for the test course.""" return self._create_user(username, email, is_staff=True) - def create_student(self, username, email=None): + def create_student(self, username, email=None, mode='honor'): """Creates a student for the test course.""" - return self._create_user(username, email, is_staff=False) + return self._create_user(username, email, is_staff=False, mode=mode) @staticmethod def get_task_status(task_id): @@ -236,6 +238,40 @@ class InstructorTaskModuleTestCase(InstructorTaskCourseTestCase): module_state_key=descriptor.location, ) + def submit_student_answer(self, username, problem_url_name, responses): + """ + Use ajax interface to submit a student answer. + + Assumes the input list of responses has two values. + """ + def get_input_id(response_id): + """Creates input id using information about the test course and the current problem.""" + # Note that this is a capa-specific convention. The form is a version of the problem's + # URL, modified so that it can be easily stored in html, prepended with "input-" and + # appended with a sequence identifier for the particular response the input goes to. + return 'input_i4x-{0}-{1}-problem-{2}_{3}'.format(TEST_COURSE_ORG.lower(), + TEST_COURSE_NUMBER.replace('.', '_'), + problem_url_name, response_id) + + # make sure that the requested user is logged in, so that the ajax call works + # on the right problem: + self.login_username(username) + # make ajax call: + modx_url = reverse('xblock_handler', kwargs={ + 'course_id': self.course.id.to_deprecated_string(), + 'usage_id': quote_slashes( + InstructorTaskModuleTestCase.problem_location(problem_url_name).to_deprecated_string() + ), + 'handler': 'xmodule_handler', + 'suffix': 'problem_check', + }) + + # assign correct identifier to each response. + resp = self.client.post(modx_url, { + get_input_id('{}_1').format(index): response for index, response in enumerate(responses, 2) + }) + return resp + class TestReportMixin(object): """ @@ -246,7 +282,7 @@ class TestReportMixin(object): if os.path.exists(reports_download_path): shutil.rmtree(reports_download_path) - def verify_rows_in_csv(self, expected_rows, verify_order=True): + def verify_rows_in_csv(self, expected_rows, verify_order=True, ignore_other_columns=False): """ Verify that the last ReportStore CSV contains the expected content. @@ -259,12 +295,20 @@ class TestReportMixin(object): content and order of `expected_rows` matches the actual csv rows. When False (default), we only verify that the content matches. + ignore_other_columns (boolean): When True, we verify that `expected_rows` + contain data which is the subset of actual csv rows. """ report_store = ReportStore.from_config() report_csv_filename = report_store.links_for(self.course.id)[0][0] with open(report_store.path_to(self.course.id, report_csv_filename)) as csv_file: # Expand the dict reader generator so we don't lose it's content csv_rows = [row for row in unicodecsv.DictReader(csv_file)] + + if ignore_other_columns: + csv_rows = [ + {key: row.get(key) for key in expected_rows[index].keys()} for index, row in enumerate(csv_rows) + ] + if verify_order: self.assertEqual(csv_rows, expected_rows) else: diff --git a/lms/djangoapps/instructor_task/tests/test_integration.py b/lms/djangoapps/instructor_task/tests/test_integration.py index 5f1c61821d..d6cb4cfc0e 100644 --- a/lms/djangoapps/instructor_task/tests/test_integration.py +++ b/lms/djangoapps/instructor_task/tests/test_integration.py @@ -43,39 +43,6 @@ class TestIntegrationTask(InstructorTaskModuleTestCase): Base class to provide general methods used for "integration" testing of particular tasks. """ - def submit_student_answer(self, username, problem_url_name, responses): - """ - Use ajax interface to submit a student answer. - - Assumes the input list of responses has two values. - """ - def get_input_id(response_id): - """Creates input id using information about the test course and the current problem.""" - # Note that this is a capa-specific convention. The form is a version of the problem's - # URL, modified so that it can be easily stored in html, prepended with "input-" and - # appended with a sequence identifier for the particular response the input goes to. - return 'input_i4x-{0}-{1}-problem-{2}_{3}'.format(TEST_COURSE_ORG.lower(), - TEST_COURSE_NUMBER.replace('.', '_'), - problem_url_name, response_id) - - # make sure that the requested user is logged in, so that the ajax call works - # on the right problem: - self.login_username(username) - # make ajax call: - modx_url = reverse('xblock_handler', kwargs={ - 'course_id': self.course.id.to_deprecated_string(), - 'usage_id': quote_slashes(InstructorTaskModuleTestCase.problem_location(problem_url_name).to_deprecated_string()), - 'handler': 'xmodule_handler', - 'suffix': 'problem_check', - }) - - # we assume we have two responses, so assign them the correct identifiers. - resp = self.client.post(modx_url, { - get_input_id('2_1'): responses[0], - get_input_id('3_1'): responses[1], - }) - return resp - def _assert_task_failure(self, entry_id, task_type, problem_url_name, expected_message): """Confirm that expected values are stored in InstructorTask on task failure.""" instructor_task = InstructorTask.objects.get(id=entry_id) @@ -606,7 +573,7 @@ class TestGradeReportConditionalContent(TestReportMixin, TestIntegrationTask): """ self.assertDictContainsSubset({'attempted': 2, 'succeeded': 2, 'failed': 0}, task_result) - def verify_grades_in_csv(self, students_grades): + def verify_grades_in_csv(self, students_grades, ignore_other_columns=False): """ Verify that the grades CSV contains the expected grades data. @@ -642,7 +609,8 @@ class TestGradeReportConditionalContent(TestReportMixin, TestIntegrationTask): user_partition_group(student) ) for student_grades in students_grades for student, grades in student_grades.iteritems() - ] + ], + ignore_other_columns=ignore_other_columns ) def test_both_groups_problems(self): @@ -668,7 +636,8 @@ class TestGradeReportConditionalContent(TestReportMixin, TestIntegrationTask): [ {self.student_a: {'grade': '1.0', 'HW': '1.0'}}, {self.student_b: {'grade': '0.5', 'HW': '0.5'}} - ] + ], + ignore_other_columns=True ) def test_one_group_problem(self): @@ -690,5 +659,6 @@ class TestGradeReportConditionalContent(TestReportMixin, TestIntegrationTask): [ {self.student_a: {'grade': '1.0', 'HW': '1.0'}}, {self.student_b: {'grade': '0.0', 'HW': '0.0'}} - ] + ], + ignore_other_columns=True ) diff --git a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py index 4b1097426a..1ec9d1a7ec 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py @@ -11,18 +11,21 @@ from mock import Mock, patch import tempfile import unicodecsv -from xmodule.modulestore.tests.factories import CourseFactory -from student.tests.factories import UserFactory -from student.models import CourseEnrollment -from xmodule.partitions.partitions import Group, UserPartition - +from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory +from certificates.tests.factories import GeneratedCertificateFactory, CertificateWhitelistFactory +from course_modes.models import CourseMode +from instructor_task.models import ReportStore +from instructor_task.tasks_helper import cohort_students_and_upload, upload_grades_csv, upload_students_csv +from instructor_task.tests.test_base import InstructorTaskCourseTestCase, TestReportMixin, InstructorTaskModuleTestCase from openedx.core.djangoapps.course_groups.models import CourseUserGroupPartitionGroup from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory import openedx.core.djangoapps.user_api.course_tag.api as course_tag_api from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartitionScheme -from instructor_task.models import ReportStore -from instructor_task.tasks_helper import cohort_students_and_upload, upload_grades_csv, upload_students_csv -from instructor_task.tests.test_base import InstructorTaskCourseTestCase, TestReportMixin +from student.tests.factories import UserFactory +from student.models import CourseEnrollment +from verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory +from xmodule.partitions.partitions import Group, UserPartition @ddt.ddt @@ -250,7 +253,7 @@ class TestInstructorGradeReport(TestReportMixin, InstructorTaskCourseTestCase): mock_iterate_grades_for.return_value = [ ( self.create_student('username', 'student@example.com'), - {'section_breakdown': [{'label': u'\u8282\u540e\u9898 01'}], 'percent': 0}, + {'section_breakdown': [{'label': u'\u8282\u540e\u9898 01'}], 'percent': 0, 'grade': None}, 'Cannot grade student' ) ] @@ -538,3 +541,141 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase): ], verify_order=False ) + + +@ddt.ddt +@patch('instructor_task.tasks_helper.DefaultStorage', new=MockDefaultStorage) +class TestGradeReportEnrollmentAndCertificateInfo(TestReportMixin, InstructorTaskModuleTestCase): + """ + Test that grade report has correct user enrolment, verification, and certificate information. + """ + def setUp(self): + super(TestGradeReportEnrollmentAndCertificateInfo, self).setUp() + + self.initialize_course() + + self.create_problem() + + self.columns_to_check = [ + 'Enrollment Track', + 'Verification Status', + 'Certificate Eligible', + 'Certificate Delivered', + 'Certificate Type' + ] + + def create_problem(self, problem_display_name='test_problem', parent=None): + """ + Create a multiple choice response problem. + """ + if parent is None: + parent = self.problem_section + + factory = MultipleChoiceResponseXMLFactory() + args = {'choices': [False, True, False]} + problem_xml = factory.build_xml(**args) + ItemFactory.create( + parent_location=parent.location, + parent=parent, + category="problem", + display_name=problem_display_name, + data=problem_xml + ) + + def user_is_embargoed(self, user, is_embargoed): + """ + Set a users emabargo state. + """ + user_profile = UserFactory(username=user.username, email=user.email).profile + user_profile.allow_certificate = not is_embargoed + user_profile.save() + + def _verify_csv_data(self, username, expected_data): + """ + Verify grade report data. + """ + with patch('instructor_task.tasks_helper._get_current_task'): + upload_grades_csv(None, None, self.course.id, None, 'graded') + report_store = ReportStore.from_config() + report_csv_filename = report_store.links_for(self.course.id)[0][0] + with open(report_store.path_to(self.course.id, report_csv_filename)) as csv_file: + for row in unicodecsv.DictReader(csv_file): + if row.get('username') == username: + csv_row_data = [row[column] for column in self.columns_to_check] + self.assertEqual(csv_row_data, expected_data) + + def _create_user_data(self, + user_enroll_mode, + has_passed, + whitelisted, + is_embargoed, + verification_status, + certificate_status, + certificate_mode): + """ + Create user data to be used during grade report generation. + """ + + user = self.create_student('u1', mode=user_enroll_mode) + + if has_passed: + self.submit_student_answer('u1', 'test_problem', ['choice_1']) + + CertificateWhitelistFactory.create(user=user, course_id=self.course.id, whitelist=whitelisted) + + self.user_is_embargoed(user, is_embargoed) + + if user_enroll_mode in CourseMode.VERIFIED_MODES: + SoftwareSecurePhotoVerificationFactory.create(user=user, status=verification_status) + + GeneratedCertificateFactory.create( + user=user, + course_id=self.course.id, + status=certificate_status, + mode=certificate_mode + ) + + return user + + @ddt.data( + ( + 'verified', False, False, False, 'approved', 'notpassing', 'honor', + ['verified', 'ID Verified', 'N', 'N', 'N/A'] + ), + ( + 'verified', False, True, False, 'approved', 'downloadable', 'verified', + ['verified', 'ID Verified', 'Y', 'Y', 'verified'] + ), + ( + 'honor', True, True, True, 'approved', 'restricted', 'honor', + ['honor', 'N/A', 'N', 'N', 'N/A'] + ), + ( + 'verified', True, True, False, 'must_retry', 'downloadable', 'honor', + ['verified', 'Not ID Verified', 'Y', 'Y', 'honor'] + ), + ) + @ddt.unpack + def test_grade_report_enrollment_and_certificate_info( + self, + user_enroll_mode, + has_passed, + whitelisted, + is_embargoed, + verification_status, + certificate_status, + certificate_mode, + expected_output + ): + + user = self._create_user_data( + user_enroll_mode, + has_passed, + whitelisted, + is_embargoed, + verification_status, + certificate_status, + certificate_mode + ) + + self._verify_csv_data(user.username, expected_output) diff --git a/lms/djangoapps/verify_student/models.py b/lms/djangoapps/verify_student/models.py index b1353a097e..dd32bcb122 100644 --- a/lms/djangoapps/verify_student/models.py +++ b/lms/djangoapps/verify_student/models.py @@ -13,6 +13,8 @@ from email.utils import formatdate import functools import json import logging + +from course_modes.models import CourseMode import pytz import requests import uuid @@ -935,6 +937,25 @@ class SoftwareSecurePhotoVerification(PhotoVerification): attempt.submit() return attempt + @classmethod + def verification_status_for_user(cls, user, course_id, user_enrollment_mode): + """ + Returns the verification status for use in grade report. + """ + if user_enrollment_mode not in CourseMode.VERIFIED_MODES: + return 'N/A' + + user_is_verified = cls.user_is_verified(user) + + if not user_is_verified: + return 'Not ID Verified' + else: + user_is_re_verified = cls.user_is_reverified_for_all(course_id, user) + if not user_is_re_verified: + return 'ID Verification Expired' + else: + return 'ID Verified' + class VerificationCheckpoint(models.Model): """Represents a point at which a user is challenged to reverify his or her identity. diff --git a/lms/djangoapps/verify_student/tests/factories.py b/lms/djangoapps/verify_student/tests/factories.py new file mode 100644 index 0000000000..6514c594a9 --- /dev/null +++ b/lms/djangoapps/verify_student/tests/factories.py @@ -0,0 +1,15 @@ +""" +Factories related to student verification. +""" + +from factory.django import DjangoModelFactory +from verify_student.models import SoftwareSecurePhotoVerification + + +class SoftwareSecurePhotoVerificationFactory(DjangoModelFactory): + """ + Factory for SoftwareSecurePhotoVerification + """ + FACTORY_FOR = SoftwareSecurePhotoVerification + + status = 'approved' diff --git a/lms/djangoapps/verify_student/tests/test_models.py b/lms/djangoapps/verify_student/tests/test_models.py index 4e66ba20ff..b55e5df0b5 100644 --- a/lms/djangoapps/verify_student/tests/test_models.py +++ b/lms/djangoapps/verify_student/tests/test_models.py @@ -128,7 +128,8 @@ def mock_software_secure_post_unavailable(url, headers=None, data=None, **kwargs @patch('verify_student.models.S3Connection', new=MockS3Connection) @patch('verify_student.models.Key', new=MockKey) @patch('verify_student.models.requests.post', new=mock_software_secure_post) -class TestPhotoVerification(TestCase): +@ddt.ddt +class TestPhotoVerification(ModuleStoreTestCase): def test_state_transitions(self): """ @@ -505,6 +506,29 @@ class TestPhotoVerification(TestCase): result = SoftwareSecurePhotoVerification.verification_for_datetime(deadline, query) self.assertEqual(result, second_attempt) + @ddt.unpack + @ddt.data( + {'enrollment_mode': 'honor', 'status': (None, None), 'output': 'N/A'}, + {'enrollment_mode': 'verified', 'status': (False, False), 'output': 'Not ID Verified'}, + {'enrollment_mode': 'verified', 'status': (True, True), 'output': 'ID Verified'}, + {'enrollment_mode': 'verified', 'status': (True, False), 'output': 'ID Verification Expired'} + ) + def test_verification_status_for_user(self, enrollment_mode, status, output): + """ + Verify verification_status_for_user returns correct status. + """ + user = UserFactory.create() + course = CourseFactory.create() + + user_reverified_path = 'verify_student.models.SoftwareSecurePhotoVerification.user_is_reverified_for_all' + with patch('verify_student.models.SoftwareSecurePhotoVerification.user_is_verified') as mock_verification: + with patch(user_reverified_path) as mock_re_verification: + mock_verification.return_value = status[0] + mock_re_verification.return_value = status[1] + + status = SoftwareSecurePhotoVerification.verification_status_for_user(user, course.id, enrollment_mode) + self.assertEqual(status, output) + @patch.dict(settings.VERIFY_STUDENT, FAKE_SETTINGS) @patch('verify_student.models.S3Connection', new=MockS3Connection)