Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
63f4aa8c
Commit
63f4aa8c
authored
May 06, 2015
by
Muhammad Ammar
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #7761 from edx/ammar/tnl1886-add-certificate-columns-in-grade-report
Add certificate columns in grade report
parents
4bd15cf2
3bb7a250
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
358 additions
and
58 deletions
+358
-58
common/djangoapps/student/tests/factories.py
+1
-0
lms/djangoapps/certificates/models.py
+27
-0
lms/djangoapps/certificates/tests/factories.py
+11
-1
lms/djangoapps/certificates/tests/tests.py
+28
-2
lms/djangoapps/instructor_task/tasks_helper.py
+24
-3
lms/djangoapps/instructor_task/tests/test_base.py
+49
-5
lms/djangoapps/instructor_task/tests/test_integration.py
+7
-37
lms/djangoapps/instructor_task/tests/test_tasks_helper.py
+150
-9
lms/djangoapps/verify_student/models.py
+21
-0
lms/djangoapps/verify_student/tests/factories.py
+15
-0
lms/djangoapps/verify_student/tests/test_models.py
+25
-1
No files found.
common/djangoapps/student/tests/factories.py
View file @
63f4aa8c
...
...
@@ -41,6 +41,7 @@ class UserProfileFactory(DjangoModelFactory):
gender
=
u'm'
mailing_address
=
None
goals
=
u'Learn a lot'
allow_certificate
=
True
class
CourseModeFactory
(
DjangoModelFactory
):
...
...
lms/djangoapps/certificates/models.py
View file @
63f4aa8c
...
...
@@ -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.
...
...
lms/djangoapps/certificates/tests/factories.py
View file @
63f4aa8c
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
...
...
lms/djangoapps/certificates/tests/tests.py
View file @
63f4aa8c
"""
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
()
...
...
lms/djangoapps/instructor_task/tasks_helper.py
View file @
63f4aa8c
...
...
@@ -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.
...
...
lms/djangoapps/instructor_task/tests/test_base.py
View file @
63f4aa8c
...
...
@@ -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
:
...
...
lms/djangoapps/instructor_task/tests/test_integration.py
View file @
63f4aa8c
...
...
@@ -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
)
lms/djangoapps/instructor_task/tests/test_tasks_helper.py
View file @
63f4aa8c
...
...
@@ -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
)
lms/djangoapps/verify_student/models.py
View file @
63f4aa8c
...
...
@@ -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.
...
...
lms/djangoapps/verify_student/tests/factories.py
0 → 100644
View file @
63f4aa8c
"""
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'
lms/djangoapps/verify_student/tests/test_models.py
View file @
63f4aa8c
...
...
@@ -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
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment