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
Show 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):
...
@@ -41,6 +41,7 @@ class UserProfileFactory(DjangoModelFactory):
gender
=
u'm'
gender
=
u'm'
mailing_address
=
None
mailing_address
=
None
goals
=
u'Learn a lot'
goals
=
u'Learn a lot'
allow_certificate
=
True
class
CourseModeFactory
(
DjangoModelFactory
):
class
CourseModeFactory
(
DjangoModelFactory
):
...
...
lms/djangoapps/certificates/models.py
View file @
63f4aa8c
...
@@ -187,6 +187,33 @@ def certificate_status_for_student(student, course_id):
...
@@ -187,6 +187,33 @@ def certificate_status_for_student(student, course_id):
return
{
'status'
:
CertificateStatuses
.
unavailable
,
'mode'
:
GeneratedCertificate
.
MODES
.
honor
}
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
):
class
ExampleCertificateSet
(
TimeStampedModel
):
"""A set of example certificates.
"""A set of example certificates.
...
...
lms/djangoapps/certificates/tests/factories.py
View file @
63f4aa8c
from
factory.django
import
DjangoModelFactory
from
factory.django
import
DjangoModelFactory
from
certificates.models
import
GeneratedCertificate
,
CertificateStatuses
,
CertificateHtmlViewConfiguration
from
certificates.models
import
(
GeneratedCertificate
,
CertificateStatuses
,
CertificateHtmlViewConfiguration
,
CertificateWhitelist
)
# Factories are self documenting
# Factories are self documenting
...
@@ -15,6 +17,14 @@ class GeneratedCertificateFactory(DjangoModelFactory):
...
@@ -15,6 +17,14 @@ class GeneratedCertificateFactory(DjangoModelFactory):
name
=
''
name
=
''
class
CertificateWhitelistFactory
(
DjangoModelFactory
):
FACTORY_FOR
=
CertificateWhitelist
course_id
=
None
whitelist
=
True
class
CertificateHtmlViewConfigurationFactory
(
DjangoModelFactory
):
class
CertificateHtmlViewConfigurationFactory
(
DjangoModelFactory
):
FACTORY_FOR
=
CertificateHtmlViewConfiguration
FACTORY_FOR
=
CertificateHtmlViewConfiguration
...
...
lms/djangoapps/certificates/tests/tests.py
View file @
63f4aa8c
"""
"""
Tests for the certificates models.
Tests for the certificates models.
"""
"""
from
ddt
import
ddt
,
data
,
unpack
from
mock
import
patch
from
mock
import
patch
from
django.conf
import
settings
from
django.conf
import
settings
...
@@ -9,7 +9,12 @@ from xmodule.modulestore.tests.factories import CourseFactory
...
@@ -9,7 +9,12 @@ from xmodule.modulestore.tests.factories import CourseFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
student.tests.factories
import
UserFactory
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
certificates.tests.factories
import
GeneratedCertificateFactory
from
util.milestones_helpers
import
(
from
util.milestones_helpers
import
(
...
@@ -19,6 +24,7 @@ from util.milestones_helpers import (
...
@@ -19,6 +24,7 @@ from util.milestones_helpers import (
)
)
@ddt
class
CertificatesModelTest
(
ModuleStoreTestCase
):
class
CertificatesModelTest
(
ModuleStoreTestCase
):
"""
"""
Tests for the GeneratedCertificate model
Tests for the GeneratedCertificate model
...
@@ -32,6 +38,26 @@ class CertificatesModelTest(ModuleStoreTestCase):
...
@@ -32,6 +38,26 @@ class CertificatesModelTest(ModuleStoreTestCase):
self
.
assertEqual
(
certificate_status
[
'status'
],
CertificateStatuses
.
unavailable
)
self
.
assertEqual
(
certificate_status
[
'status'
],
CertificateStatuses
.
unavailable
)
self
.
assertEqual
(
certificate_status
[
'mode'
],
GeneratedCertificate
.
MODES
.
honor
)
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
})
@patch.dict
(
settings
.
FEATURES
,
{
'ENABLE_PREREQUISITE_COURSES'
:
True
,
'MILESTONES_APP'
:
True
})
def
test_course_milestone_collected
(
self
):
def
test_course_milestone_collected
(
self
):
seed_milestone_relationship_types
()
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
...
@@ -22,6 +22,7 @@ from util.file import course_filename_prefix_generator, UniversalNewlineIterator
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
xmodule.split_test_module
import
get_split_user_partitions
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.courses
import
get_course_by_id
,
get_problems_in_section
from
courseware.grades
import
iterate_grades_for
from
courseware.grades
import
iterate_grades_for
from
courseware.models
import
StudentModule
from
courseware.models
import
StudentModule
...
@@ -36,6 +37,7 @@ from openedx.core.djangoapps.course_groups.models import CourseUserGroup
...
@@ -36,6 +37,7 @@ from openedx.core.djangoapps.course_groups.models import CourseUserGroup
from
opaque_keys.edx.keys
import
UsageKey
from
opaque_keys.edx.keys
import
UsageKey
from
openedx.core.djangoapps.course_groups.cohorts
import
add_user_to_cohort
,
is_course_cohorted
from
openedx.core.djangoapps.course_groups.cohorts
import
add_user_to_cohort
,
is_course_cohorted
from
student.models
import
CourseEnrollment
from
student.models
import
CourseEnrollment
from
verify_student.models
import
SoftwareSecurePhotoVerification
# define different loggers for use within tasks and on client side
# 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):
...
@@ -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
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
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,
...
@@ -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
)
experiment_partitions
=
get_split_user_partitions
(
course
.
user_partitions
)
group_configs_header
=
[
u'Experiment Group ({})'
.
format
(
partition
.
name
)
for
partition
in
experiment_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
# Loop over all our students and build our CSV lists in memory
header
=
None
header
=
None
rows
=
[]
rows
=
[]
...
@@ -623,7 +629,8 @@ def upload_grades_csv(_xmodule_instance_args, _entry_id, course_id, _task_input,
...
@@ -623,7 +629,8 @@ def upload_grades_csv(_xmodule_instance_args, _entry_id, course_id, _task_input,
if
not
header
:
if
not
header
:
header
=
[
section
[
'label'
]
for
section
in
gradeset
[
u'section_breakdown'
]]
header
=
[
section
[
'label'
]
for
section
in
gradeset
[
u'section_breakdown'
]]
rows
.
append
(
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
=
{
percents
=
{
...
@@ -642,6 +649,19 @@ def upload_grades_csv(_xmodule_instance_args, _entry_id, course_id, _task_input,
...
@@ -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
=
LmsPartitionService
(
student
,
course_id
)
.
get_group
(
partition
,
assign
=
False
)
group_configs_group_names
.
append
(
group
.
name
if
group
else
''
)
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
# 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
# found in the user's gradeset, just assume it's a 0. The aggregated
# grades for their sections and overall course will be calculated
# 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,
...
@@ -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
]
row_percents
=
[
percents
.
get
(
label
,
0.0
)
for
label
in
header
]
rows
.
append
(
rows
.
append
(
[
student
.
id
,
student
.
email
,
student
.
username
,
gradeset
[
'percent'
]]
+
[
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
:
else
:
# An empty gradeset means we failed to grade a student.
# 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
...
@@ -10,9 +10,11 @@ import unicodecsv
from
uuid
import
uuid4
from
uuid
import
uuid4
from
celery.states
import
SUCCESS
,
FAILURE
from
celery.states
import
SUCCESS
,
FAILURE
from
django.core.urlresolvers
import
reverse
from
django.conf
import
settings
from
django.conf
import
settings
from
django.test.testcases
import
TestCase
from
django.test.testcases
import
TestCase
from
django.contrib.auth.models
import
User
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
opaque_keys.edx.locations
import
Location
,
SlashSeparatedCourseKey
from
capa.tests.response_xml_factory
import
OptionResponseXMLFactory
from
capa.tests.response_xml_factory
import
OptionResponseXMLFactory
...
@@ -147,21 +149,21 @@ class InstructorTaskCourseTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase)
...
@@ -147,21 +149,21 @@ class InstructorTaskCourseTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase)
self
.
login
(
InstructorTaskCourseTestCase
.
get_user_email
(
username
),
"test"
)
self
.
login
(
InstructorTaskCourseTestCase
.
get_user_email
(
username
),
"test"
)
self
.
current_user
=
username
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."""
"""Creates a user and enrolls them in the test course."""
if
email
is
None
:
if
email
is
None
:
email
=
InstructorTaskCourseTestCase
.
get_user_email
(
username
)
email
=
InstructorTaskCourseTestCase
.
get_user_email
(
username
)
thisuser
=
UserFactory
.
create
(
username
=
username
,
email
=
email
,
is_staff
=
is_staff
)
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
return
thisuser
def
create_instructor
(
self
,
username
,
email
=
None
):
def
create_instructor
(
self
,
username
,
email
=
None
):
"""Creates an instructor for the test course."""
"""Creates an instructor for the test course."""
return
self
.
_create_user
(
username
,
email
,
is_staff
=
True
)
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."""
"""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
@staticmethod
def
get_task_status
(
task_id
):
def
get_task_status
(
task_id
):
...
@@ -236,6 +238,40 @@ class InstructorTaskModuleTestCase(InstructorTaskCourseTestCase):
...
@@ -236,6 +238,40 @@ class InstructorTaskModuleTestCase(InstructorTaskCourseTestCase):
module_state_key
=
descriptor
.
location
,
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
):
class
TestReportMixin
(
object
):
"""
"""
...
@@ -246,7 +282,7 @@ class TestReportMixin(object):
...
@@ -246,7 +282,7 @@ class TestReportMixin(object):
if
os
.
path
.
exists
(
reports_download_path
):
if
os
.
path
.
exists
(
reports_download_path
):
shutil
.
rmtree
(
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.
Verify that the last ReportStore CSV contains the expected content.
...
@@ -259,12 +295,20 @@ class TestReportMixin(object):
...
@@ -259,12 +295,20 @@ class TestReportMixin(object):
content and order of `expected_rows` matches the
content and order of `expected_rows` matches the
actual csv rows. When False (default), we only verify
actual csv rows. When False (default), we only verify
that the content matches.
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_store
=
ReportStore
.
from_config
()
report_csv_filename
=
report_store
.
links_for
(
self
.
course
.
id
)[
0
][
0
]
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
:
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
# Expand the dict reader generator so we don't lose it's content
csv_rows
=
[
row
for
row
in
unicodecsv
.
DictReader
(
csv_file
)]
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
:
if
verify_order
:
self
.
assertEqual
(
csv_rows
,
expected_rows
)
self
.
assertEqual
(
csv_rows
,
expected_rows
)
else
:
else
:
...
...
lms/djangoapps/instructor_task/tests/test_integration.py
View file @
63f4aa8c
...
@@ -43,39 +43,6 @@ class TestIntegrationTask(InstructorTaskModuleTestCase):
...
@@ -43,39 +43,6 @@ class TestIntegrationTask(InstructorTaskModuleTestCase):
Base class to provide general methods used for "integration" testing of particular tasks.
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
):
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."""
"""Confirm that expected values are stored in InstructorTask on task failure."""
instructor_task
=
InstructorTask
.
objects
.
get
(
id
=
entry_id
)
instructor_task
=
InstructorTask
.
objects
.
get
(
id
=
entry_id
)
...
@@ -606,7 +573,7 @@ class TestGradeReportConditionalContent(TestReportMixin, TestIntegrationTask):
...
@@ -606,7 +573,7 @@ class TestGradeReportConditionalContent(TestReportMixin, TestIntegrationTask):
"""
"""
self
.
assertDictContainsSubset
({
'attempted'
:
2
,
'succeeded'
:
2
,
'failed'
:
0
},
task_result
)
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.
Verify that the grades CSV contains the expected grades data.
...
@@ -642,7 +609,8 @@ class TestGradeReportConditionalContent(TestReportMixin, TestIntegrationTask):
...
@@ -642,7 +609,8 @@ class TestGradeReportConditionalContent(TestReportMixin, TestIntegrationTask):
user_partition_group
(
student
)
user_partition_group
(
student
)
)
)
for
student_grades
in
students_grades
for
student
,
grades
in
student_grades
.
iteritems
()
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
):
def
test_both_groups_problems
(
self
):
...
@@ -668,7 +636,8 @@ class TestGradeReportConditionalContent(TestReportMixin, TestIntegrationTask):
...
@@ -668,7 +636,8 @@ class TestGradeReportConditionalContent(TestReportMixin, TestIntegrationTask):
[
[
{
self
.
student_a
:
{
'grade'
:
'1.0'
,
'HW'
:
'1.0'
}},
{
self
.
student_a
:
{
'grade'
:
'1.0'
,
'HW'
:
'1.0'
}},
{
self
.
student_b
:
{
'grade'
:
'0.5'
,
'HW'
:
'0.5'
}}
{
self
.
student_b
:
{
'grade'
:
'0.5'
,
'HW'
:
'0.5'
}}
]
],
ignore_other_columns
=
True
)
)
def
test_one_group_problem
(
self
):
def
test_one_group_problem
(
self
):
...
@@ -690,5 +659,6 @@ class TestGradeReportConditionalContent(TestReportMixin, TestIntegrationTask):
...
@@ -690,5 +659,6 @@ class TestGradeReportConditionalContent(TestReportMixin, TestIntegrationTask):
[
[
{
self
.
student_a
:
{
'grade'
:
'1.0'
,
'HW'
:
'1.0'
}},
{
self
.
student_a
:
{
'grade'
:
'1.0'
,
'HW'
:
'1.0'
}},
{
self
.
student_b
:
{
'grade'
:
'0.0'
,
'HW'
:
'0.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
...
@@ -11,18 +11,21 @@ from mock import Mock, patch
import
tempfile
import
tempfile
import
unicodecsv
import
unicodecsv
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
capa.tests.response_xml_factory
import
MultipleChoiceResponseXMLFactory
from
student.tests.factories
import
UserFactory
from
certificates.tests.factories
import
GeneratedCertificateFactory
,
CertificateWhitelistFactory
from
student.models
import
CourseEnrollment
from
course_modes.models
import
CourseMode
from
xmodule.partitions.partitions
import
Group
,
UserPartition
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.models
import
CourseUserGroupPartitionGroup
from
openedx.core.djangoapps.course_groups.tests.helpers
import
CohortFactory
from
openedx.core.djangoapps.course_groups.tests.helpers
import
CohortFactory
import
openedx.core.djangoapps.user_api.course_tag.api
as
course_tag_api
import
openedx.core.djangoapps.user_api.course_tag.api
as
course_tag_api
from
openedx.core.djangoapps.user_api.partition_schemes
import
RandomUserPartitionScheme
from
openedx.core.djangoapps.user_api.partition_schemes
import
RandomUserPartitionScheme
from
instructor_task.models
import
ReportStore
from
student.tests.factories
import
UserFactory
from
instructor_task.tasks_helper
import
cohort_students_and_upload
,
upload_grades_csv
,
upload_students_csv
from
student.models
import
CourseEnrollment
from
instructor_task.tests.test_base
import
InstructorTaskCourseTestCase
,
TestReportMixin
from
verify_student.tests.factories
import
SoftwareSecurePhotoVerificationFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
from
xmodule.partitions.partitions
import
Group
,
UserPartition
@ddt.ddt
@ddt.ddt
...
@@ -250,7 +253,7 @@ class TestInstructorGradeReport(TestReportMixin, InstructorTaskCourseTestCase):
...
@@ -250,7 +253,7 @@ class TestInstructorGradeReport(TestReportMixin, InstructorTaskCourseTestCase):
mock_iterate_grades_for
.
return_value
=
[
mock_iterate_grades_for
.
return_value
=
[
(
(
self
.
create_student
(
'username'
,
'student@example.com'
),
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'
'Cannot grade student'
)
)
]
]
...
@@ -538,3 +541,141 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase):
...
@@ -538,3 +541,141 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase):
],
],
verify_order
=
False
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
...
@@ -13,6 +13,8 @@ from email.utils import formatdate
import
functools
import
functools
import
json
import
json
import
logging
import
logging
from
course_modes.models
import
CourseMode
import
pytz
import
pytz
import
requests
import
requests
import
uuid
import
uuid
...
@@ -935,6 +937,25 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
...
@@ -935,6 +937,25 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
attempt
.
submit
()
attempt
.
submit
()
return
attempt
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
):
class
VerificationCheckpoint
(
models
.
Model
):
"""Represents a point at which a user is challenged to reverify his or her identity.
"""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
...
@@ -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.S3Connection'
,
new
=
MockS3Connection
)
@patch
(
'verify_student.models.Key'
,
new
=
MockKey
)
@patch
(
'verify_student.models.Key'
,
new
=
MockKey
)
@patch
(
'verify_student.models.requests.post'
,
new
=
mock_software_secure_post
)
@patch
(
'verify_student.models.requests.post'
,
new
=
mock_software_secure_post
)
class
TestPhotoVerification
(
TestCase
):
@ddt.ddt
class
TestPhotoVerification
(
ModuleStoreTestCase
):
def
test_state_transitions
(
self
):
def
test_state_transitions
(
self
):
"""
"""
...
@@ -505,6 +506,29 @@ class TestPhotoVerification(TestCase):
...
@@ -505,6 +506,29 @@ class TestPhotoVerification(TestCase):
result
=
SoftwareSecurePhotoVerification
.
verification_for_datetime
(
deadline
,
query
)
result
=
SoftwareSecurePhotoVerification
.
verification_for_datetime
(
deadline
,
query
)
self
.
assertEqual
(
result
,
second_attempt
)
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.dict
(
settings
.
VERIFY_STUDENT
,
FAKE_SETTINGS
)
@patch
(
'verify_student.models.S3Connection'
,
new
=
MockS3Connection
)
@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