Merge pull request #7761 from edx/ammar/tnl1886-add-certificate-columns-in-grade-report
Add certificate columns in grade report
This commit is contained in:
@@ -41,6 +41,7 @@ class UserProfileFactory(DjangoModelFactory):
|
||||
gender = u'm'
|
||||
mailing_address = None
|
||||
goals = u'Learn a lot'
|
||||
allow_certificate = True
|
||||
|
||||
|
||||
class CourseModeFactory(DjangoModelFactory):
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
15
lms/djangoapps/verify_student/tests/factories.py
Normal file
15
lms/djangoapps/verify_student/tests/factories.py
Normal file
@@ -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'
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user