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
094ed321
Commit
094ed321
authored
Nov 04, 2015
by
Saleem Latif
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added ability to regenerate certificates from Instructor Dashboard
parent
222bdd98
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
567 additions
and
21 deletions
+567
-21
lms/djangoapps/certificates/models.py
+26
-0
lms/djangoapps/instructor/tests/test_certificates.py
+77
-1
lms/djangoapps/instructor/views/api.py
+38
-1
lms/djangoapps/instructor/views/api_urls.py
+4
-0
lms/djangoapps/instructor/views/instructor_dashboard.py
+6
-1
lms/djangoapps/instructor_task/api.py
+24
-0
lms/djangoapps/instructor_task/tasks_helper.py
+27
-10
lms/djangoapps/instructor_task/tests/test_api.py
+17
-0
lms/djangoapps/instructor_task/tests/test_tasks_helper.py
+146
-0
lms/static/js/instructor_dashboard/certificates.js
+48
-2
lms/static/js/spec/instructor_dashboard/certificates_spec.js
+116
-0
lms/static/js/spec/main.js
+5
-0
lms/static/sass/course/instructor/_instructor_2.scss
+14
-6
lms/templates/instructor/instructor_dashboard_2/certificates.html
+19
-0
No files found.
lms/djangoapps/certificates/models.py
View file @
094ed321
...
...
@@ -54,6 +54,7 @@ import os
from
django.contrib.auth.models
import
User
from
django.core.exceptions
import
ValidationError
from
django.db
import
models
,
transaction
from
django.db.models
import
Count
from
django.db.models.signals
import
post_save
from
django.dispatch
import
receiver
from
django.conf
import
settings
...
...
@@ -187,6 +188,31 @@ class GeneratedCertificate(models.Model):
return
None
@classmethod
def
get_unique_statuses
(
cls
,
course_key
=
None
,
flat
=
False
):
"""
1 - Return unique statuses as a list of dictionaries containing the following key value pairs
[
{'status': 'status value from db', 'count': 'occurrence count of the status'},
{...},
..., ]
2 - if flat is 'True' then return unique statuses as a list
3 - if course_key is given then return unique statuses associated with the given course
:param course_key: Course Key identifier
:param flat: boolean showing whether to return statuses as a list of values or a list of dictionaries.
"""
query
=
cls
.
objects
if
course_key
:
query
=
query
.
filter
(
course_id
=
course_key
)
if
flat
:
return
query
.
values_list
(
'status'
,
flat
=
True
)
.
distinct
()
else
:
return
query
.
values
(
'status'
)
.
annotate
(
count
=
Count
(
'status'
))
@receiver
(
post_save
,
sender
=
GeneratedCertificate
)
def
handle_post_cert_generated
(
sender
,
instance
,
**
kwargs
):
# pylint: disable=no-self-argument, unused-argument
...
...
lms/djangoapps/instructor/tests/test_certificates.py
View file @
094ed321
...
...
@@ -12,7 +12,8 @@ from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
config_models.models
import
cache
from
courseware.tests.factories
import
GlobalStaffFactory
,
InstructorFactory
,
UserFactory
from
certificates.models
import
CertificateGenerationConfiguration
from
certificates.tests.factories
import
GeneratedCertificateFactory
from
certificates.models
import
CertificateGenerationConfiguration
,
CertificateStatuses
from
certificates
import
api
as
certs_api
...
...
@@ -486,3 +487,78 @@ class CertificatesInstructorApiTest(SharedModuleStoreTestCase):
self
.
assertEqual
(
response
.
status_code
,
200
)
res_json
=
json
.
loads
(
response
.
content
)
self
.
assertTrue
(
res_json
[
'success'
])
def
test_certificate_regeneration_success
(
self
):
"""
Test certificate regeneration is successful when accessed with 'certificate_statuses'
present in GeneratedCertificate table.
"""
# Create a generated Certificate of some user with status 'downloadable'
GeneratedCertificateFactory
.
create
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
,
status
=
CertificateStatuses
.
downloadable
,
mode
=
'honor'
)
# Login the client and access the url with 'certificate_statuses'
self
.
client
.
login
(
username
=
self
.
global_staff
.
username
,
password
=
'test'
)
url
=
reverse
(
'start_certificate_regeneration'
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course
.
id
)})
response
=
self
.
client
.
post
(
url
,
data
=
{
'certificate_statuses'
:
[
CertificateStatuses
.
downloadable
]})
# Assert 200 status code in response
self
.
assertEqual
(
response
.
status_code
,
200
)
res_json
=
json
.
loads
(
response
.
content
)
# Assert request is successful
self
.
assertTrue
(
res_json
[
'success'
])
# Assert success message
self
.
assertEqual
(
res_json
[
'message'
],
u'Certificate regeneration task has been started. You can view the status of the generation task in '
u'the "Pending Tasks" section.'
)
def
test_certificate_regeneration_error
(
self
):
"""
Test certificate regeneration errors out when accessed with either empty list of 'certificate_statuses' or
the 'certificate_statuses' that are not present in GeneratedCertificate table.
"""
# Create a dummy course and GeneratedCertificate with the same status as the one we will use to access
# 'start_certificate_regeneration' but their error message should be displayed as GeneratedCertificate
# belongs to a different course
dummy_course
=
CourseFactory
.
create
()
GeneratedCertificateFactory
.
create
(
user
=
self
.
user
,
course_id
=
dummy_course
.
id
,
status
=
CertificateStatuses
.
generating
,
mode
=
'honor'
)
# Login the client and access the url without 'certificate_statuses'
self
.
client
.
login
(
username
=
self
.
global_staff
.
username
,
password
=
'test'
)
url
=
reverse
(
'start_certificate_regeneration'
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course
.
id
)})
response
=
self
.
client
.
post
(
url
)
# Assert 400 status code in response
self
.
assertEqual
(
response
.
status_code
,
400
)
res_json
=
json
.
loads
(
response
.
content
)
# Assert Error Message
self
.
assertEqual
(
res_json
[
'message'
],
u'Please select one or more certificate statuses that require certificate regeneration.'
)
# Access the url passing 'certificate_statuses' that are not present in db
url
=
reverse
(
'start_certificate_regeneration'
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course
.
id
)})
response
=
self
.
client
.
post
(
url
,
data
=
{
'certificate_statuses'
:
[
CertificateStatuses
.
generating
]})
# Assert 400 status code in response
self
.
assertEqual
(
response
.
status_code
,
400
)
res_json
=
json
.
loads
(
response
.
content
)
# Assert Error Message
self
.
assertEqual
(
res_json
[
'message'
],
u'Please select certificate statuses from the list only.'
)
lms/djangoapps/instructor/views/api.py
View file @
094ed321
...
...
@@ -92,7 +92,7 @@ from instructor.views import INVOICE_KEY
from
submissions
import
api
as
sub_api
# installed from the edx-submissions repository
from
certificates
import
api
as
certs_api
from
certificates.models
import
CertificateWhitelist
from
certificates.models
import
CertificateWhitelist
,
GeneratedCertificate
from
bulk_email.models
import
CourseEmail
from
student.models
import
get_user_by_username_or_email
...
...
@@ -2712,6 +2712,43 @@ def start_certificate_generation(request, course_id):
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
@require_global_staff
@require_POST
def
start_certificate_regeneration
(
request
,
course_id
):
"""
Start regenerating certificates for students whose certificate statuses lie with in 'certificate_statuses'
entry in POST data.
"""
course_key
=
CourseKey
.
from_string
(
course_id
)
certificates_statuses
=
request
.
POST
.
getlist
(
'certificate_statuses'
,
[])
if
not
certificates_statuses
:
return
JsonResponse
(
{
'message'
:
_
(
'Please select one or more certificate statuses that require certificate regeneration.'
)},
status
=
400
)
# Check if the selected statuses are allowed
allowed_statuses
=
GeneratedCertificate
.
get_unique_statuses
(
course_key
=
course_key
,
flat
=
True
)
if
not
set
(
certificates_statuses
)
.
issubset
(
allowed_statuses
):
return
JsonResponse
(
{
'message'
:
_
(
'Please select certificate statuses from the list only.'
)},
status
=
400
)
try
:
instructor_task
.
api
.
regenerate_certificates
(
request
,
course_key
,
certificates_statuses
)
except
AlreadyRunningError
as
error
:
return
JsonResponse
({
'message'
:
error
.
message
},
status
=
400
)
response_payload
=
{
'message'
:
_
(
'Certificate regeneration task has been started. '
'You can view the status of the generation task in the "Pending Tasks" section.'
),
'success'
:
True
}
return
JsonResponse
(
response_payload
)
@ensure_csrf_cookie
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
@require_global_staff
@require_POST
def
create_certificate_exception
(
request
,
course_id
,
white_list_student
=
None
):
"""
Add Students to certificate white list.
...
...
lms/djangoapps/instructor/views/api_urls.py
View file @
094ed321
...
...
@@ -143,6 +143,10 @@ urlpatterns = patterns(
'instructor.views.api.start_certificate_generation'
,
name
=
'start_certificate_generation'
),
url
(
r'^start_certificate_regeneration'
,
'instructor.views.api.start_certificate_regeneration'
,
name
=
'start_certificate_regeneration'
),
url
(
r'^create_certificate_exception/(?P<white_list_student>[^/]*)'
,
'instructor.views.api.create_certificate_exception'
,
name
=
'create_certificate_exception'
),
...
...
lms/djangoapps/instructor/views/instructor_dashboard.py
View file @
094ed321
...
...
@@ -37,7 +37,7 @@ from student.models import CourseEnrollment
from
shoppingcart.models
import
Coupon
,
PaidCourseRegistration
,
CourseRegCodeItem
from
course_modes.models
import
CourseMode
,
CourseModesArchive
from
student.roles
import
CourseFinanceAdminRole
,
CourseSalesAdminRole
from
certificates.models
import
CertificateGenerationConfiguration
,
CertificateWhitelist
from
certificates.models
import
CertificateGenerationConfiguration
,
CertificateWhitelist
,
GeneratedCertificate
from
certificates
import
api
as
certs_api
from
util.date_utils
import
get_default_time_display
...
...
@@ -299,6 +299,7 @@ def _section_certificates(course):
'enabled_for_course'
:
certs_api
.
cert_generation_enabled
(
course
.
id
),
'instructor_generation_enabled'
:
instructor_generation_enabled
,
'html_cert_enabled'
:
html_cert_enabled
,
'certificate_statuses'
:
GeneratedCertificate
.
get_unique_statuses
(
course_key
=
course
.
id
),
'urls'
:
{
'generate_example_certificates'
:
reverse
(
'generate_example_certificates'
,
...
...
@@ -312,6 +313,10 @@ def _section_certificates(course):
'start_certificate_generation'
,
kwargs
=
{
'course_id'
:
course
.
id
}
),
'start_certificate_regeneration'
:
reverse
(
'start_certificate_regeneration'
,
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 @
094ed321
...
...
@@ -512,3 +512,27 @@ def generate_certificates_for_students(request, course_key, students=None): # p
task_key
=
""
return
submit_task
(
request
,
task_type
,
task_class
,
course_key
,
task_input
,
task_key
)
def
regenerate_certificates
(
request
,
course_key
,
statuses_to_regenerate
,
students
=
None
):
"""
Submits a task to regenerate certificates for given students enrolled in the course or
all students if argument 'students' is None.
Regenerate Certificate only if the status of the existing generated certificate is in 'statuses_to_regenerate'
list passed in the arguments.
Raises AlreadyRunningError if certificates are currently being generated.
"""
if
students
:
task_type
=
'regenerate_certificates_certain_student'
students
=
[
student
.
id
for
student
in
students
]
task_input
=
{
'students'
:
students
}
else
:
task_type
=
'regenerate_certificates_all_student'
task_input
=
{}
task_input
.
update
({
"statuses_to_regenerate"
:
statuses_to_regenerate
})
task_class
=
generate_certificates
task_key
=
""
return
submit_task
(
request
,
task_type
,
task_class
,
course_key
,
task_input
,
task_key
)
lms/djangoapps/instructor_task/tasks_helper.py
View file @
094ed321
...
...
@@ -1414,7 +1414,8 @@ def generate_students_certificates(
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
)
statuses_to_regenerate
=
task_input
.
get
(
'statuses_to_regenerate'
,
[])
students_require_certs
=
students_require_certificate
(
course_id
,
enrolled_students
,
statuses_to_regenerate
)
task_progress
.
skipped
=
task_progress
.
total
-
len
(
students_require_certs
)
...
...
@@ -1523,15 +1524,31 @@ def cohort_students_and_upload(_xmodule_instance_args, _entry_id, course_id, tas
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.
def
students_require_certificate
(
course_id
,
enrolled_students
,
statuses_to_regenerate
=
None
):
"""
Returns list of students where certificates needs to be generated.
if 'statuses_to_regenerate' is given then return students that have Generated Certificates
and the generated certificate status lies in 'statuses_to_regenerate'
if 'statuses_to_regenerate' is not given then return all the enrolled student skipping the ones
whose certificates have already been generated.
:param course_id:
:param enrolled_students:
:param statuses_to_regenerate:
"""
# compute those students where certificates already generated
students_already_have_certs
=
User
.
objects
.
filter
(
~
Q
(
generatedcertificate__status
=
CertificateStatuses
.
unavailable
),
generatedcertificate__course_id
=
course_id
)
return
list
(
set
(
enrolled_students
)
-
set
(
students_already_have_certs
))
if
statuses_to_regenerate
:
# Return Students that have Generated Certificates and the generated certificate status
# lies in 'statuses_to_regenerate'
return
User
.
objects
.
filter
(
generatedcertificate__course_id
=
course_id
,
generatedcertificate__status__in
=
statuses_to_regenerate
)
else
:
# compute those students whose certificates are already generated
students_already_have_certs
=
User
.
objects
.
filter
(
~
Q
(
generatedcertificate__status
=
CertificateStatuses
.
unavailable
),
generatedcertificate__course_id
=
course_id
)
# Return all the enrolled student skipping the ones whose certificates have already been generated
return
list
(
set
(
enrolled_students
)
-
set
(
students_already_have_certs
))
lms/djangoapps/instructor_task/tests/test_api.py
View file @
094ed321
...
...
@@ -22,6 +22,7 @@ from instructor_task.api import (
submit_executive_summary_report
,
submit_course_survey_report
,
generate_certificates_for_all_students
,
regenerate_certificates
)
from
instructor_task.api_helper
import
AlreadyRunningError
...
...
@@ -31,6 +32,7 @@ from instructor_task.tests.test_base import (InstructorTaskTestCase,
InstructorTaskModuleTestCase
,
TestReportMixin
,
TEST_COURSE_KEY
)
from
certificates.models
import
CertificateStatuses
class
InstructorTaskReportTest
(
InstructorTaskTestCase
):
...
...
@@ -263,3 +265,18 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa
self
.
course
.
id
)
self
.
_test_resubmission
(
api_call
)
def
test_regenerate_certificates
(
self
):
"""
Tests certificates regeneration task submission api
"""
def
api_call
():
"""
wrapper method for regenerate_certificates
"""
return
regenerate_certificates
(
self
.
create_task_request
(
self
.
instructor
),
self
.
course
.
id
,
[
CertificateStatuses
.
downloadable
,
CertificateStatuses
.
generating
]
)
self
.
_test_resubmission
(
api_call
)
lms/djangoapps/instructor_task/tests/test_tasks_helper.py
View file @
094ed321
...
...
@@ -1635,3 +1635,149 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
},
result
)
def
test_certificate_regeneration_for_students
(
self
):
"""
Verify that certificates are regenerated for all eligible students enrolled in a course whose generated
certificate statuses lies in the list 'statuses_to_regenerate' given in task_input.
"""
# 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'
)
# mark 3 students to have certificates generated with status 'error'
for
student
in
students
[
2
:
5
]:
GeneratedCertificateFactory
.
create
(
user
=
student
,
course_id
=
self
.
course
.
id
,
status
=
CertificateStatuses
.
error
,
mode
=
'honor'
)
# mark 6th students to have certificates generated with status 'deleted'
for
student
in
students
[
5
:
6
]:
GeneratedCertificateFactory
.
create
(
user
=
student
,
course_id
=
self
.
course
.
id
,
status
=
CertificateStatuses
.
deleted
,
mode
=
'honor'
)
# white-list 7 students
for
student
in
students
[:
7
]:
CertificateWhitelistFactory
.
create
(
user
=
student
,
course_id
=
self
.
course
.
id
,
whitelist
=
True
)
current_task
=
Mock
()
current_task
.
update_state
=
Mock
()
# Certificates should be regenerated for students having generated certificates with status
# 'downloadable' or 'error' which are total of 5 students in this test case
task_input
=
{
'statuses_to_regenerate'
:
[
CertificateStatuses
.
downloadable
,
CertificateStatuses
.
error
]}
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
,
task_input
,
'certificates generated'
)
self
.
assertDictContainsSubset
(
{
'action_name'
:
'certificates generated'
,
'total'
:
10
,
'attempted'
:
5
,
'succeeded'
:
5
,
'failed'
:
0
,
'skipped'
:
5
},
result
)
def
test_certificate_regeneration_with_expected_failures
(
self
):
"""
Verify that certificates are regenerated for all eligible students enrolled in a course whose generated
certificate statuses lies in the list 'statuses_to_regenerate' given in task_input.
"""
# 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'
)
# mark 3 students to have certificates generated with status 'error'
for
student
in
students
[
2
:
5
]:
GeneratedCertificateFactory
.
create
(
user
=
student
,
course_id
=
self
.
course
.
id
,
status
=
CertificateStatuses
.
error
,
mode
=
'honor'
)
# mark 6th students to have certificates generated with status 'deleted'
for
student
in
students
[
5
:
6
]:
GeneratedCertificateFactory
.
create
(
user
=
student
,
course_id
=
self
.
course
.
id
,
status
=
CertificateStatuses
.
deleted
,
mode
=
'honor'
)
# mark rest of the 4 students with having generated certificates with status 'generating'
# These students are not added in white-list and they have not completed grades so certificate generation
# for these students should fail other than the one student that has been added to white-list
# so from these students 3 failures and 1 success
for
student
in
students
[
6
:]:
GeneratedCertificateFactory
.
create
(
user
=
student
,
course_id
=
self
.
course
.
id
,
status
=
CertificateStatuses
.
generating
,
mode
=
'honor'
)
# white-list 7 students
for
student
in
students
[:
7
]:
CertificateWhitelistFactory
.
create
(
user
=
student
,
course_id
=
self
.
course
.
id
,
whitelist
=
True
)
current_task
=
Mock
()
current_task
.
update_state
=
Mock
()
# Regenerated certificates for students having generated certificates with status
# 'deleted' or 'generating'
task_input
=
{
'statuses_to_regenerate'
:
[
CertificateStatuses
.
deleted
,
CertificateStatuses
.
generating
]}
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
,
task_input
,
'certificates generated'
)
self
.
assertDictContainsSubset
(
{
'action_name'
:
'certificates generated'
,
'total'
:
10
,
'attempted'
:
5
,
'succeeded'
:
2
,
'failed'
:
3
,
'skipped'
:
5
},
result
)
lms/static/js/instructor_dashboard/certificates.js
View file @
094ed321
var
edx
=
edx
||
{};
var
onCertificatesReady
=
null
;
(
function
(
$
,
gettext
,
_
)
{
'use strict'
;
...
...
@@ -6,7 +7,7 @@ var edx = edx || {};
edx
.
instructor_dashboard
=
edx
.
instructor_dashboard
||
{};
edx
.
instructor_dashboard
.
certificates
=
{};
$
(
function
()
{
onCertificatesReady
=
function
()
{
/**
* Show a confirmation message before letting staff members
* enable/disable self-generated certificates for a course.
...
...
@@ -59,7 +60,52 @@ var edx = edx || {};
}
});
});
});
/**
* Start regenerating certificates for students.
*/
$section
.
on
(
'click'
,
'#btn-start-regenerating-certificates'
,
function
(
event
)
{
if
(
!
confirm
(
gettext
(
'Start regenerating certificates for students in this course?'
)
)
)
{
event
.
preventDefault
();
return
;
}
var
$btn_regenerating_certs
=
$
(
this
),
$certificate_regeneration_status
=
$
(
'.certificate-regeneration-status'
),
url
=
$btn_regenerating_certs
.
data
(
'endpoint'
);
$
.
ajax
({
type
:
"POST"
,
data
:
$
(
"#certificate-regenerating-form"
).
serializeArray
(),
url
:
url
,
success
:
function
(
data
)
{
$btn_regenerating_certs
.
attr
(
'disabled'
,
'disabled'
);
if
(
data
.
success
){
$certificate_regeneration_status
.
text
(
data
.
message
).
removeClass
(
'msg-error'
).
addClass
(
'msg-success'
);
}
else
{
$certificate_regeneration_status
.
text
(
data
.
message
).
removeClass
(
'msg-success'
).
addClass
(
"msg-error"
);
}
},
error
:
function
(
jqXHR
)
{
try
{
var
response
=
JSON
.
parse
(
jqXHR
.
responseText
);
$certificate_regeneration_status
.
text
(
gettext
(
response
.
message
)).
removeClass
(
'msg-success'
).
addClass
(
"msg-error"
);
}
catch
(
error
){
$certificate_regeneration_status
.
text
(
gettext
(
'Error while regenerating certificates. Please try again.'
)).
removeClass
(
'msg-success'
).
addClass
(
"msg-error"
);
}
}
});
});
};
// Call onCertificatesReady on document.ready event
$
(
onCertificatesReady
);
var
Certificates
=
(
function
()
{
function
Certificates
(
$section
)
{
...
...
lms/static/js/spec/instructor_dashboard/certificates_spec.js
0 → 100644
View file @
094ed321
/*global define, onCertificatesReady */
define
([
'jquery'
,
'common/js/spec_helpers/ajax_helpers'
,
'js/instructor_dashboard/certificates'
],
function
(
$
,
AjaxHelpers
)
{
'use strict'
;
describe
(
"edx.instructor_dashboard.certificates.regenerate_certificates"
,
function
()
{
var
$regenerate_certificates_button
=
null
,
$certificate_regeneration_status
=
null
,
requests
=
null
;
var
MESSAGES
=
{
success_message
:
'Certificate regeneration task has been started. '
+
'You can view the status of the generation task in the "Pending Tasks" section.'
,
error_message
:
'Please select one or more certificate statuses that require certificate regeneration.'
,
server_error_message
:
"Error while regenerating certificates. Please try again."
};
var
expected
=
{
error_class
:
'msg-error'
,
success_class
:
'msg-success'
,
url
:
'test/url/'
,
postData
:
[],
selected_statuses
:
[
'downloadable'
,
'error'
],
body
:
'certificate_statuses=downloadable&certificate_statuses=error'
};
var
select_options
=
function
(
option_values
){
$
.
each
(
option_values
,
function
(
index
,
element
){
$
(
"#certificate-statuses option[value="
+
element
+
"]"
).
attr
(
'selected'
,
'selected'
);
});
};
beforeEach
(
function
()
{
var
fixture
=
'<section id = "certificates"><h2>Regenerate Certificates</h2>'
+
'<form id="certificate-regenerating-form" method="post" action="'
+
expected
.
url
+
'">'
+
' <p id="status-multi-select-tip">Select one or more certificate statuses '
+
' below using your mouse and ctrl or command key.</p>'
+
' <select class="multi-select" multiple id="certificate-statuses" '
+
' name="certificate_statuses" aria-describedby="status-multi-select-tip">'
+
' <option value="downloadable">Downloadable (2)</option>'
+
' <option value="error">Error (2)</option>'
+
' <option value="generating">Generating (1)</option>'
+
' </select>'
+
' <label for="certificate-statuses">'
+
' Select certificate statuses that need regeneration and click Regenerate '
+
' Certificates button.'
+
' </label>'
+
' <input type="button" id="btn-start-regenerating-certificates" value="Regenerate Certificates"'
+
' data-endpoint="'
+
expected
.
url
+
'"/>'
+
'</form>'
+
'<div class="message certificate-regeneration-status"></div></section>'
;
setFixtures
(
fixture
);
onCertificatesReady
();
$regenerate_certificates_button
=
$
(
"#btn-start-regenerating-certificates"
);
$certificate_regeneration_status
=
$
(
".certificate-regeneration-status"
);
requests
=
AjaxHelpers
.
requests
(
this
);
});
it
(
"does not regenerate certificates if user cancels operation in confirm popup"
,
function
()
{
spyOn
(
window
,
'confirm'
).
andReturn
(
false
);
$regenerate_certificates_button
.
click
();
expect
(
window
.
confirm
).
toHaveBeenCalled
();
AjaxHelpers
.
expectNoRequests
(
requests
);
});
it
(
"sends regenerate certificates request if user accepts operation in confirm popup"
,
function
()
{
spyOn
(
window
,
'confirm'
).
andReturn
(
true
);
$regenerate_certificates_button
.
click
();
expect
(
window
.
confirm
).
toHaveBeenCalled
();
AjaxHelpers
.
expectRequest
(
requests
,
'POST'
,
expected
.
url
);
});
it
(
"sends regenerate certificates request with selected certificate statuses"
,
function
()
{
spyOn
(
window
,
'confirm'
).
andReturn
(
true
);
select_options
(
expected
.
selected_statuses
);
$regenerate_certificates_button
.
click
();
AjaxHelpers
.
expectRequest
(
requests
,
'POST'
,
expected
.
url
,
expected
.
body
);
});
it
(
"displays error message in case of server side error"
,
function
()
{
spyOn
(
window
,
'confirm'
).
andReturn
(
true
);
select_options
(
expected
.
selected_statuses
);
$regenerate_certificates_button
.
click
();
AjaxHelpers
.
respondWithError
(
requests
,
500
,
{
message
:
MESSAGES
.
server_error_message
});
expect
(
$certificate_regeneration_status
).
toHaveClass
(
expected
.
error_class
);
expect
(
$certificate_regeneration_status
.
text
()).
toEqual
(
MESSAGES
.
server_error_message
);
});
it
(
"displays error message returned by the server in case of unsuccessful request"
,
function
()
{
spyOn
(
window
,
'confirm'
).
andReturn
(
true
);
select_options
(
expected
.
selected_statuses
);
$regenerate_certificates_button
.
click
();
AjaxHelpers
.
respondWithError
(
requests
,
400
,
{
message
:
MESSAGES
.
error_message
});
expect
(
$certificate_regeneration_status
).
toHaveClass
(
expected
.
error_class
);
expect
(
$certificate_regeneration_status
.
text
()).
toEqual
(
MESSAGES
.
error_message
);
});
it
(
"displays success message returned by the server in case of successful request"
,
function
()
{
spyOn
(
window
,
'confirm'
).
andReturn
(
true
);
select_options
(
expected
.
selected_statuses
);
$regenerate_certificates_button
.
click
();
AjaxHelpers
.
respondWithJson
(
requests
,
{
message
:
MESSAGES
.
success_message
,
success
:
true
});
expect
(
$certificate_regeneration_status
).
toHaveClass
(
expected
.
success_class
);
expect
(
$certificate_regeneration_status
.
text
()).
toEqual
(
MESSAGES
.
success_message
);
});
});
}
);
lms/static/js/spec/main.js
View file @
094ed321
...
...
@@ -293,6 +293,10 @@
exports
:
'coffee/src/instructor_dashboard/student_admin'
,
deps
:
[
'jquery'
,
'underscore'
,
'coffee/src/instructor_dashboard/util'
,
'string_utils'
]
},
'js/instructor_dashboard/certificates'
:
{
exports
:
'js/instructor_dashboard/certificates'
,
deps
:
[
'jquery'
,
'gettext'
,
'underscore'
]
},
// LMS class loaded explicitly until they are converted to use RequireJS
'js/student_account/account'
:
{
exports
:
'js/student_account/account'
,
...
...
@@ -644,6 +648,7 @@
'lms/include/js/spec/instructor_dashboard/ecommerce_spec.js'
,
'lms/include/js/spec/instructor_dashboard/student_admin_spec.js'
,
'lms/include/js/spec/instructor_dashboard/certificates_exception_spec.js'
,
'lms/include/js/spec/instructor_dashboard/certificates_spec.js'
,
'lms/include/js/spec/student_account/account_spec.js'
,
'lms/include/js/spec/student_account/access_spec.js'
,
'lms/include/js/spec/student_account/logistration_factory_spec.js'
,
...
...
lms/static/sass/course/instructor/_instructor_2.scss
View file @
094ed321
...
...
@@ -92,6 +92,20 @@
}
}
.msg-success
{
border-top
:
2px
solid
$confirm-color
;
background
:
tint
(
$confirm-color
,
95%
);
color
:
$confirm-color
;
}
.multi-select
{
min-width
:
150px
;
option
{
padding
:
(
$baseline
/
5
)
$baseline
(
$baseline
/
10
)
(
$baseline
/
4
);
}
}
// inline copy
.copy-confirm
{
color
:
$confirm-color
;
...
...
@@ -2125,12 +2139,6 @@ input[name="subject"] {
}
#certificate-white-list-editor
{
.msg-success
{
border-top
:
2px
solid
$confirm-color
;
background
:
tint
(
$confirm-color
,
95%
);
color
:
$confirm-color
;
}
.certificate-exception-inputs
{
.student-username-or-email
{
width
:
300px
;
...
...
lms/templates/instructor/instructor_dashboard_2/certificates.html
View file @
094ed321
...
...
@@ -92,6 +92,25 @@ import json
%endif
% endif
<hr>
<p
class=
"start-certificate-regeneration"
>
<h2>
${_("Regenerate Certificates")}
</h2>
<form
id=
"certificate-regenerating-form"
method=
"post"
action=
"${section_data['urls']['start_certificate_regeneration']}"
>
<p
id=
'status-multi-select-tip'
>
${_('Select one or more certificate statuses below using your mouse and ctrl or command key.')}
</p>
<select
class=
"multi-select"
multiple
id=
"certificate-statuses"
name=
"certificate_statuses"
aria-describedby=
"status-multi-select-tip"
>
%for status in section_data['certificate_statuses']:
<option
value=
"${status['status']}"
>
${status['status'].title() + " ({})".format(status['count'])}
</option>
%endfor
</select>
<label
for=
"certificate-statuses"
>
${_("Select certificate statuses that need regeneration and click Regenerate Certificates button.")}
</label>
<input
type=
"button"
id=
"btn-start-regenerating-certificates"
value=
"${_('Regenerate Certificates')}"
data-endpoint=
"${section_data['urls']['start_certificate_regeneration']}"
/>
</form>
<div
class=
"message certificate-regeneration-status"
></div>
</div>
<div
class=
"certificate_exception-container"
>
<hr>
<h2>
${_("Certificate Exceptions")}
</h2>
...
...
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