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
1ca37737
Commit
1ca37737
authored
Jul 02, 2015
by
Matt Drayer
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #8566 from edx/ziafazal/SOL-980
ziafazal/SOL-980: Generate certificates from instructor dashboard
parents
8824d032
611d16b2
Hide whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
473 additions
and
18 deletions
+473
-18
common/test/acceptance/pages/lms/instructor_dashboard.py
+47
-0
common/test/acceptance/pages/studio/settings.py
+12
-0
common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py
+44
-0
common/test/db_fixtures/certificates_web_view.json
+9
-0
lms/djangoapps/certificates/api.py
+8
-2
lms/djangoapps/instructor/tests/test_certificates.py
+39
-1
lms/djangoapps/instructor/views/api.py
+19
-0
lms/djangoapps/instructor/views/api_urls.py
+4
-0
lms/djangoapps/instructor/views/instructor_dashboard.py
+9
-1
lms/djangoapps/instructor_task/api.py
+16
-1
lms/djangoapps/instructor_task/tasks.py
+18
-1
lms/djangoapps/instructor_task/tasks_helper.py
+60
-2
lms/djangoapps/instructor_task/tests/test_api.py
+12
-1
lms/djangoapps/instructor_task/tests/test_tasks.py
+37
-4
lms/djangoapps/instructor_task/tests/test_tasks_helper.py
+56
-3
lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee
+3
-0
lms/static/js/instructor_dashboard/certificates.js
+57
-2
lms/templates/instructor/instructor_dashboard_2/certificates.html
+23
-0
No files found.
common/test/acceptance/pages/lms/instructor_dashboard.py
View file @
1ca37737
...
...
@@ -57,6 +57,15 @@ class InstructorDashboardPage(CoursePage):
student_admin_section
.
wait_for_page
()
return
student_admin_section
def
select_certificates
(
self
):
"""
Selects the certificates tab and returns the CertificatesSection
"""
self
.
q
(
css
=
'a[data-section=certificates]'
)
.
first
.
click
()
certificates_section
=
CertificatesPage
(
self
.
browser
)
certificates_section
.
wait_for_page
()
return
certificates_section
@staticmethod
def
get_asset_path
(
file_name
):
"""
...
...
@@ -884,3 +893,41 @@ class StudentAdminPage(PageObject):
"""
input_box
=
self
.
student_email_input
.
first
.
results
[
0
]
input_box
.
send_keys
(
email_addres
)
class
CertificatesPage
(
PageObject
):
"""
Certificates section of the Instructor dashboard.
"""
url
=
None
PAGE_SELECTOR
=
'section#certificates'
def
is_browser_on_page
(
self
):
return
self
.
q
(
css
=
'a[data-section=certificates].active-section'
)
.
present
def
get_selector
(
self
,
css_selector
):
"""
Makes query selector by pre-pending certificates section
"""
return
self
.
q
(
css
=
' '
.
join
([
self
.
PAGE_SELECTOR
,
css_selector
]))
@property
def
generate_certificates_button
(
self
):
"""
Returns the "Generate Certificates" button.
"""
return
self
.
get_selector
(
'#btn-start-generating-certificates'
)
@property
def
certificate_generation_status
(
self
):
"""
Returns certificate generation status message container.
"""
return
self
.
get_selector
(
'div.certificate-generation-status'
)
@property
def
pending_tasks_section
(
self
):
"""
Returns the "Pending Instructor Tasks" section.
"""
return
self
.
get_selector
(
'div.running-tasks-container'
)
common/test/acceptance/pages/studio/settings.py
View file @
1ca37737
...
...
@@ -49,6 +49,10 @@ class SettingsPage(CoursePage):
"""
Returns the pre-requisite course drop down field options.
"""
self
.
wait_for_element_visibility
(
'#pre-requisite-course'
,
'Prerequisite course element is available'
)
return
self
.
get_elements
(
'#pre-requisite-course'
)
@property
...
...
@@ -56,6 +60,10 @@ class SettingsPage(CoursePage):
"""
Returns the enable entrance exam checkbox.
"""
self
.
wait_for_element_visibility
(
'#entrance-exam-enabled'
,
'Entrance exam checkbox is available'
)
return
self
.
get_element
(
'#entrance-exam-enabled'
)
@property
...
...
@@ -64,6 +72,10 @@ class SettingsPage(CoursePage):
Returns the alert confirmation element, which contains text
such as 'Your changes have been saved.'
"""
self
.
wait_for_element_visibility
(
'#alert-confirmation-title'
,
'Alert confirmation title element is available'
)
return
self
.
get_element
(
'#alert-confirmation-title'
)
@property
...
...
common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py
View file @
1ca37737
...
...
@@ -4,6 +4,7 @@ End-to-end tests for the LMS Instructor Dashboard.
"""
from
nose.plugins.attrib
import
attr
from
bok_choy.promise
import
EmptyPromise
from
..helpers
import
UniqueCourseTest
,
get_modal_alert
,
EventsTestMixin
from
...pages.common.logout
import
LogoutPage
...
...
@@ -396,3 +397,46 @@ class DataDownloadsTest(BaseInstructorDashboardTest):
self
.
data_download_section
.
wait_for_available_report
()
self
.
verify_report_requested_event
(
report_name
)
self
.
verify_report_download
(
report_name
)
@attr
(
'shard_5'
)
class
CertificatesTest
(
BaseInstructorDashboardTest
):
"""
Tests for Certificates functionality on instructor dashboard.
"""
def
setUp
(
self
):
super
(
CertificatesTest
,
self
)
.
setUp
()
self
.
course_fixture
=
CourseFixture
(
**
self
.
course_info
)
.
install
()
self
.
log_in_as_instructor
()
instructor_dashboard_page
=
self
.
visit_instructor_dashboard
()
self
.
certificates_section
=
instructor_dashboard_page
.
select_certificates
()
def
test_generate_certificates_buttons_is_visible
(
self
):
"""
Scenario: On the Certificates tab of the Instructor Dashboard, Generate Certificates button is visible.
Given that I am on the Certificates tab on the Instructor Dashboard
Then I see 'Generate Certificates' button
And when I click on 'Generate Certificates' button
Then I should see a status message and 'Generate Certificates' button should be disabled.
"""
self
.
assertTrue
(
self
.
certificates_section
.
generate_certificates_button
.
visible
)
self
.
certificates_section
.
generate_certificates_button
.
click
()
alert
=
get_modal_alert
(
self
.
certificates_section
.
browser
)
alert
.
accept
()
self
.
certificates_section
.
wait_for_ajax
()
EmptyPromise
(
lambda
:
self
.
certificates_section
.
certificate_generation_status
.
visible
,
'Certificate generation status shown'
)
.
fulfill
()
disabled
=
self
.
certificates_section
.
generate_certificates_button
.
attrs
(
'disabled'
)
self
.
assertEqual
(
disabled
[
0
],
'true'
)
def
test_pending_tasks_section_is_visible
(
self
):
"""
Scenario: On the Certificates tab of the Instructor Dashboard, Pending Instructor Tasks section is visible.
Given that I am on the Certificates tab on the Instructor Dashboard
Then I see 'Pending Instructor Tasks' section
"""
self
.
assertTrue
(
self
.
certificates_section
.
pending_tasks_section
.
visible
)
common/test/db_fixtures/certificates_web_view.json
View file @
1ca37737
[
{
"pk"
:
99
,
"model"
:
"certificates.certificategenerationconfiguration"
,
"fields"
:
{
"change_date"
:
"2015-06-18 11:02:13"
,
"changed_by"
:
99
,
"enabled"
:
true
}
},
{
"pk"
:
99
,
"model"
:
"auth.user"
,
"fields"
:
{
"date_joined"
:
"2015-06-12 11:02:13"
,
...
...
lms/djangoapps/certificates/api.py
View file @
1ca37737
...
...
@@ -26,7 +26,8 @@ from certificates.queue import XQueueCertInterface
log
=
logging
.
getLogger
(
"edx.certificate"
)
def
generate_user_certificates
(
student
,
course_key
,
course
=
None
,
insecure
=
False
,
generation_mode
=
'batch'
):
def
generate_user_certificates
(
student
,
course_key
,
course
=
None
,
insecure
=
False
,
generation_mode
=
'batch'
,
forced_grade
=
None
):
"""
It will add the add-cert request into the xqueue.
...
...
@@ -45,12 +46,17 @@ def generate_user_certificates(student, course_key, course=None, insecure=False,
insecure - (Boolean)
generation_mode - who has requested certificate generation. Its value should `batch`
in case of django command and `self` if student initiated the request.
forced_grade - a string indicating to replace grade parameter. if present grading
will be skipped.
"""
xqueue
=
XQueueCertInterface
()
if
insecure
:
xqueue
.
use_https
=
False
generate_pdf
=
not
has_html_certificates_enabled
(
course_key
,
course
)
status
,
cert
=
xqueue
.
add_cert
(
student
,
course_key
,
course
=
course
,
generate_pdf
=
generate_pdf
)
status
,
cert
=
xqueue
.
add_cert
(
student
,
course_key
,
course
=
course
,
generate_pdf
=
generate_pdf
,
forced_grade
=
forced_grade
)
if
status
in
[
CertificateStatuses
.
generating
,
CertificateStatuses
.
downloadable
]:
emit_certificate_event
(
'created'
,
student
,
course_key
,
course
,
{
'user_id'
:
student
.
id
,
...
...
lms/djangoapps/instructor/tests/test_certificates.py
View file @
1ca37737
...
...
@@ -2,13 +2,15 @@
import
contextlib
import
ddt
import
mock
import
json
from
nose.plugins.attrib
import
attr
from
django.core.urlresolvers
import
reverse
from
django.test.utils
import
override_settings
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
config_models.models
import
cache
from
courseware.tests.factories
import
GlobalStaffFactory
,
InstructorFactory
from
courseware.tests.factories
import
GlobalStaffFactory
,
InstructorFactory
,
UserFactory
from
certificates.models
import
CertificateGenerationConfiguration
from
certificates
import
api
as
certs_api
...
...
@@ -222,3 +224,39 @@ class CertificatesInstructorApiTest(ModuleStoreTestCase):
)
expected_redirect
+=
'#view-certificates'
self
.
assertRedirects
(
response
,
expected_redirect
)
def
test_certificate_generation_api_without_global_staff
(
self
):
"""
Test certificates generation api endpoint returns permission denied if
user who made the request is not member of global staff.
"""
user
=
UserFactory
.
create
()
self
.
client
.
login
(
username
=
user
.
username
,
password
=
'test'
)
url
=
reverse
(
'start_certificate_generation'
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course
.
id
)}
)
response
=
self
.
client
.
post
(
url
)
self
.
assertEqual
(
response
.
status_code
,
403
)
self
.
client
.
login
(
username
=
self
.
instructor
.
username
,
password
=
'test'
)
response
=
self
.
client
.
post
(
url
)
self
.
assertEqual
(
response
.
status_code
,
403
)
def
test_certificate_generation_api_with_global_staff
(
self
):
"""
Test certificates generation api endpoint returns success status when called with
valid course key
"""
self
.
client
.
login
(
username
=
self
.
global_staff
.
username
,
password
=
'test'
)
url
=
reverse
(
'start_certificate_generation'
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course
.
id
)}
)
response
=
self
.
client
.
post
(
url
)
self
.
assertEqual
(
response
.
status_code
,
200
)
res_json
=
json
.
loads
(
response
.
content
)
self
.
assertIsNotNone
(
res_json
[
'message'
])
self
.
assertIsNotNone
(
res_json
[
'task_id'
])
lms/djangoapps/instructor/views/api.py
View file @
1ca37737
...
...
@@ -2642,3 +2642,22 @@ def mark_student_can_skip_entrance_exam(request, course_id): # pylint: disable=
'message'
:
message
,
}
return
JsonResponse
(
response_payload
)
@ensure_csrf_cookie
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
@require_global_staff
@require_POST
def
start_certificate_generation
(
request
,
course_id
):
"""
Start generating certificates for all students enrolled in given course.
"""
course_key
=
CourseKey
.
from_string
(
course_id
)
task
=
instructor_task
.
api
.
generate_certificates_for_all_students
(
request
,
course_key
)
message
=
_
(
'Certificate generation task for all students of this course has been started. '
'You can view the status of the generation task in the "Pending Tasks" section.'
)
response_payload
=
{
'message'
:
message
,
'task_id'
:
task
.
task_id
}
return
JsonResponse
(
response_payload
)
lms/djangoapps/instructor/views/api_urls.py
View file @
1ca37737
...
...
@@ -132,4 +132,8 @@ urlpatterns = patterns(
url
(
r'^enable_certificate_generation$'
,
'instructor.views.api.enable_certificate_generation'
,
name
=
'enable_certificate_generation'
),
url
(
r'^start_certificate_generation'
,
'instructor.views.api.start_certificate_generation'
,
name
=
'start_certificate_generation'
),
)
lms/djangoapps/instructor/views/instructor_dashboard.py
View file @
1ca37737
...
...
@@ -259,7 +259,15 @@ def _section_certificates(course):
'enable_certificate_generation'
:
reverse
(
'enable_certificate_generation'
,
kwargs
=
{
'course_id'
:
course
.
id
}
)
),
'start_certificate_generation'
:
reverse
(
'start_certificate_generation'
,
kwargs
=
{
'course_id'
:
course
.
id
}
),
'list_instructor_tasks_url'
:
reverse
(
'list_instructor_tasks'
,
kwargs
=
{
'course_id'
:
course
.
id
}
),
}
}
...
...
lms/djangoapps/instructor_task/api.py
View file @
1ca37737
...
...
@@ -24,7 +24,8 @@ from instructor_task.tasks import (
cohort_students
,
enrollment_report_features_csv
,
calculate_may_enroll_csv
,
exec_summary_report_csv
exec_summary_report_csv
,
generate_certificates
,
)
from
instructor_task.api_helper
import
(
...
...
@@ -419,3 +420,17 @@ def submit_cohort_students(request, course_key, file_name):
task_key
=
""
return
submit_task
(
request
,
task_type
,
task_class
,
course_key
,
task_input
,
task_key
)
def
generate_certificates_for_all_students
(
request
,
course_key
):
# pylint: disable=invalid-name
"""
Submits a task to generate certificates for all students enrolled in the course.
Raises AlreadyRunningError if certificates are currently being generated.
"""
task_type
=
'generate_certificates_all_student'
task_class
=
generate_certificates
task_input
=
{}
task_key
=
""
return
submit_task
(
request
,
task_type
,
task_class
,
course_key
,
task_input
,
task_key
)
lms/djangoapps/instructor_task/tasks.py
View file @
1ca37737
...
...
@@ -40,7 +40,8 @@ from instructor_task.tasks_helper import (
cohort_students_and_upload
,
upload_enrollment_report
,
upload_may_enroll_csv
,
upload_exec_summary_report
upload_exec_summary_report
,
generate_students_certificates
,
)
...
...
@@ -225,6 +226,22 @@ def calculate_may_enroll_csv(entry_id, xmodule_instance_args):
return
run_main_task
(
entry_id
,
task_fn
,
action_name
)
@task
(
base
=
BaseInstructorTask
,
routing_key
=
settings
.
GRADES_DOWNLOAD_ROUTING_KEY
)
# pylint: disable=not-callable
def
generate_certificates
(
entry_id
,
xmodule_instance_args
):
"""
Grade students and generate certificates.
"""
# Translators: This is a past-tense verb that is inserted into task progress messages as {action}.
action_name
=
ugettext_noop
(
'certificates generated'
)
TASK_LOG
.
info
(
u"Task:
%
s, InstructorTask ID:
%
s, Task type:
%
s, Preparing for task execution"
,
xmodule_instance_args
.
get
(
'task_id'
),
entry_id
,
action_name
)
task_fn
=
partial
(
generate_students_certificates
,
xmodule_instance_args
)
return
run_main_task
(
entry_id
,
task_fn
,
action_name
)
@task
(
base
=
BaseInstructorTask
)
# pylint: disable=E1102
def
cohort_students
(
entry_id
,
xmodule_instance_args
):
"""
...
...
lms/djangoapps/instructor_task/tasks_helper.py
View file @
1ca37737
...
...
@@ -18,6 +18,7 @@ from celery.states import SUCCESS, FAILURE
from
django.contrib.auth.models
import
User
from
django.core.files.storage
import
DefaultStorage
from
django.db
import
transaction
,
reset_queries
from
django.db.models
import
Q
import
dogstats_wrapper
as
dog_stats_api
from
pytz
import
UTC
from
StringIO
import
StringIO
...
...
@@ -33,7 +34,12 @@ 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
django.utils.translation
import
ugettext
as
_
from
certificates.models
import
CertificateWhitelist
,
certificate_info_for_user
from
certificates.models
import
(
CertificateWhitelist
,
certificate_info_for_user
,
CertificateStatuses
)
from
certificates.api
import
generate_user_certificates
from
courseware.courses
import
get_course_by_id
,
get_problems_in_section
from
courseware.grades
import
iterate_grades_for
from
courseware.models
import
StudentModule
...
...
@@ -50,7 +56,7 @@ 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
,
CourseAccessRole
from
verify_student.models
import
SoftwareSecurePhotoVerification
from
util.query
import
use_read_replica_if_available
# define different loggers for use within tasks and on client side
TASK_LOG
=
logging
.
getLogger
(
'edx.celery.task'
)
...
...
@@ -1247,6 +1253,44 @@ def upload_exec_summary_report(_xmodule_instance_args, _entry_id, course_id, _ta
return
task_progress
.
update_task_state
(
extra_meta
=
current_step
)
def
generate_students_certificates
(
_xmodule_instance_args
,
_entry_id
,
course_id
,
task_input
,
action_name
):
# pylint: disable=unused-argument
"""
For a given `course_id`, generate certificates for all students
that are enrolled.
"""
start_time
=
time
()
enrolled_students
=
use_read_replica_if_available
(
CourseEnrollment
.
objects
.
users_enrolled_in
(
course_id
))
task_progress
=
TaskProgress
(
action_name
,
enrolled_students
.
count
(),
start_time
)
current_step
=
{
'step'
:
'Calculating students already have certificates'
}
task_progress
.
update_task_state
(
extra_meta
=
current_step
)
students_require_certs
=
students_require_certificate
(
course_id
,
enrolled_students
)
task_progress
.
skipped
=
task_progress
.
total
-
len
(
students_require_certs
)
current_step
=
{
'step'
:
'Generating Certificates'
}
task_progress
.
update_task_state
(
extra_meta
=
current_step
)
course
=
modulestore
()
.
get_course
(
course_id
,
depth
=
0
)
# Generate certificate for each student
for
student
in
students_require_certs
:
task_progress
.
attempted
+=
1
status
=
generate_user_certificates
(
student
,
course_id
,
course
=
course
)
if
status
in
[
CertificateStatuses
.
generating
,
CertificateStatuses
.
downloadable
]:
task_progress
.
succeeded
+=
1
else
:
task_progress
.
failed
+=
1
return
task_progress
.
update_task_state
(
extra_meta
=
current_step
)
def
cohort_students_and_upload
(
_xmodule_instance_args
,
_entry_id
,
course_id
,
task_input
,
action_name
):
"""
Within a given course, cohort students in bulk, then upload the results
...
...
@@ -1330,3 +1374,17 @@ def cohort_students_and_upload(_xmodule_instance_args, _entry_id, course_id, tas
upload_csv_to_report_store
(
output_rows
,
'cohort_results'
,
course_id
,
start_date
)
return
task_progress
.
update_task_state
(
extra_meta
=
current_step
)
def
students_require_certificate
(
course_id
,
enrolled_students
):
""" Returns list of students where certificates needs to be generated.
Removing those students who have their certificate already generated
from total enrolled students for given course.
:param course_id:
:param enrolled_students:
"""
# compute those students where certificates already generated
students_already_have_certs
=
use_read_replica_if_available
(
User
.
objects
.
filter
(
~
Q
(
generatedcertificate__status
=
CertificateStatuses
.
unavailable
),
generatedcertificate__course_id
=
course_id
))
return
list
(
set
(
enrolled_students
)
-
set
(
students_already_have_certs
))
lms/djangoapps/instructor_task/tests/test_api.py
View file @
1ca37737
...
...
@@ -18,7 +18,8 @@ from instructor_task.api import (
submit_cohort_students
,
submit_detailed_enrollment_features_csv
,
submit_calculate_may_enroll_csv
,
submit_executive_summary_report
submit_executive_summary_report
,
generate_certificates_for_all_students
,
)
from
instructor_task.api_helper
import
AlreadyRunningError
...
...
@@ -236,3 +237,13 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa
file_name
=
u'filename.csv'
)
self
.
_test_resubmission
(
api_call
)
def
test_submit_generate_certs_students
(
self
):
"""
Tests certificates generation task submission api
"""
api_call
=
lambda
:
generate_certificates_for_all_students
(
self
.
create_task_request
(
self
.
instructor
),
self
.
course
.
id
)
self
.
_test_resubmission
(
api_call
)
lms/djangoapps/instructor_task/tests/test_tasks.py
View file @
1ca37737
...
...
@@ -22,7 +22,12 @@ from student.tests.factories import UserFactory, CourseEnrollmentFactory
from
instructor_task.models
import
InstructorTask
from
instructor_task.tests.test_base
import
InstructorTaskModuleTestCase
from
instructor_task.tests.factories
import
InstructorTaskFactory
from
instructor_task.tasks
import
rescore_problem
,
reset_problem_attempts
,
delete_problem_state
from
instructor_task.tasks
import
(
rescore_problem
,
reset_problem_attempts
,
delete_problem_state
,
generate_certificates
,
)
from
instructor_task.tasks_helper
import
UpdateProblemModuleStateError
PROBLEM_URL_NAME
=
"test_urlname"
...
...
@@ -97,15 +102,20 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
with
self
.
assertRaises
(
ItemNotFoundError
):
self
.
_run_task_with_mock_celery
(
task_class
,
task_entry
.
id
,
task_entry
.
task_id
)
def
_test_run_with_task
(
self
,
task_class
,
action_name
,
expected_num_succeeded
,
expected_num_skipped
=
0
):
def
_test_run_with_task
(
self
,
task_class
,
action_name
,
expected_num_succeeded
,
expected_num_skipped
=
0
,
expected_attempted
=
0
,
expected_total
=
0
):
"""Run a task and check the number of StudentModules processed."""
task_entry
=
self
.
_create_input_entry
()
status
=
self
.
_run_task_with_mock_celery
(
task_class
,
task_entry
.
id
,
task_entry
.
task_id
)
expected_attempted
=
expected_attempted
\
if
expected_attempted
else
expected_num_succeeded
+
expected_num_skipped
expected_total
=
expected_total
\
if
expected_total
else
expected_num_succeeded
+
expected_num_skipped
# check return value
self
.
assertEquals
(
status
.
get
(
'attempted'
),
expected_
num_succeeded
+
expected_num_skipp
ed
)
self
.
assertEquals
(
status
.
get
(
'attempted'
),
expected_
attempt
ed
)
self
.
assertEquals
(
status
.
get
(
'succeeded'
),
expected_num_succeeded
)
self
.
assertEquals
(
status
.
get
(
'skipped'
),
expected_num_skipped
)
self
.
assertEquals
(
status
.
get
(
'total'
),
expected_
num_succeeded
+
expected_num_skipped
)
self
.
assertEquals
(
status
.
get
(
'total'
),
expected_
total
)
self
.
assertEquals
(
status
.
get
(
'action_name'
),
action_name
)
self
.
assertGreater
(
status
.
get
(
'duration_ms'
),
0
)
# compare with entry in table:
...
...
@@ -438,3 +448,26 @@ class TestDeleteStateInstructorTask(TestInstructorTasks):
StudentModule
.
objects
.
get
(
course_id
=
self
.
course
.
id
,
student
=
student
,
module_state_key
=
self
.
location
)
class
TestCertificateGenerationnstructorTask
(
TestInstructorTasks
):
"""Tests instructor task that generates student certificates."""
def
test_generate_certificates_missing_current_task
(
self
):
"""
Test error is raised when certificate generation task run without current task
"""
self
.
_test_missing_current_task
(
generate_certificates
)
def
test_generate_certificates_task_run
(
self
):
"""
Test certificate generation task run without any errors
"""
self
.
_test_run_with_task
(
generate_certificates
,
'certificates generated'
,
0
,
0
,
expected_attempted
=
1
,
expected_total
=
1
)
lms/djangoapps/instructor_task/tests/test_tasks_helper.py
View file @
1ca37737
...
...
@@ -11,14 +11,13 @@ from mock import Mock, patch
import
tempfile
import
unicodecsv
from
django.core.urlresolvers
import
reverse
from
django.test.utils
import
override_settings
from
capa.tests.response_xml_factory
import
MultipleChoiceResponseXMLFactory
from
certificates.models
import
CertificateStatuses
from
certificates.tests.factories
import
GeneratedCertificateFactory
,
CertificateWhitelistFactory
from
course_modes.models
import
CourseMode
from
courseware.tests.factories
import
InstructorFactory
from
instructor_task.models
import
ReportStore
from
instructor_task.tasks_helper
import
cohort_students_and_upload
,
upload_grades_csv
,
upload_students_csv
,
\
upload_enrollment_report
,
upload_exec_summary_report
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
...
...
@@ -38,6 +37,9 @@ from instructor_task.tasks_helper import (
upload_problem_grade_report
,
upload_students_csv
,
upload_may_enroll_csv
,
upload_enrollment_report
,
upload_exec_summary_report
,
generate_students_certificates
,
)
from
openedx.core.djangoapps.util.testing
import
ContentGroupTestCase
,
TestConditionalContent
...
...
@@ -1300,3 +1302,54 @@ class TestGradeReportEnrollmentAndCertificateInfo(TestReportMixin, InstructorTas
)
self
.
_verify_csv_data
(
user
.
username
,
expected_output
)
@override_settings
(
CERT_QUEUE
=
'test-queue'
)
class
TestCertificateGeneration
(
InstructorTaskModuleTestCase
):
"""
Test certificate generation task works.
"""
def
setUp
(
self
):
super
(
TestCertificateGeneration
,
self
)
.
setUp
()
self
.
initialize_course
()
def
test_certificate_generation_for_students
(
self
):
"""
Verify that certificates generated for all eligible students enrolled in a course.
"""
# create 10 students
students
=
[
self
.
create_student
(
username
=
'student_{}'
.
format
(
i
),
email
=
'student_{}@example.com'
.
format
(
i
))
for
i
in
xrange
(
1
,
11
)]
# mark 2 students to have certificates generated already
for
student
in
students
[:
2
]:
GeneratedCertificateFactory
.
create
(
user
=
student
,
course_id
=
self
.
course
.
id
,
status
=
CertificateStatuses
.
downloadable
,
mode
=
'honor'
)
# white-list 5 students
for
student
in
students
[
2
:
7
]:
CertificateWhitelistFactory
.
create
(
user
=
student
,
course_id
=
self
.
course
.
id
,
whitelist
=
True
)
current_task
=
Mock
()
current_task
.
update_state
=
Mock
()
with
self
.
assertNumQueries
(
112
):
with
patch
(
'instructor_task.tasks_helper._get_current_task'
)
as
mock_current_task
:
mock_current_task
.
return_value
=
current_task
with
patch
(
'capa.xqueue_interface.XQueueInterface.send_to_queue'
)
as
mock_queue
:
mock_queue
.
return_value
=
(
0
,
"Successfully queued"
)
result
=
generate_students_certificates
(
None
,
None
,
self
.
course
.
id
,
None
,
'certificates generated'
)
self
.
assertDictContainsSubset
(
{
'action_name'
:
'certificates generated'
,
'total'
:
10
,
'attempted'
:
8
,
'succeeded'
:
5
,
'failed'
:
3
,
'skipped'
:
2
},
result
)
lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee
View file @
1ca37737
...
...
@@ -179,6 +179,9 @@ setup_instructor_dashboard_sections = (idash_content) ->
,
constructor
:
window
.
InstructorDashboard
.
sections
.
CohortManagement
$element
:
idash_content
.
find
".
#{
CSS_IDASH_SECTION
}
#cohort_management"
,
constructor
:
window
.
InstructorDashboard
.
sections
.
Certificates
$element
:
idash_content
.
find
".
#{
CSS_IDASH_SECTION
}
#certificates"
]
sections_to_initialize
.
map
({
constructor
,
$element
})
->
...
...
lms/static/js/instructor_dashboard/certificates.js
View file @
1ca37737
var
edx
=
edx
||
{};
(
function
(
$
,
gettext
)
{
(
function
(
$
,
gettext
,
_
)
{
'use strict'
;
edx
.
instructor_dashboard
=
edx
.
instructor_dashboard
||
{};
...
...
@@ -33,5 +33,60 @@ var edx = edx || {};
$
(
'#refresh-example-certificate-status'
).
on
(
'click'
,
function
()
{
window
.
location
.
reload
();
});
/**
* Start generating certificates for all students.
*/
var
$section
=
$
(
"section#certificates"
);
$section
.
on
(
'click'
,
'#btn-start-generating-certificates'
,
function
(
event
)
{
if
(
!
confirm
(
gettext
(
'Start generating certificates for all students in this course?'
)
)
)
{
event
.
preventDefault
();
return
;
}
var
$btn_generating_certs
=
$
(
this
),
$certificate_generation_status
=
$
(
'.certificate-generation-status'
);
var
url
=
$btn_generating_certs
.
data
(
'endpoint'
);
$
.
ajax
({
type
:
"POST"
,
url
:
url
,
success
:
function
(
data
)
{
$btn_generating_certs
.
attr
(
'disabled'
,
'disabled'
);
$certificate_generation_status
.
text
(
data
.
message
);
},
error
:
function
(
jqXHR
,
textStatus
,
errorThrown
)
{
$certificate_generation_status
.
text
(
gettext
(
'Error while generating certificates. Please try again.'
));
}
});
});
});
var
Certificates
=
(
function
()
{
function
Certificates
(
$section
)
{
$section
.
data
(
'wrapper'
,
this
);
this
.
instructor_tasks
=
new
window
.
InstructorDashboard
.
util
.
PendingInstructorTasks
(
$section
);
}
Certificates
.
prototype
.
onClickTitle
=
function
()
{
return
this
.
instructor_tasks
.
task_poller
.
start
();
};
Certificates
.
prototype
.
onExit
=
function
()
{
return
this
.
instructor_tasks
.
task_poller
.
stop
();
};
return
Certificates
;
})();
_
.
defaults
(
window
,
{
InstructorDashboard
:
{}
});
})(
$
,
gettext
);
_
.
defaults
(
window
.
InstructorDashboard
,
{
sections
:
{}
});
_
.
defaults
(
window
.
InstructorDashboard
.
sections
,
{
Certificates
:
Certificates
});
})(
$
,
gettext
,
_
);
lms/templates/instructor/instructor_dashboard_2/certificates.html
View file @
1ca37737
...
...
@@ -55,4 +55,27 @@
<button
class=
"is-disabled"
disabled
>
${_('Enable Student-Generated Certificates')}
</button>
% endif
</div>
<hr
/>
<div
class=
"start-certificate-generation"
>
<h2>
${_("Generate Certificates")}
</h2>
<form
id=
"certificates-generating-form"
method=
"post"
action=
"${section_data['urls']['start_certificate_generation']}"
>
<input
type=
"button"
id=
"btn-start-generating-certificates"
value=
"${_('Generate Certificates')}"
data-endpoint=
"${section_data['urls']['start_certificate_generation']}"
/>
</form>
<div
class=
"certificate-generation-status"
></div>
</div>
%if settings.FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):
<div
class=
"running-tasks-container action-type-container"
>
<hr>
<h2>
${_("Pending Tasks")}
</h2>
<div
class=
"running-tasks-section"
>
<p>
${_("The status for any active tasks appears in a table below.")}
</p>
<br
/>
<div
class=
"running-tasks-table"
data-endpoint=
"${ section_data['urls']['list_instructor_tasks_url'] }"
></div>
</div>
<div
class=
"no-pending-tasks-message"
></div>
</div>
%endif
</div>
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