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
Show 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):
...
@@ -57,6 +57,15 @@ class InstructorDashboardPage(CoursePage):
student_admin_section
.
wait_for_page
()
student_admin_section
.
wait_for_page
()
return
student_admin_section
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
@staticmethod
def
get_asset_path
(
file_name
):
def
get_asset_path
(
file_name
):
"""
"""
...
@@ -884,3 +893,41 @@ class StudentAdminPage(PageObject):
...
@@ -884,3 +893,41 @@ class StudentAdminPage(PageObject):
"""
"""
input_box
=
self
.
student_email_input
.
first
.
results
[
0
]
input_box
=
self
.
student_email_input
.
first
.
results
[
0
]
input_box
.
send_keys
(
email_addres
)
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):
...
@@ -49,6 +49,10 @@ class SettingsPage(CoursePage):
"""
"""
Returns the pre-requisite course drop down field options.
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'
)
return
self
.
get_elements
(
'#pre-requisite-course'
)
@property
@property
...
@@ -56,6 +60,10 @@ class SettingsPage(CoursePage):
...
@@ -56,6 +60,10 @@ class SettingsPage(CoursePage):
"""
"""
Returns the enable entrance exam checkbox.
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'
)
return
self
.
get_element
(
'#entrance-exam-enabled'
)
@property
@property
...
@@ -64,6 +72,10 @@ class SettingsPage(CoursePage):
...
@@ -64,6 +72,10 @@ class SettingsPage(CoursePage):
Returns the alert confirmation element, which contains text
Returns the alert confirmation element, which contains text
such as 'Your changes have been saved.'
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'
)
return
self
.
get_element
(
'#alert-confirmation-title'
)
@property
@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.
...
@@ -4,6 +4,7 @@ End-to-end tests for the LMS Instructor Dashboard.
"""
"""
from
nose.plugins.attrib
import
attr
from
nose.plugins.attrib
import
attr
from
bok_choy.promise
import
EmptyPromise
from
..helpers
import
UniqueCourseTest
,
get_modal_alert
,
EventsTestMixin
from
..helpers
import
UniqueCourseTest
,
get_modal_alert
,
EventsTestMixin
from
...pages.common.logout
import
LogoutPage
from
...pages.common.logout
import
LogoutPage
...
@@ -396,3 +397,46 @@ class DataDownloadsTest(BaseInstructorDashboardTest):
...
@@ -396,3 +397,46 @@ class DataDownloadsTest(BaseInstructorDashboardTest):
self
.
data_download_section
.
wait_for_available_report
()
self
.
data_download_section
.
wait_for_available_report
()
self
.
verify_report_requested_event
(
report_name
)
self
.
verify_report_requested_event
(
report_name
)
self
.
verify_report_download
(
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
,
"pk"
:
99
,
"model"
:
"certificates.certificategenerationconfiguration"
,
"fields"
:
{
"change_date"
:
"2015-06-18 11:02:13"
,
"changed_by"
:
99
,
"enabled"
:
true
}
},
{
"pk"
:
99
,
"model"
:
"auth.user"
,
"model"
:
"auth.user"
,
"fields"
:
{
"fields"
:
{
"date_joined"
:
"2015-06-12 11:02:13"
,
"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
...
@@ -26,7 +26,8 @@ from certificates.queue import XQueueCertInterface
log
=
logging
.
getLogger
(
"edx.certificate"
)
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.
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,
...
@@ -45,12 +46,17 @@ def generate_user_certificates(student, course_key, course=None, insecure=False,
insecure - (Boolean)
insecure - (Boolean)
generation_mode - who has requested certificate generation. Its value should `batch`
generation_mode - who has requested certificate generation. Its value should `batch`
in case of django command and `self` if student initiated the request.
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
()
xqueue
=
XQueueCertInterface
()
if
insecure
:
if
insecure
:
xqueue
.
use_https
=
False
xqueue
.
use_https
=
False
generate_pdf
=
not
has_html_certificates_enabled
(
course_key
,
course
)
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
]:
if
status
in
[
CertificateStatuses
.
generating
,
CertificateStatuses
.
downloadable
]:
emit_certificate_event
(
'created'
,
student
,
course_key
,
course
,
{
emit_certificate_event
(
'created'
,
student
,
course_key
,
course
,
{
'user_id'
:
student
.
id
,
'user_id'
:
student
.
id
,
...
...
lms/djangoapps/instructor/tests/test_certificates.py
View file @
1ca37737
...
@@ -2,13 +2,15 @@
...
@@ -2,13 +2,15 @@
import
contextlib
import
contextlib
import
ddt
import
ddt
import
mock
import
mock
import
json
from
nose.plugins.attrib
import
attr
from
nose.plugins.attrib
import
attr
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
django.test.utils
import
override_settings
from
django.test.utils
import
override_settings
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
config_models.models
import
cache
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.models
import
CertificateGenerationConfiguration
from
certificates
import
api
as
certs_api
from
certificates
import
api
as
certs_api
...
@@ -222,3 +224,39 @@ class CertificatesInstructorApiTest(ModuleStoreTestCase):
...
@@ -222,3 +224,39 @@ class CertificatesInstructorApiTest(ModuleStoreTestCase):
)
)
expected_redirect
+=
'#view-certificates'
expected_redirect
+=
'#view-certificates'
self
.
assertRedirects
(
response
,
expected_redirect
)
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=
...
@@ -2642,3 +2642,22 @@ def mark_student_can_skip_entrance_exam(request, course_id): # pylint: disable=
'message'
:
message
,
'message'
:
message
,
}
}
return
JsonResponse
(
response_payload
)
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(
...
@@ -132,4 +132,8 @@ urlpatterns = patterns(
url
(
r'^enable_certificate_generation$'
,
url
(
r'^enable_certificate_generation$'
,
'instructor.views.api.enable_certificate_generation'
,
'instructor.views.api.enable_certificate_generation'
,
name
=
'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):
...
@@ -259,7 +259,15 @@ def _section_certificates(course):
'enable_certificate_generation'
:
reverse
(
'enable_certificate_generation'
:
reverse
(
'enable_certificate_generation'
,
'enable_certificate_generation'
,
kwargs
=
{
'course_id'
:
course
.
id
}
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 (
...
@@ -24,7 +24,8 @@ from instructor_task.tasks import (
cohort_students
,
cohort_students
,
enrollment_report_features_csv
,
enrollment_report_features_csv
,
calculate_may_enroll_csv
,
calculate_may_enroll_csv
,
exec_summary_report_csv
exec_summary_report_csv
,
generate_certificates
,
)
)
from
instructor_task.api_helper
import
(
from
instructor_task.api_helper
import
(
...
@@ -419,3 +420,17 @@ def submit_cohort_students(request, course_key, file_name):
...
@@ -419,3 +420,17 @@ def submit_cohort_students(request, course_key, file_name):
task_key
=
""
task_key
=
""
return
submit_task
(
request
,
task_type
,
task_class
,
course_key
,
task_input
,
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 (
...
@@ -40,7 +40,8 @@ from instructor_task.tasks_helper import (
cohort_students_and_upload
,
cohort_students_and_upload
,
upload_enrollment_report
,
upload_enrollment_report
,
upload_may_enroll_csv
,
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):
...
@@ -225,6 +226,22 @@ def calculate_may_enroll_csv(entry_id, xmodule_instance_args):
return
run_main_task
(
entry_id
,
task_fn
,
action_name
)
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
@task
(
base
=
BaseInstructorTask
)
# pylint: disable=E1102
def
cohort_students
(
entry_id
,
xmodule_instance_args
):
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
...
@@ -18,6 +18,7 @@ from celery.states import SUCCESS, FAILURE
from
django.contrib.auth.models
import
User
from
django.contrib.auth.models
import
User
from
django.core.files.storage
import
DefaultStorage
from
django.core.files.storage
import
DefaultStorage
from
django.db
import
transaction
,
reset_queries
from
django.db
import
transaction
,
reset_queries
from
django.db.models
import
Q
import
dogstats_wrapper
as
dog_stats_api
import
dogstats_wrapper
as
dog_stats_api
from
pytz
import
UTC
from
pytz
import
UTC
from
StringIO
import
StringIO
from
StringIO
import
StringIO
...
@@ -33,7 +34,12 @@ from util.file import course_filename_prefix_generator, UniversalNewlineIterator
...
@@ -33,7 +34,12 @@ 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
django.utils.translation
import
ugettext
as
_
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.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
...
@@ -50,7 +56,7 @@ from opaque_keys.edx.keys import UsageKey
...
@@ -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
openedx.core.djangoapps.course_groups.cohorts
import
add_user_to_cohort
,
is_course_cohorted
from
student.models
import
CourseEnrollment
,
CourseAccessRole
from
student.models
import
CourseEnrollment
,
CourseAccessRole
from
verify_student.models
import
SoftwareSecurePhotoVerification
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
# define different loggers for use within tasks and on client side
TASK_LOG
=
logging
.
getLogger
(
'edx.celery.task'
)
TASK_LOG
=
logging
.
getLogger
(
'edx.celery.task'
)
...
@@ -1247,6 +1253,44 @@ def upload_exec_summary_report(_xmodule_instance_args, _entry_id, course_id, _ta
...
@@ -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
)
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
):
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
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
...
@@ -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
)
upload_csv_to_report_store
(
output_rows
,
'cohort_results'
,
course_id
,
start_date
)
return
task_progress
.
update_task_state
(
extra_meta
=
current_step
)
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 (
...
@@ -18,7 +18,8 @@ from instructor_task.api import (
submit_cohort_students
,
submit_cohort_students
,
submit_detailed_enrollment_features_csv
,
submit_detailed_enrollment_features_csv
,
submit_calculate_may_enroll_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
from
instructor_task.api_helper
import
AlreadyRunningError
...
@@ -236,3 +237,13 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa
...
@@ -236,3 +237,13 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa
file_name
=
u'filename.csv'
file_name
=
u'filename.csv'
)
)
self
.
_test_resubmission
(
api_call
)
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
...
@@ -22,7 +22,12 @@ from student.tests.factories import UserFactory, CourseEnrollmentFactory
from
instructor_task.models
import
InstructorTask
from
instructor_task.models
import
InstructorTask
from
instructor_task.tests.test_base
import
InstructorTaskModuleTestCase
from
instructor_task.tests.test_base
import
InstructorTaskModuleTestCase
from
instructor_task.tests.factories
import
InstructorTaskFactory
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
from
instructor_task.tasks_helper
import
UpdateProblemModuleStateError
PROBLEM_URL_NAME
=
"test_urlname"
PROBLEM_URL_NAME
=
"test_urlname"
...
@@ -97,15 +102,20 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
...
@@ -97,15 +102,20 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
with
self
.
assertRaises
(
ItemNotFoundError
):
with
self
.
assertRaises
(
ItemNotFoundError
):
self
.
_run_task_with_mock_celery
(
task_class
,
task_entry
.
id
,
task_entry
.
task_id
)
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."""
"""Run a task and check the number of StudentModules processed."""
task_entry
=
self
.
_create_input_entry
()
task_entry
=
self
.
_create_input_entry
()
status
=
self
.
_run_task_with_mock_celery
(
task_class
,
task_entry
.
id
,
task_entry
.
task_id
)
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
# 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
(
'succeeded'
),
expected_num_succeeded
)
self
.
assertEquals
(
status
.
get
(
'skipped'
),
expected_num_skipped
)
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
.
assertEquals
(
status
.
get
(
'action_name'
),
action_name
)
self
.
assertGreater
(
status
.
get
(
'duration_ms'
),
0
)
self
.
assertGreater
(
status
.
get
(
'duration_ms'
),
0
)
# compare with entry in table:
# compare with entry in table:
...
@@ -438,3 +448,26 @@ class TestDeleteStateInstructorTask(TestInstructorTasks):
...
@@ -438,3 +448,26 @@ class TestDeleteStateInstructorTask(TestInstructorTasks):
StudentModule
.
objects
.
get
(
course_id
=
self
.
course
.
id
,
StudentModule
.
objects
.
get
(
course_id
=
self
.
course
.
id
,
student
=
student
,
student
=
student
,
module_state_key
=
self
.
location
)
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
...
@@ -11,14 +11,13 @@ from mock import Mock, patch
import
tempfile
import
tempfile
import
unicodecsv
import
unicodecsv
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
django.test.utils
import
override_settings
from
capa.tests.response_xml_factory
import
MultipleChoiceResponseXMLFactory
from
capa.tests.response_xml_factory
import
MultipleChoiceResponseXMLFactory
from
certificates.models
import
CertificateStatuses
from
certificates.tests.factories
import
GeneratedCertificateFactory
,
CertificateWhitelistFactory
from
certificates.tests.factories
import
GeneratedCertificateFactory
,
CertificateWhitelistFactory
from
course_modes.models
import
CourseMode
from
course_modes.models
import
CourseMode
from
courseware.tests.factories
import
InstructorFactory
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
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
...
@@ -38,6 +37,9 @@ from instructor_task.tasks_helper import (
...
@@ -38,6 +37,9 @@ from instructor_task.tasks_helper import (
upload_problem_grade_report
,
upload_problem_grade_report
,
upload_students_csv
,
upload_students_csv
,
upload_may_enroll_csv
,
upload_may_enroll_csv
,
upload_enrollment_report
,
upload_exec_summary_report
,
generate_students_certificates
,
)
)
from
openedx.core.djangoapps.util.testing
import
ContentGroupTestCase
,
TestConditionalContent
from
openedx.core.djangoapps.util.testing
import
ContentGroupTestCase
,
TestConditionalContent
...
@@ -1300,3 +1302,54 @@ class TestGradeReportEnrollmentAndCertificateInfo(TestReportMixin, InstructorTas
...
@@ -1300,3 +1302,54 @@ class TestGradeReportEnrollmentAndCertificateInfo(TestReportMixin, InstructorTas
)
)
self
.
_verify_csv_data
(
user
.
username
,
expected_output
)
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) ->
...
@@ -179,6 +179,9 @@ setup_instructor_dashboard_sections = (idash_content) ->
,
,
constructor
:
window
.
InstructorDashboard
.
sections
.
CohortManagement
constructor
:
window
.
InstructorDashboard
.
sections
.
CohortManagement
$element
:
idash_content
.
find
".
#{
CSS_IDASH_SECTION
}
#cohort_management"
$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
})
->
sections_to_initialize
.
map
({
constructor
,
$element
})
->
...
...
lms/static/js/instructor_dashboard/certificates.js
View file @
1ca37737
var
edx
=
edx
||
{};
var
edx
=
edx
||
{};
(
function
(
$
,
gettext
)
{
(
function
(
$
,
gettext
,
_
)
{
'use strict'
;
'use strict'
;
edx
.
instructor_dashboard
=
edx
.
instructor_dashboard
||
{};
edx
.
instructor_dashboard
=
edx
.
instructor_dashboard
||
{};
...
@@ -33,5 +33,60 @@ var edx = edx || {};
...
@@ -33,5 +33,60 @@ var edx = edx || {};
$
(
'#refresh-example-certificate-status'
).
on
(
'click'
,
function
()
{
$
(
'#refresh-example-certificate-status'
).
on
(
'click'
,
function
()
{
window
.
location
.
reload
();
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.'
));
}
});
});
});
})(
$
,
gettext
);
});
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
:
{}
});
_
.
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 @@
...
@@ -55,4 +55,27 @@
<button
class=
"is-disabled"
disabled
>
${_('Enable Student-Generated Certificates')}
</button>
<button
class=
"is-disabled"
disabled
>
${_('Enable Student-Generated Certificates')}
</button>
% endif
% endif
</div>
</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>
</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