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
3604e2dd
Commit
3604e2dd
authored
Mar 06, 2015
by
Will Daly
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #7195 from edx/will/certs-instructor-dash
ECOM-1140: Instructor dashboard example certificates
parents
9e8a0b86
940f8922
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
490 additions
and
16 deletions
+490
-16
lms/djangoapps/courseware/tests/test_views.py
+12
-2
lms/djangoapps/courseware/views.py
+6
-7
lms/djangoapps/instructor/tests/test_certificates.py
+221
-0
lms/djangoapps/instructor/views/api.py
+76
-5
lms/djangoapps/instructor/views/api_urls.py
+9
-0
lms/djangoapps/instructor/views/instructor_dashboard.py
+56
-0
lms/envs/common.py
+4
-1
lms/static/js/instructor_dashboard/certificates.js
+37
-0
lms/static/sass/course/instructor/_instructor_2.scss
+11
-0
lms/templates/instructor/instructor_dashboard_2/certificates.html
+58
-0
lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html
+0
-1
No files found.
lms/djangoapps/courseware/tests/test_views.py
View file @
3604e2dd
...
@@ -15,6 +15,7 @@ from django.http import Http404, HttpResponseBadRequest
...
@@ -15,6 +15,7 @@ from django.http import Http404, HttpResponseBadRequest
from
django.test
import
TestCase
from
django.test
import
TestCase
from
django.test.client
import
RequestFactory
from
django.test.client
import
RequestFactory
from
django.test.utils
import
override_settings
from
django.test.utils
import
override_settings
from
certificates
import
api
as
certs_api
from
certificates.models
import
CertificateStatuses
,
CertificateGenerationConfiguration
from
certificates.models
import
CertificateStatuses
,
CertificateGenerationConfiguration
from
certificates.tests.factories
import
GeneratedCertificateFactory
from
certificates.tests.factories
import
GeneratedCertificateFactory
from
edxmako.middleware
import
MakoMiddleware
from
edxmako.middleware
import
MakoMiddleware
...
@@ -677,10 +678,19 @@ class ProgressPageTests(ModuleStoreTestCase):
...
@@ -677,10 +678,19 @@ class ProgressPageTests(ModuleStoreTestCase):
resp
=
views
.
progress
(
self
.
request
,
course_id
=
self
.
course
.
id
.
to_deprecated_string
())
resp
=
views
.
progress
(
self
.
request
,
course_id
=
self
.
course
.
id
.
to_deprecated_string
())
self
.
assertEqual
(
resp
.
status_code
,
200
)
self
.
assertEqual
(
resp
.
status_code
,
200
)
def
test_resp_with_generate_cert_config_enabled
(
self
):
def
test_generate_cert_config
(
self
):
resp
=
views
.
progress
(
self
.
request
,
course_id
=
unicode
(
self
.
course
.
id
))
self
.
assertNotContains
(
resp
,
'Create Your Certificate'
)
# Enable the feature, but do not enable it for this course
CertificateGenerationConfiguration
(
enabled
=
True
)
.
save
()
CertificateGenerationConfiguration
(
enabled
=
True
)
.
save
()
resp
=
views
.
progress
(
self
.
request
,
course_id
=
unicode
(
self
.
course
.
id
))
resp
=
views
.
progress
(
self
.
request
,
course_id
=
unicode
(
self
.
course
.
id
))
self
.
assertEqual
(
resp
.
status_code
,
200
)
self
.
assertNotContains
(
resp
,
'Create Your Certificate'
)
# Enable certificate generation for this course
certs_api
.
set_cert_generation_enabled
(
self
.
course
.
id
,
True
)
resp
=
views
.
progress
(
self
.
request
,
course_id
=
unicode
(
self
.
course
.
id
))
self
.
assertContains
(
resp
,
'Create Your Certificate'
)
class
VerifyCourseKeyDecoratorTests
(
TestCase
):
class
VerifyCourseKeyDecoratorTests
(
TestCase
):
...
...
lms/djangoapps/courseware/views.py
View file @
3604e2dd
...
@@ -23,8 +23,7 @@ from django.utils.timezone import UTC
...
@@ -23,8 +23,7 @@ from django.utils.timezone import UTC
from
django.views.decorators.http
import
require_GET
,
require_POST
from
django.views.decorators.http
import
require_GET
,
require_POST
from
django.http
import
Http404
,
HttpResponse
,
HttpResponseBadRequest
from
django.http
import
Http404
,
HttpResponse
,
HttpResponseBadRequest
from
django.shortcuts
import
redirect
from
django.shortcuts
import
redirect
from
certificates.api
import
certificate_downloadable_status
,
generate_user_certificates
from
certificates
import
api
as
certs_api
from
certificates.models
import
CertificateGenerationConfiguration
from
edxmako.shortcuts
import
render_to_response
,
render_to_string
,
marketing_link
from
edxmako.shortcuts
import
render_to_response
,
render_to_string
,
marketing_link
from
django_future.csrf
import
ensure_csrf_cookie
from
django_future.csrf
import
ensure_csrf_cookie
from
django.views.decorators.cache
import
cache_control
from
django.views.decorators.cache
import
cache_control
...
@@ -1013,7 +1012,7 @@ def _progress(request, course_key, student_id):
...
@@ -1013,7 +1012,7 @@ def _progress(request, course_key, student_id):
raise
Http404
raise
Http404
# checking certificate generation configuration
# checking certificate generation configuration
show_generate_cert_btn
=
CertificateGenerationConfiguration
.
current
()
.
enabled
show_generate_cert_btn
=
certs_api
.
cert_generation_enabled
(
course_key
)
context
=
{
context
=
{
'course'
:
course
,
'course'
:
course
,
...
@@ -1023,12 +1022,12 @@ def _progress(request, course_key, student_id):
...
@@ -1023,12 +1022,12 @@ def _progress(request, course_key, student_id):
'staff_access'
:
staff_access
,
'staff_access'
:
staff_access
,
'student'
:
student
,
'student'
:
student
,
'reverifications'
:
fetch_reverify_banner_info
(
request
,
course_key
),
'reverifications'
:
fetch_reverify_banner_info
(
request
,
course_key
),
'passed'
:
is_course_passed
(
course
,
grade_summary
)
if
show_generate_cert_btn
else
False
,
'passed'
:
is_course_passed
(
course
,
grade_summary
),
'show_generate_cert_btn'
:
show_generate_cert_btn
'show_generate_cert_btn'
:
show_generate_cert_btn
}
}
if
show_generate_cert_btn
:
if
show_generate_cert_btn
:
context
.
update
(
certificate_downloadable_status
(
student
,
course_key
))
context
.
update
(
cert
s_api
.
cert
ificate_downloadable_status
(
student
,
course_key
))
with
grades
.
manual_transaction
():
with
grades
.
manual_transaction
():
response
=
render_to_response
(
'courseware/progress.html'
,
context
)
response
=
render_to_response
(
'courseware/progress.html'
,
context
)
...
@@ -1301,10 +1300,10 @@ def generate_user_cert(request, course_id):
...
@@ -1301,10 +1300,10 @@ def generate_user_cert(request, course_id):
if
not
is_course_passed
(
course
,
None
,
student
,
request
):
if
not
is_course_passed
(
course
,
None
,
student
,
request
):
return
HttpResponseBadRequest
(
_
(
"Your certificate will be available when you pass the course."
))
return
HttpResponseBadRequest
(
_
(
"Your certificate will be available when you pass the course."
))
certificate_status
=
certificate_downloadable_status
(
student
,
course
.
id
)
certificate_status
=
cert
s_api
.
cert
ificate_downloadable_status
(
student
,
course
.
id
)
if
not
certificate_status
[
"is_downloadable"
]
and
not
certificate_status
[
"is_generating"
]:
if
not
certificate_status
[
"is_downloadable"
]
and
not
certificate_status
[
"is_generating"
]:
generate_user_certificates
(
student
,
course
.
id
,
course
=
course
)
certs_api
.
generate_user_certificates
(
student
,
course
.
id
)
_track_successful_certificate_generation
(
student
.
id
,
course
.
id
)
_track_successful_certificate_generation
(
student
.
id
,
course
.
id
)
return
HttpResponse
(
_
(
"Creating certificate"
))
return
HttpResponse
(
_
(
"Creating certificate"
))
...
...
lms/djangoapps/instructor/tests/test_certificates.py
0 → 100644
View file @
3604e2dd
"""Tests for the certificates panel of the instructor dash. """
import
contextlib
import
ddt
import
mock
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
certificates.models
import
CertificateGenerationConfiguration
from
certificates
import
api
as
certs_api
@ddt.ddt
class
CertificatesInstructorDashTest
(
ModuleStoreTestCase
):
"""Tests for the certificate panel of the instructor dash. """
ERROR_REASON
=
"An error occurred!"
DOWNLOAD_URL
=
"http://www.example.com/abcd123/cert.pdf"
def
setUp
(
self
):
super
(
CertificatesInstructorDashTest
,
self
)
.
setUp
()
self
.
course
=
CourseFactory
.
create
()
self
.
url
=
reverse
(
'instructor_dashboard'
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course
.
id
)}
)
self
.
global_staff
=
GlobalStaffFactory
()
self
.
instructor
=
InstructorFactory
(
course_key
=
self
.
course
.
id
)
# Need to clear the cache for model-based configuration
cache
.
clear
()
# Enable the certificate generation feature
CertificateGenerationConfiguration
.
objects
.
create
(
enabled
=
True
)
def
test_visible_only_to_global_staff
(
self
):
# Instructors don't see the certificates section
self
.
client
.
login
(
username
=
self
.
instructor
.
username
,
password
=
"test"
)
self
.
_assert_certificates_visible
(
False
)
# Global staff can see the certificates section
self
.
client
.
login
(
username
=
self
.
global_staff
.
username
,
password
=
"test"
)
self
.
_assert_certificates_visible
(
True
)
def
test_visible_only_when_feature_flag_enabled
(
self
):
# Disable the feature flag
CertificateGenerationConfiguration
.
objects
.
create
(
enabled
=
False
)
cache
.
clear
()
# Now even global staff can't see the certificates section
self
.
client
.
login
(
username
=
self
.
global_staff
.
username
,
password
=
"test"
)
self
.
_assert_certificates_visible
(
False
)
@ddt.data
(
"started"
,
"error"
,
"success"
)
def
test_show_certificate_status
(
self
,
status
):
self
.
client
.
login
(
username
=
self
.
global_staff
.
username
,
password
=
"test"
)
with
self
.
_certificate_status
(
"honor"
,
status
):
self
.
_assert_certificate_status
(
"honor"
,
status
)
def
test_show_enabled_button
(
self
):
self
.
client
.
login
(
username
=
self
.
global_staff
.
username
,
password
=
"test"
)
# Initially, no example certs are generated, so
# the enable button should be disabled
self
.
_assert_enable_certs_button_is_disabled
()
with
self
.
_certificate_status
(
"honor"
,
"success"
):
# Certs are disabled for the course, so the enable button should be shown
self
.
_assert_enable_certs_button
(
True
)
# Enable certificates for the course
certs_api
.
set_cert_generation_enabled
(
self
.
course
.
id
,
True
)
# Now the "disable" button should be shown
self
.
_assert_enable_certs_button
(
False
)
def
test_can_disable_even_after_failure
(
self
):
self
.
client
.
login
(
username
=
self
.
global_staff
.
username
,
password
=
"test"
)
with
self
.
_certificate_status
(
"honor"
,
"error"
):
# When certs are disabled for a course, then don't allow them
# to be enabled if certificate generation doesn't complete successfully
certs_api
.
set_cert_generation_enabled
(
self
.
course
.
id
,
False
)
self
.
_assert_enable_certs_button_is_disabled
()
# However, if certificates are already enabled, allow them
# to be disabled even if an error has occurred
certs_api
.
set_cert_generation_enabled
(
self
.
course
.
id
,
True
)
self
.
_assert_enable_certs_button
(
False
)
def
_assert_certificates_visible
(
self
,
is_visible
):
"""Check that the certificates section is visible on the instructor dash. """
response
=
self
.
client
.
get
(
self
.
url
)
if
is_visible
:
self
.
assertContains
(
response
,
"Certificates"
)
else
:
self
.
assertNotContains
(
response
,
"Certificates"
)
@contextlib.contextmanager
def
_certificate_status
(
self
,
description
,
status
):
"""Configure the certificate status by mocking the certificates API. """
patched
=
'instructor.views.instructor_dashboard.certs_api.example_certificates_status'
with
mock
.
patch
(
patched
)
as
certs_api_status
:
cert_status
=
[{
'description'
:
description
,
'status'
:
status
}]
if
status
==
'error'
:
cert_status
[
0
][
'error_reason'
]
=
self
.
ERROR_REASON
if
status
==
'success'
:
cert_status
[
0
][
'download_url'
]
=
self
.
DOWNLOAD_URL
certs_api_status
.
return_value
=
cert_status
yield
def
_assert_certificate_status
(
self
,
cert_name
,
expected_status
):
"""Check the certificate status display on the instructor dash. """
response
=
self
.
client
.
get
(
self
.
url
)
if
expected_status
==
'started'
:
expected
=
'Generating example {name} certificate'
.
format
(
name
=
cert_name
)
self
.
assertContains
(
response
,
expected
)
elif
expected_status
==
'error'
:
expected
=
self
.
ERROR_REASON
self
.
assertContains
(
response
,
expected
)
elif
expected_status
==
'success'
:
expected
=
self
.
DOWNLOAD_URL
self
.
assertContains
(
response
,
expected
)
else
:
self
.
fail
(
"Invalid certificate status: {status}"
.
format
(
status
=
expected_status
))
def
_assert_enable_certs_button_is_disabled
(
self
):
"""Check that the "enable student-generated certificates" button is disabled. """
response
=
self
.
client
.
get
(
self
.
url
)
expected_html
=
'<button class="is-disabled" disabled>Enable Student-Generated Certificates</button>'
self
.
assertContains
(
response
,
expected_html
)
def
_assert_enable_certs_button
(
self
,
is_enabled
):
"""Check whether the button says "enable" or "disable" cert generation. """
response
=
self
.
client
.
get
(
self
.
url
)
expected_html
=
(
'Enable Student-Generated Certificates'
if
is_enabled
else
'Disable Student-Generated Certificates'
)
self
.
assertContains
(
response
,
expected_html
)
@override_settings
(
CERT_QUEUE
=
'certificates'
)
@ddt.ddt
class
CertificatesInstructorApiTest
(
ModuleStoreTestCase
):
"""Tests for the certificates end-points in the instructor dash API. """
def
setUp
(
self
):
super
(
CertificatesInstructorApiTest
,
self
)
.
setUp
()
self
.
course
=
CourseFactory
.
create
()
self
.
global_staff
=
GlobalStaffFactory
()
self
.
instructor
=
InstructorFactory
(
course_key
=
self
.
course
.
id
)
# Enable certificate generation
cache
.
clear
()
CertificateGenerationConfiguration
.
objects
.
create
(
enabled
=
True
)
@ddt.data
(
'generate_example_certificates'
,
'enable_certificate_generation'
)
def
test_allow_only_global_staff
(
self
,
url_name
):
url
=
reverse
(
url_name
,
kwargs
=
{
'course_id'
:
self
.
course
.
id
})
# Instructors do not have access
self
.
client
.
login
(
username
=
self
.
instructor
.
username
,
password
=
'test'
)
response
=
self
.
client
.
post
(
url
)
self
.
assertEqual
(
response
.
status_code
,
403
)
# Global staff have access
self
.
client
.
login
(
username
=
self
.
global_staff
.
username
,
password
=
'test'
)
response
=
self
.
client
.
post
(
url
)
self
.
assertEqual
(
response
.
status_code
,
302
)
def
test_generate_example_certificates
(
self
):
self
.
client
.
login
(
username
=
self
.
global_staff
.
username
,
password
=
'test'
)
url
=
reverse
(
'generate_example_certificates'
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course
.
id
)}
)
response
=
self
.
client
.
post
(
url
)
# Expect a redirect back to the instructor dashboard
self
.
_assert_redirects_to_instructor_dash
(
response
)
# Expect that certificate generation started
# Cert generation will fail here because XQueue isn't configured,
# but the status should at least not be None.
status
=
certs_api
.
example_certificates_status
(
self
.
course
.
id
)
self
.
assertIsNot
(
status
,
None
)
@ddt.data
(
True
,
False
)
def
test_enable_certificate_generation
(
self
,
is_enabled
):
self
.
client
.
login
(
username
=
self
.
global_staff
.
username
,
password
=
'test'
)
url
=
reverse
(
'enable_certificate_generation'
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course
.
id
)}
)
params
=
{
'certificates-enabled'
:
'true'
if
is_enabled
else
'false'
}
response
=
self
.
client
.
post
(
url
,
data
=
params
)
# Expect a redirect back to the instructor dashboard
self
.
_assert_redirects_to_instructor_dash
(
response
)
# Expect that certificate generation is now enabled for the course
actual_enabled
=
certs_api
.
cert_generation_enabled
(
self
.
course
.
id
)
self
.
assertEqual
(
is_enabled
,
actual_enabled
)
def
_assert_redirects_to_instructor_dash
(
self
,
response
):
"""Check that the response redirects to the certificates section. """
expected_redirect
=
reverse
(
'instructor_dashboard'
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course
.
id
)}
)
expected_redirect
+=
'#view-certificates'
self
.
assertRedirects
(
response
,
expected_redirect
)
lms/djangoapps/instructor/views/api.py
View file @
3604e2dd
...
@@ -23,16 +23,15 @@ from django.core.validators import validate_email
...
@@ -23,16 +23,15 @@ from django.core.validators import validate_email
from
django.utils.translation
import
ugettext
as
_
from
django.utils.translation
import
ugettext
as
_
from
django.http
import
HttpResponse
,
HttpResponseBadRequest
,
HttpResponseForbidden
,
HttpResponseNotFound
from
django.http
import
HttpResponse
,
HttpResponseBadRequest
,
HttpResponseForbidden
,
HttpResponseNotFound
from
django.utils.html
import
strip_tags
from
django.utils.html
import
strip_tags
from
django.shortcuts
import
redirect
import
string
# pylint: disable=deprecated-module
import
string
# pylint: disable=deprecated-module
import
random
import
random
import
unicodecsv
import
unicodecsv
import
urllib
import
urllib
import
decimal
import
decimal
from
student
import
auth
from
student
import
auth
from
student.roles
import
CourseSalesAdminRole
from
student.roles
import
GlobalStaff
,
CourseSalesAdminRole
from
util.file
import
store_uploaded_file
,
course_and_time_based_filename_generator
,
FileValidationException
,
UniversalNewlineIterator
from
util.file
import
store_uploaded_file
,
course_and_time_based_filename_generator
,
FileValidationException
,
UniversalNewlineIterator
import
datetime
import
pytz
from
util.json_request
import
JsonResponse
from
util.json_request
import
JsonResponse
from
instructor.views.instructor_task_helpers
import
extract_email_features
,
extract_task_features
from
instructor.views.instructor_task_helpers
import
extract_email_features
,
extract_task_features
...
@@ -58,7 +57,10 @@ from shoppingcart.models import (
...
@@ -58,7 +57,10 @@ from shoppingcart.models import (
CourseMode
,
CourseMode
,
CourseRegistrationCodeInvoiceItem
,
CourseRegistrationCodeInvoiceItem
,
)
)
from
student.models
import
CourseEnrollment
,
unique_id_for_user
,
anonymous_id_for_user
,
EntranceExamConfiguration
from
student.models
import
(
CourseEnrollment
,
unique_id_for_user
,
anonymous_id_for_user
,
UserProfile
,
Registration
,
EntranceExamConfiguration
)
import
instructor_task.api
import
instructor_task.api
from
instructor_task.api_helper
import
AlreadyRunningError
from
instructor_task.api_helper
import
AlreadyRunningError
from
instructor_task.models
import
ReportStore
from
instructor_task.models
import
ReportStore
...
@@ -82,6 +84,8 @@ from instructor.views import INVOICE_KEY
...
@@ -82,6 +84,8 @@ from instructor.views import INVOICE_KEY
from
submissions
import
api
as
sub_api
# installed from the edx-submissions repository
from
submissions
import
api
as
sub_api
# installed from the edx-submissions repository
from
certificates
import
api
as
certs_api
from
bulk_email.models
import
CourseEmail
from
bulk_email.models
import
CourseEmail
from
.tools
import
(
from
.tools
import
(
...
@@ -100,7 +104,6 @@ from .tools import (
...
@@ -100,7 +104,6 @@ from .tools import (
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
opaque_keys
import
InvalidKeyError
from
opaque_keys
import
InvalidKeyError
from
student.models
import
UserProfile
,
Registration
log
=
logging
.
getLogger
(
__name__
)
log
=
logging
.
getLogger
(
__name__
)
...
@@ -234,6 +237,20 @@ def require_level(level):
...
@@ -234,6 +237,20 @@ def require_level(level):
return
decorator
return
decorator
def
require_global_staff
(
func
):
"""View decorator that requires that the user have global staff permissions. """
def
wrapped
(
request
,
*
args
,
**
kwargs
):
# pylint: disable=missing-docstring
if
GlobalStaff
()
.
has_user
(
request
.
user
):
return
func
(
request
,
*
args
,
**
kwargs
)
else
:
return
HttpResponseForbidden
(
u"Must be {platform_name} staff to perform this action."
.
format
(
platform_name
=
settings
.
PLATFORM_NAME
)
)
return
wrapped
def
require_sales_admin
(
func
):
def
require_sales_admin
(
func
):
"""
"""
Decorator for checking sales administrator access before executing an HTTP endpoint. This decorator
Decorator for checking sales administrator access before executing an HTTP endpoint. This decorator
...
@@ -2284,6 +2301,60 @@ def _split_input_list(str_list):
...
@@ -2284,6 +2301,60 @@ def _split_input_list(str_list):
return
new_list
return
new_list
def
_instructor_dash_url
(
course_key
,
section
=
None
):
"""Return the URL for a section in the instructor dashboard.
Arguments:
course_key (CourseKey)
Keyword Arguments:
section (str): The name of the section to load.
Returns:
unicode: The URL of a section in the instructor dashboard.
"""
url
=
reverse
(
'instructor_dashboard'
,
kwargs
=
{
'course_id'
:
unicode
(
course_key
)})
if
section
is
not
None
:
url
+=
u'#view-{section}'
.
format
(
section
=
section
)
return
url
@require_global_staff
@require_POST
def
generate_example_certificates
(
request
,
course_id
=
None
):
# pylint: disable=unused-argument
"""Start generating a set of example certificates.
Example certificates are used to verify that certificates have
been configured correctly for the course.
Redirects back to the intructor dashboard once certificate
generation has begun.
"""
course_key
=
CourseKey
.
from_string
(
course_id
)
certs_api
.
generate_example_certificates
(
course_key
)
return
redirect
(
_instructor_dash_url
(
course_key
,
section
=
'certificates'
))
@require_global_staff
@require_POST
def
enable_certificate_generation
(
request
,
course_id
=
None
):
"""Enable/disable self-generated certificates for a course.
Once self-generated certificates have been enabled, students
who have passed the course will be able to generate certificates.
Redirects back to the intructor dashboard once the
setting has been updated.
"""
course_key
=
CourseKey
.
from_string
(
course_id
)
is_enabled
=
(
request
.
POST
.
get
(
'certificates-enabled'
,
'false'
)
==
'true'
)
certs_api
.
set_cert_generation_enabled
(
course_key
,
is_enabled
)
return
redirect
(
_instructor_dash_url
(
course_key
,
section
=
'certificates'
))
#---- Gradebook (shown to small courses only) ----
#---- Gradebook (shown to small courses only) ----
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
@require_level
(
'staff'
)
@require_level
(
'staff'
)
...
...
lms/djangoapps/instructor/views/api_urls.py
View file @
3604e2dd
...
@@ -109,4 +109,13 @@ urlpatterns = patterns(
...
@@ -109,4 +109,13 @@ urlpatterns = patterns(
# Cohort management
# Cohort management
url
(
r'add_users_to_cohorts$'
,
url
(
r'add_users_to_cohorts$'
,
'instructor.views.api.add_users_to_cohorts'
,
name
=
"add_users_to_cohorts"
),
'instructor.views.api.add_users_to_cohorts'
,
name
=
"add_users_to_cohorts"
),
# Certificates
url
(
r'^generate_example_certificates$'
,
'instructor.views.api.generate_example_certificates'
,
name
=
'generate_example_certificates'
),
url
(
r'^enable_certificate_generation$'
,
'instructor.views.api.enable_certificate_generation'
,
name
=
'enable_certificate_generation'
),
)
)
lms/djangoapps/instructor/views/instructor_dashboard.py
View file @
3604e2dd
...
@@ -36,6 +36,8 @@ from student.models import CourseEnrollment
...
@@ -36,6 +36,8 @@ from student.models import CourseEnrollment
from
shoppingcart.models
import
Coupon
,
PaidCourseRegistration
from
shoppingcart.models
import
Coupon
,
PaidCourseRegistration
from
course_modes.models
import
CourseMode
,
CourseModesArchive
from
course_modes.models
import
CourseMode
,
CourseModesArchive
from
student.roles
import
CourseFinanceAdminRole
,
CourseSalesAdminRole
from
student.roles
import
CourseFinanceAdminRole
,
CourseSalesAdminRole
from
certificates.models
import
CertificateGenerationConfiguration
from
certificates
import
api
as
certs_api
from
class_dashboard.dashboard_data
import
get_section_display_name
,
get_array_section_has_problem
from
class_dashboard.dashboard_data
import
get_section_display_name
,
get_array_section_has_problem
from
.tools
import
get_units_with_due_date
,
title_or_url
,
bulk_email_is_enabled_for_course
from
.tools
import
get_units_with_due_date
,
title_or_url
,
bulk_email_is_enabled_for_course
...
@@ -108,6 +110,13 @@ def instructor_dashboard_2(request, course_id):
...
@@ -108,6 +110,13 @@ def instructor_dashboard_2(request, course_id):
if
course_mode_has_price
and
(
access
[
'finance_admin'
]
or
access
[
'sales_admin'
]):
if
course_mode_has_price
and
(
access
[
'finance_admin'
]
or
access
[
'sales_admin'
]):
sections
.
append
(
_section_e_commerce
(
course
,
access
,
paid_modes
[
0
],
is_white_label
))
sections
.
append
(
_section_e_commerce
(
course
,
access
,
paid_modes
[
0
],
is_white_label
))
# Certificates panel
# This is used to generate example certificates
# and enable self-generated certificates for a course.
certs_enabled
=
CertificateGenerationConfiguration
.
current
()
.
enabled
if
certs_enabled
and
access
[
'admin'
]:
sections
.
append
(
_section_certificates
(
course
))
disable_buttons
=
not
_is_small_course
(
course_key
)
disable_buttons
=
not
_is_small_course
(
course_key
)
analytics_dashboard_message
=
None
analytics_dashboard_message
=
None
...
@@ -182,6 +191,53 @@ def _section_e_commerce(course, access, paid_mode, coupons_enabled):
...
@@ -182,6 +191,53 @@ def _section_e_commerce(course, access, paid_mode, coupons_enabled):
return
section_data
return
section_data
def
_section_certificates
(
course
):
"""Section information for the certificates panel.
The certificates panel allows global staff to generate
example certificates and enable self-generated certificates
for a course.
Arguments:
course (Course)
Returns:
dict
"""
example_cert_status
=
certs_api
.
example_certificates_status
(
course
.
id
)
# Allow the user to enable self-generated certificates for students
# *only* once a set of example certificates has been successfully generated.
# If certificates have been misconfigured for the course (for example, if
# the PDF template hasn't been uploaded yet), then we don't want
# to turn on self-generated certificates for students!
can_enable_for_course
=
(
example_cert_status
is
not
None
and
all
(
cert_status
[
'status'
]
==
'success'
for
cert_status
in
example_cert_status
)
)
return
{
'section_key'
:
'certificates'
,
'section_display_name'
:
_
(
'Certificates'
),
'example_certificate_status'
:
example_cert_status
,
'can_enable_for_course'
:
can_enable_for_course
,
'enabled_for_course'
:
certs_api
.
cert_generation_enabled
(
course
.
id
),
'urls'
:
{
'generate_example_certificates'
:
reverse
(
'generate_example_certificates'
,
kwargs
=
{
'course_id'
:
course
.
id
}
),
'enable_certificate_generation'
:
reverse
(
'enable_certificate_generation'
,
kwargs
=
{
'course_id'
:
course
.
id
}
)
}
}
@ensure_csrf_cookie
@ensure_csrf_cookie
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
@require_POST
@require_POST
...
...
lms/envs/common.py
View file @
3604e2dd
...
@@ -1097,7 +1097,10 @@ rwd_header_footer_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/common_he
...
@@ -1097,7 +1097,10 @@ rwd_header_footer_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/common_he
staff_grading_js
=
sorted
(
rooted_glob
(
PROJECT_ROOT
/
'static'
,
'coffee/src/staff_grading/**/*.js'
))
staff_grading_js
=
sorted
(
rooted_glob
(
PROJECT_ROOT
/
'static'
,
'coffee/src/staff_grading/**/*.js'
))
open_ended_js
=
sorted
(
rooted_glob
(
PROJECT_ROOT
/
'static'
,
'coffee/src/open_ended/**/*.js'
))
open_ended_js
=
sorted
(
rooted_glob
(
PROJECT_ROOT
/
'static'
,
'coffee/src/open_ended/**/*.js'
))
notes_js
=
sorted
(
rooted_glob
(
PROJECT_ROOT
/
'static'
,
'coffee/src/notes/**/*.js'
))
notes_js
=
sorted
(
rooted_glob
(
PROJECT_ROOT
/
'static'
,
'coffee/src/notes/**/*.js'
))
instructor_dash_js
=
sorted
(
rooted_glob
(
PROJECT_ROOT
/
'static'
,
'coffee/src/instructor_dashboard/**/*.js'
))
instructor_dash_js
=
(
sorted
(
rooted_glob
(
PROJECT_ROOT
/
'static'
,
'coffee/src/instructor_dashboard/**/*.js'
))
+
sorted
(
rooted_glob
(
PROJECT_ROOT
/
'static'
,
'js/instructor_dashboard/**/*.js'
))
)
# JavaScript used by the student account and profile pages
# JavaScript used by the student account and profile pages
# These are not courseware, so they do not need many of the courseware-specific
# These are not courseware, so they do not need many of the courseware-specific
...
...
lms/static/js/instructor_dashboard/certificates.js
0 → 100644
View file @
3604e2dd
var
edx
=
edx
||
{};
(
function
(
$
,
gettext
)
{
'use strict'
;
edx
.
instructor_dashboard
=
edx
.
instructor_dashboard
||
{};
edx
.
instructor_dashboard
.
certificates
=
{};
$
(
function
()
{
/**
* Show a confirmation message before letting staff members
* enable/disable self-generated certificates for a course.
*/
$
(
'#enable-certificates-form'
).
on
(
'submit'
,
function
(
event
)
{
var
isEnabled
=
$
(
'#certificates-enabled'
).
val
()
===
'true'
,
confirmMessage
=
''
;
if
(
isEnabled
)
{
confirmMessage
=
gettext
(
'Allow students to generate certificates for this course?'
);
}
else
{
confirmMessage
=
gettext
(
'Prevent students from generating certificates in this course?'
);
}
if
(
!
confirm
(
confirmMessage
)
)
{
event
.
preventDefault
();
}
});
/**
* Refresh the status for example certificate generation
* by reloading the instructor dashboard.
*/
$
(
'#refresh-example-certificate-status'
).
on
(
'click'
,
function
()
{
window
.
location
.
reload
();
});
});
})(
$
,
gettext
);
lms/static/sass/course/instructor/_instructor_2.scss
View file @
3604e2dd
...
@@ -1773,6 +1773,17 @@ input[name="subject"] {
...
@@ -1773,6 +1773,17 @@ input[name="subject"] {
}
}
}
}
.certificates-wrapper
{
.generate-example-certificates-wrapper
{
margin-bottom
:
30px
;
}
.example-certificate-status-wrapper
{
width
:
75%
;
}
}
.profile-distribution-widget
{
.profile-distribution-widget
{
margin-bottom
:
(
$baseline
*
2
);
margin-bottom
:
(
$baseline
*
2
);
...
...
lms/templates/instructor/instructor_dashboard_2/certificates.html
0 → 100644
View file @
3604e2dd
<
%!
from
django
.
utils
.
translation
import
ugettext
as
_
%
>
<
%
page
args=
"section_data"
/>
<div
class=
"certificates-wrapper"
>
<div
class=
"example-certificates"
>
<h2>
${_('Example Certificates')}
</h2>
<div
class=
"generate-example-certificates-wrapper"
>
<p>
${_('Generate example certificates for the course.')}
</p>
<form
id=
"generate-example-certificates-form"
method=
"post"
action=
"${section_data['urls']['generate_example_certificates']}"
>
<input
type=
"hidden"
name=
"csrfmiddlewaretoken"
value=
"${csrf_token}"
>
<input
type=
"submit"
id=
"generate-example-certificates-submit"
value=
"${_('Generate Example Certificates')}"
/>
</form>
</div>
% if section_data['example_certificate_status'] is not None:
<div
class=
"example-certificate-status-wrapper"
>
<p>
${_("Status:")}
</p>
<ul>
% for cert_status in section_data['example_certificate_status']:
% if cert_status['status'] == 'started':
<li>
${_('Generating example {name} certificate').format(name=cert_status['description'])}
</li>
% elif cert_status['status'] == 'error':
<li>
${_('Error generating example {name} certificate: {error}').format(name=cert_status['description'], error=cert_status['error_reason'])}
</li>
% elif cert_status['status'] == 'success':
<li><a
href=
"${cert_status['download_url']}"
>
${_('View {name} certificate').format(name=cert_status['description'])}
</a></li>
</li>
% endif
% endfor
</ul>
<button
id=
"refresh-example-certificate-status"
>
${_("Refresh Status")}
</button>
</div>
% endif
</div>
<hr
/>
<div
class=
"enable-certificates"
>
<h2>
${_("Student-Generated Certificates")}
</h2>
% if section_data['enabled_for_course']:
<form
id=
"enable-certificates-form"
method=
"post"
action=
"${section_data['urls']['enable_certificate_generation']}"
>
<input
type=
"hidden"
name=
"csrfmiddlewaretoken"
value=
"${csrf_token}"
>
<input
type=
"hidden"
id=
"certificates-enabled"
name=
"certificates-enabled"
value=
"false"
/>
<input
type=
"submit"
id=
"disable-certificates-submit"
value=
"${_('Disable Student-Generated Certificates')}"
/>
</form>
% elif section_data['can_enable_for_course']:
<form
id=
"enable-certificates-form"
method=
"post"
action=
"${section_data['urls']['enable_certificate_generation']}"
>
<input
type=
"hidden"
name=
"csrfmiddlewaretoken"
value=
"${csrf_token}"
>
<input
type=
"hidden"
id=
"certificates-enabled"
name=
"certificates-enabled"
value=
"true"
/>
<input
type=
"submit"
id=
"enable-certificates-submit"
value=
"${_('Enable Student-Generated Certificates')}"
/>
</form>
% else:
<p>
${_("You must successfully generate example certificates before you enable student-generated certificates.")}
</p>
<button
class=
"is-disabled"
disabled
>
${_('Enable Student-Generated Certificates')}
</button>
% endif
</div>
</div>
lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html
View file @
3604e2dd
...
@@ -53,7 +53,6 @@
...
@@ -53,7 +53,6 @@
<
%
static:js
group=
'application'
/>
<
%
static:js
group=
'application'
/>
## Backbone classes declared explicitly until RequireJS is supported
## Backbone classes declared explicitly until RequireJS is supported
<script
type=
"text/javascript"
src=
"${static.url('js/instructor_dashboard/ecommerce.js')}"
></script>
<script
type=
"text/javascript"
src=
"${static.url('js/models/notification.js')}"
></script>
<script
type=
"text/javascript"
src=
"${static.url('js/models/notification.js')}"
></script>
<script
type=
"text/javascript"
src=
"${static.url('js/views/notification.js')}"
></script>
<script
type=
"text/javascript"
src=
"${static.url('js/views/notification.js')}"
></script>
<script
type=
"text/javascript"
src=
"${static.url('js/views/file_uploader.js')}"
></script>
<script
type=
"text/javascript"
src=
"${static.url('js/views/file_uploader.js')}"
></script>
...
...
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