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
37aebaa7
Commit
37aebaa7
authored
Dec 16, 2015
by
asadiqbal
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
SOL-1492
parent
7f6e8b88
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
451 additions
and
97 deletions
+451
-97
lms/djangoapps/certificates/tests/test_support_views.py
+141
-12
lms/djangoapps/certificates/urls.py
+2
-1
lms/djangoapps/certificates/views/support.py
+88
-12
lms/djangoapps/support/static/support/js/collections/certificate.js
+13
-4
lms/djangoapps/support/static/support/js/spec/views/certificates_spec.js
+92
-28
lms/djangoapps/support/static/support/js/views/certificates.js
+64
-22
lms/djangoapps/support/static/support/templates/certificates.underscore
+9
-2
lms/djangoapps/support/static/support/templates/certificates_results.underscore
+9
-0
lms/djangoapps/support/tests/test_views.py
+17
-9
lms/djangoapps/support/views/certificate.py
+2
-1
lms/static/sass/views/_support.scss
+12
-5
lms/templates/support/certificates.html
+2
-1
No files found.
lms/djangoapps/certificates/tests/test_support_views.py
View file @
37aebaa7
...
...
@@ -7,7 +7,6 @@ import json
import
ddt
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
from
django.test
import
TestCase
from
django.test.utils
import
override_settings
from
opaque_keys.edx.keys
import
CourseKey
...
...
@@ -22,7 +21,7 @@ FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy()
FEATURES_WITH_CERTS_ENABLED
[
'CERTIFICATES_HTML_VIEW'
]
=
True
class
CertificateSupportTestCase
(
TestCase
):
class
CertificateSupportTestCase
(
ModuleStore
TestCase
):
"""
Base class for tests of the certificate support views.
"""
...
...
@@ -36,6 +35,9 @@ class CertificateSupportTestCase(TestCase):
STUDENT_PASSWORD
=
"student"
CERT_COURSE_KEY
=
CourseKey
.
from_string
(
"edX/DemoX/Demo_Course"
)
COURSE_NOT_EXIST_KEY
=
CourseKey
.
from_string
(
"test/TestX/Test_Course_Not_Exist"
)
EXISTED_COURSE_KEY_1
=
CourseKey
.
from_string
(
"test1/Test1X/Test_Course_Exist_1"
)
EXISTED_COURSE_KEY_2
=
CourseKey
.
from_string
(
"test2/Test2X/Test_Course_Exist_2"
)
CERT_GRADE
=
0.89
CERT_STATUS
=
CertificateStatuses
.
downloadable
CERT_MODE
=
"verified"
...
...
@@ -47,6 +49,11 @@ class CertificateSupportTestCase(TestCase):
Log in as the support team member.
"""
super
(
CertificateSupportTestCase
,
self
)
.
setUp
()
CourseFactory
(
org
=
CertificateSupportTestCase
.
EXISTED_COURSE_KEY_1
.
org
,
course
=
CertificateSupportTestCase
.
EXISTED_COURSE_KEY_1
.
course
,
run
=
CertificateSupportTestCase
.
EXISTED_COURSE_KEY_1
.
run
,
)
# Create the support staff user
self
.
support
=
UserFactory
(
...
...
@@ -79,7 +86,7 @@ class CertificateSupportTestCase(TestCase):
@ddt.ddt
class
CertificateSearchTests
(
ModuleStoreTestCase
,
CertificateSupportTestCase
):
class
CertificateSearchTests
(
CertificateSupportTestCase
):
"""
Tests for the certificate search end-point used by the support team.
"""
...
...
@@ -137,14 +144,20 @@ class CertificateSearchTests(ModuleStoreTestCase, CertificateSupportTestCase):
(
CertificateSupportTestCase
.
STUDENT_EMAIL
,
True
),
(
"bar"
,
False
),
(
"bar@example.com"
,
False
),
(
""
,
False
),
(
CertificateSupportTestCase
.
STUDENT_USERNAME
,
False
,
'invalid_key'
),
(
CertificateSupportTestCase
.
STUDENT_USERNAME
,
False
,
unicode
(
CertificateSupportTestCase
.
COURSE_NOT_EXIST_KEY
)),
(
CertificateSupportTestCase
.
STUDENT_USERNAME
,
True
,
unicode
(
CertificateSupportTestCase
.
EXISTED_COURSE_KEY_1
)),
)
@ddt.unpack
def
test_search
(
self
,
query
,
expect_result
):
response
=
self
.
_search
(
query
)
self
.
assertEqual
(
response
.
status_code
,
200
)
results
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
len
(
results
),
1
if
expect_result
else
0
)
def
test_search
(
self
,
user_filter
,
expect_result
,
course_filter
=
None
):
response
=
self
.
_search
(
user_filter
,
course_filter
)
if
expect_result
:
self
.
assertEqual
(
response
.
status_code
,
200
)
results
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
len
(
results
),
1
)
else
:
self
.
assertEqual
(
response
.
status_code
,
400
)
def
test_results
(
self
):
response
=
self
.
_search
(
self
.
STUDENT_USERNAME
)
...
...
@@ -184,14 +197,16 @@ class CertificateSearchTests(ModuleStoreTestCase, CertificateSupportTestCase):
)
)
def
_search
(
self
,
query
):
def
_search
(
self
,
user_filter
,
course_filter
=
None
):
"""Execute a search and return the response. """
url
=
reverse
(
"certificates:search"
)
+
"?query="
+
query
url
=
reverse
(
"certificates:search"
)
+
"?user="
+
user_filter
if
course_filter
:
url
+=
'&course_id='
+
course_filter
return
self
.
client
.
get
(
url
)
@ddt.ddt
class
CertificateRegenerateTests
(
ModuleStoreTestCase
,
CertificateSupportTestCase
):
class
CertificateRegenerateTests
(
CertificateSupportTestCase
):
"""
Tests for the certificate regeneration end-point used by the support team.
"""
...
...
@@ -308,3 +323,117 @@ class CertificateRegenerateTests(ModuleStoreTestCase, CertificateSupportTestCase
params
[
"username"
]
=
username
return
self
.
client
.
post
(
url
,
params
)
@ddt.ddt
class
CertificateGenerateTests
(
CertificateSupportTestCase
):
"""
Tests for the certificate generation end-point used by the support team.
"""
def
setUp
(
self
):
"""
Create a course and enroll the student in the course.
"""
super
(
CertificateGenerateTests
,
self
)
.
setUp
()
self
.
course
=
CourseFactory
(
org
=
self
.
EXISTED_COURSE_KEY_2
.
org
,
course
=
self
.
EXISTED_COURSE_KEY_2
.
course
,
run
=
self
.
EXISTED_COURSE_KEY_2
.
run
)
CourseEnrollment
.
enroll
(
self
.
student
,
self
.
EXISTED_COURSE_KEY_2
,
self
.
CERT_MODE
)
@ddt.data
(
(
GlobalStaff
,
True
),
(
SupportStaffRole
,
True
),
(
None
,
False
),
)
@ddt.unpack
def
test_access_control
(
self
,
role
,
has_access
):
# Create a user and log in
user
=
UserFactory
(
username
=
"foo"
,
password
=
"foo"
)
success
=
self
.
client
.
login
(
username
=
"foo"
,
password
=
"foo"
)
self
.
assertTrue
(
success
,
msg
=
"Could not log in"
)
# Assign the user to the role
if
role
is
not
None
:
role
()
.
add_users
(
user
)
# Make a POST request
# Since we're not passing valid parameters, we'll get an error response
# but at least we'll know we have access
response
=
self
.
_generate
()
if
has_access
:
self
.
assertEqual
(
response
.
status_code
,
400
)
else
:
self
.
assertEqual
(
response
.
status_code
,
403
)
def
test_generate_certificate
(
self
):
response
=
self
.
_generate
(
course_key
=
self
.
course
.
id
,
# pylint: disable=no-member
username
=
self
.
STUDENT_USERNAME
,
)
self
.
assertEqual
(
response
.
status_code
,
200
)
def
test_generate_certificate_missing_params
(
self
):
# Missing username
response
=
self
.
_generate
(
course_key
=
self
.
EXISTED_COURSE_KEY_2
)
self
.
assertEqual
(
response
.
status_code
,
400
)
# Missing course key
response
=
self
.
_generate
(
username
=
self
.
STUDENT_USERNAME
)
self
.
assertEqual
(
response
.
status_code
,
400
)
def
test_generate_no_such_user
(
self
):
response
=
self
.
_generate
(
course_key
=
unicode
(
self
.
EXISTED_COURSE_KEY_2
),
username
=
"invalid_username"
,
)
self
.
assertEqual
(
response
.
status_code
,
400
)
def
test_generate_no_such_course
(
self
):
response
=
self
.
_generate
(
course_key
=
CourseKey
.
from_string
(
"edx/invalid/course"
),
username
=
self
.
STUDENT_USERNAME
)
self
.
assertEqual
(
response
.
status_code
,
400
)
def
test_generate_user_is_not_enrolled
(
self
):
# Unenroll the user
CourseEnrollment
.
unenroll
(
self
.
student
,
self
.
EXISTED_COURSE_KEY_2
)
# Can no longer regenerate certificates for the user
response
=
self
.
_generate
(
course_key
=
self
.
EXISTED_COURSE_KEY_2
,
username
=
self
.
STUDENT_USERNAME
)
self
.
assertEqual
(
response
.
status_code
,
400
)
def
test_generate_user_has_no_certificate
(
self
):
# Delete the user's certificate
GeneratedCertificate
.
objects
.
all
()
.
delete
()
# Should be able to generate
response
=
self
.
_generate
(
course_key
=
self
.
EXISTED_COURSE_KEY_2
,
username
=
self
.
STUDENT_USERNAME
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# A new certificate is created
num_certs
=
GeneratedCertificate
.
objects
.
filter
(
user
=
self
.
student
)
.
count
()
self
.
assertEqual
(
num_certs
,
1
)
def
_generate
(
self
,
course_key
=
None
,
username
=
None
):
"""Call the generation end-point and return the response. """
url
=
reverse
(
"certificates:generate_certificate_for_user"
)
params
=
{}
if
course_key
is
not
None
:
params
[
"course_key"
]
=
course_key
if
username
is
not
None
:
params
[
"username"
]
=
username
return
self
.
client
.
post
(
url
,
params
)
lms/djangoapps/certificates/urls.py
View file @
37aebaa7
...
...
@@ -27,8 +27,9 @@ urlpatterns = patterns(
# End-points used by student support
# The views in the lms/djangoapps/support use these end-points
# to retrieve certificate information and regenerate certificates.
url
(
r'search'
,
views
.
search_
by_user
,
name
=
"search"
),
url
(
r'search'
,
views
.
search_
certificates
,
name
=
"search"
),
url
(
r'regenerate'
,
views
.
regenerate_certificate_for_user
,
name
=
"regenerate_certificate_for_user"
),
url
(
r'generate'
,
views
.
generate_certificate_for_user
,
name
=
"generate_certificate_for_user"
),
)
...
...
lms/djangoapps/certificates/views/support.py
View file @
37aebaa7
...
...
@@ -5,6 +5,7 @@ See lms/djangoapps/support for more details.
"""
import
logging
import
urllib
from
functools
import
wraps
from
django.http
import
(
...
...
@@ -25,6 +26,8 @@ from student.models import User, CourseEnrollment
from
courseware.access
import
has_access
from
util.json_request
import
JsonResponse
from
certificates
import
api
from
instructor_task.api
import
generate_certificates_for_students
from
openedx.core.djangoapps.content.course_overviews.models
import
CourseOverview
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -46,11 +49,15 @@ def require_certificate_permission(func):
@require_GET
@require_certificate_permission
def
search_
by_user
(
request
):
def
search_
certificates
(
request
):
"""
Search for certificates for a particular user.
Search for certificates for a particular user
OR along with the given course
.
Supports search by either username or email address.
Supports search by either username or email address along with course id.
First filter the records for the given username/email and then filter against the given course id (if given).
Show the 'Regenerate' button if a record found in 'generatedcertificate' model otherwise it will show the Generate
button.
Arguments:
request (HttpRequest): The request object.
...
...
@@ -59,7 +66,8 @@ def search_by_user(request):
JsonResponse
Example Usage:
GET /certificates/search?query=bob@example.com
GET /certificates/search?user=bob@example.com
GET /certificates/search?user=bob@example.com&course_id=xyz
Response: 200 OK
Content-Type: application/json
...
...
@@ -77,27 +85,46 @@ def search_by_user(request):
]
"""
query
=
request
.
GET
.
get
(
"query"
)
if
not
query
:
return
JsonResponse
([])
user_filter
=
request
.
GET
.
get
(
"user"
,
""
)
if
not
user_filter
:
msg
=
_
(
"user is not given."
)
return
HttpResponseBadRequest
(
msg
)
try
:
user
=
User
.
objects
.
get
(
Q
(
email
=
query
)
|
Q
(
username
=
query
))
user
=
User
.
objects
.
get
(
Q
(
email
=
user_filter
)
|
Q
(
username
=
user_filter
))
except
User
.
DoesNotExist
:
return
JsonResponse
([]
)
return
HttpResponseBadRequest
(
_
(
"user '{user}' does not exist"
)
.
format
(
user
=
user_filter
)
)
certificates
=
api
.
get_certificates_for_user
(
user
.
username
)
for
cert
in
certificates
:
cert
[
"course_key"
]
=
unicode
(
cert
[
"course_key"
])
cert
[
"created"
]
=
cert
[
"created"
]
.
isoformat
()
cert
[
"modified"
]
=
cert
[
"modified"
]
.
isoformat
()
cert
[
"regenerate"
]
=
True
course_id
=
urllib
.
quote_plus
(
request
.
GET
.
get
(
"course_id"
,
""
),
safe
=
':/'
)
if
course_id
:
try
:
course_key
=
CourseKey
.
from_string
(
course_id
)
except
InvalidKeyError
:
return
HttpResponseBadRequest
(
_
(
"Course id '{course_id}' is not valid"
)
.
format
(
course_id
=
course_id
))
else
:
try
:
if
CourseOverview
.
get_from_id
(
course_key
):
certificates
=
[
certificate
for
certificate
in
certificates
if
certificate
[
'course_key'
]
==
course_id
]
if
not
certificates
:
return
JsonResponse
([{
'username'
:
user
.
username
,
'course_key'
:
course_id
,
'regenerate'
:
False
}])
except
CourseOverview
.
DoesNotExist
:
msg
=
_
(
"The course does not exist against the given key '{course_key}'"
)
.
format
(
course_key
=
course_key
)
return
HttpResponseBadRequest
(
msg
)
return
JsonResponse
(
certificates
)
def
_validate_
regen_
post_params
(
params
):
def
_validate_post_params
(
params
):
"""
Validate request POST parameters to the regenerate certificates end-point.
Validate request POST parameters to the
generate and
regenerate certificates end-point.
Arguments:
params (QueryDict): Request parameters.
...
...
@@ -149,7 +176,7 @@ def regenerate_certificate_for_user(request):
"""
# Check the POST parameters, returning a 400 response if they're not valid.
params
,
response
=
_validate_
regen_
post_params
(
request
.
POST
)
params
,
response
=
_validate_post_params
(
request
.
POST
)
if
response
is
not
None
:
return
response
...
...
@@ -186,3 +213,52 @@ def regenerate_certificate_for_user(request):
params
[
"user"
]
.
id
,
params
[
"course_key"
]
)
return
HttpResponse
(
200
)
@transaction.non_atomic_requests
@require_POST
@require_certificate_permission
def
generate_certificate_for_user
(
request
):
"""
Generate certificates for a user.
This is meant to be used by support staff through the UI in lms/djangoapps/support
Arguments:
request (HttpRequest): The request object
Returns:
HttpResponse
Example Usage:
POST /certificates/generate
* username: "bob"
* course_key: "edX/DemoX/Demo_Course"
Response: 200 OK
"""
# Check the POST parameters, returning a 400 response if they're not valid.
params
,
response
=
_validate_post_params
(
request
.
POST
)
if
response
is
not
None
:
return
response
try
:
# Check that the course exists
CourseOverview
.
get_from_id
(
params
[
"course_key"
])
except
CourseOverview
.
DoesNotExist
:
msg
=
_
(
"The course {course_key} does not exist"
)
.
format
(
course_key
=
params
[
"course_key"
])
return
HttpResponseBadRequest
(
msg
)
else
:
# Check that the user is enrolled in the course
if
not
CourseEnrollment
.
is_enrolled
(
params
[
"user"
],
params
[
"course_key"
]):
msg
=
_
(
"User {username} is not enrolled in the course {course_key}"
)
.
format
(
username
=
params
[
"user"
]
.
username
,
course_key
=
params
[
"course_key"
]
)
return
HttpResponseBadRequest
(
msg
)
# Attempt to generate certificate
generate_certificates_for_students
(
request
,
params
[
"course_key"
],
students
=
[
params
[
"user"
]])
return
HttpResponse
(
200
)
lms/djangoapps/support/static/support/js/collections/certificate.js
View file @
37aebaa7
...
...
@@ -6,15 +6,24 @@
model
:
CertModel
,
initialize
:
function
(
options
)
{
this
.
userQuery
=
options
.
userQuery
||
''
;
this
.
userFilter
=
options
.
userFilter
||
''
;
this
.
courseFilter
=
options
.
courseFilter
||
''
;
},
setUserQuery
:
function
(
userQuery
)
{
this
.
userQuery
=
userQuery
;
setUserFilter
:
function
(
userFilter
)
{
this
.
userFilter
=
userFilter
;
},
setCourseFilter
:
function
(
courseFilter
)
{
this
.
courseFilter
=
courseFilter
;
},
url
:
function
()
{
return
'/certificates/search?query='
+
this
.
userQuery
;
var
url
=
'/certificates/search?user='
+
this
.
userFilter
;
if
(
this
.
courseFilter
)
{
url
+=
'&course_id='
+
this
.
courseFilter
;
}
return
url
;
}
});
});
...
...
lms/djangoapps/support/static/support/js/spec/views/certificates_spec.js
View file @
37aebaa7
...
...
@@ -9,7 +9,7 @@ define([
var
view
=
null
,
SEARCH_RESULTS
=
[
REGENERATE_
SEARCH_RESULTS
=
[
{
'username'
:
'student'
,
'status'
:
'notpassing'
,
...
...
@@ -18,7 +18,8 @@ define([
'type'
:
'honor'
,
'course_key'
:
'course-v1:edX+DemoX+Demo_Course'
,
'download_url'
:
null
,
'modified'
:
'2015-08-06T19:47:07+00:00'
'modified'
:
'2015-08-06T19:47:07+00:00'
,
'regenerate'
:
true
},
{
'username'
:
'student'
,
...
...
@@ -28,8 +29,23 @@ define([
'type'
:
'verified'
,
'course_key'
:
'edx/test/2015'
,
'download_url'
:
'http://www.example.com/certificate.pdf'
,
'modified'
:
'2015-08-06T19:47:05+00:00'
},
'modified'
:
'2015-08-06T19:47:05+00:00'
,
'regenerate'
:
true
}
],
GENERATE_SEARCH_RESULTS
=
[
{
'username'
:
'student'
,
'status'
:
''
,
'created'
:
''
,
'grade'
:
''
,
'type'
:
''
,
'course_key'
:
'edx/test1/2016'
,
'download_url'
:
null
,
'modified'
:
''
,
'regenerate'
:
false
}
],
getSearchResults
=
function
()
{
...
...
@@ -49,19 +65,29 @@ define([
return
results
;
},
searchFor
=
function
(
query
,
requests
,
response
)
{
searchFor
=
function
(
user_filter
,
course_filter
,
requests
,
response
)
{
// Enter the search term and submit
view
.
setUserQuery
(
query
);
var
url
=
'/certificates/search?user='
+
user_filter
;
view
.
setUserFilter
(
user_filter
);
if
(
course_filter
)
{
view
.
setCourseFilter
(
course_filter
);
url
+=
'&course_id='
+
course_filter
;
}
view
.
triggerSearch
();
// Simulate a response from the server
AjaxHelpers
.
expectJsonRequest
(
requests
,
'GET'
,
'/certificates/search?query=student@example.com'
);
AjaxHelpers
.
expectJsonRequest
(
requests
,
'GET'
,
url
);
AjaxHelpers
.
respondWithJson
(
requests
,
response
);
},
regenerateCerts
=
function
(
username
,
courseKey
)
{
var
sel
=
'.btn-cert-regenerate[data-course-key="'
+
courseKey
+
'"]'
;
$
(
sel
).
click
();
},
generateCerts
=
function
(
username
,
courseKey
)
{
var
sel
=
'.btn-cert-generate[data-course-key="'
+
courseKey
+
'"]'
;
$
(
sel
).
click
();
};
beforeEach
(
function
()
{
...
...
@@ -80,35 +106,49 @@ define([
var
requests
=
AjaxHelpers
.
requests
(
this
),
results
=
[];
searchFor
(
'student@example.com'
,
requests
,
SEARCH_RESULTS
);
searchFor
(
'student@example.com'
,
''
,
requests
,
REGENERATE_
SEARCH_RESULTS
);
results
=
getSearchResults
();
// Expect that the results displayed on the page match the results
// returned by the server.
expect
(
results
.
length
).
toEqual
(
SEARCH_RESULTS
.
length
);
expect
(
results
.
length
).
toEqual
(
REGENERATE_
SEARCH_RESULTS
.
length
);
// Check the first row of results
expect
(
results
[
0
][
0
]).
toEqual
(
SEARCH_RESULTS
[
0
].
course_key
);
expect
(
results
[
0
][
1
]).
toEqual
(
SEARCH_RESULTS
[
0
].
type
);
expect
(
results
[
0
][
2
]).
toEqual
(
SEARCH_RESULTS
[
0
].
status
);
expect
(
results
[
0
][
0
]).
toEqual
(
REGENERATE_
SEARCH_RESULTS
[
0
].
course_key
);
expect
(
results
[
0
][
1
]).
toEqual
(
REGENERATE_
SEARCH_RESULTS
[
0
].
type
);
expect
(
results
[
0
][
2
]).
toEqual
(
REGENERATE_
SEARCH_RESULTS
[
0
].
status
);
expect
(
results
[
0
][
3
]).
toContain
(
'Not available'
);
expect
(
results
[
0
][
4
]).
toEqual
(
SEARCH_RESULTS
[
0
].
grade
);
expect
(
results
[
0
][
5
]).
toEqual
(
SEARCH_RESULTS
[
0
].
modified
);
expect
(
results
[
0
][
4
]).
toEqual
(
REGENERATE_
SEARCH_RESULTS
[
0
].
grade
);
expect
(
results
[
0
][
5
]).
toEqual
(
REGENERATE_
SEARCH_RESULTS
[
0
].
modified
);
// Check the second row of results
expect
(
results
[
1
][
0
]).
toEqual
(
SEARCH_RESULTS
[
1
].
course_key
);
expect
(
results
[
1
][
1
]).
toEqual
(
SEARCH_RESULTS
[
1
].
type
);
expect
(
results
[
1
][
2
]).
toEqual
(
SEARCH_RESULTS
[
1
].
status
);
expect
(
results
[
1
][
3
]).
toContain
(
SEARCH_RESULTS
[
1
].
download_url
);
expect
(
results
[
1
][
4
]).
toEqual
(
SEARCH_RESULTS
[
1
].
grade
);
expect
(
results
[
1
][
5
]).
toEqual
(
SEARCH_RESULTS
[
1
].
modified
);
expect
(
results
[
1
][
0
]).
toEqual
(
REGENERATE_SEARCH_RESULTS
[
1
].
course_key
);
expect
(
results
[
1
][
1
]).
toEqual
(
REGENERATE_SEARCH_RESULTS
[
1
].
type
);
expect
(
results
[
1
][
2
]).
toEqual
(
REGENERATE_SEARCH_RESULTS
[
1
].
status
);
expect
(
results
[
1
][
3
]).
toContain
(
REGENERATE_SEARCH_RESULTS
[
1
].
download_url
);
expect
(
results
[
1
][
4
]).
toEqual
(
REGENERATE_SEARCH_RESULTS
[
1
].
grade
);
expect
(
results
[
1
][
5
]).
toEqual
(
REGENERATE_SEARCH_RESULTS
[
1
].
modified
);
searchFor
(
'student@example.com'
,
'edx/test1/2016'
,
requests
,
GENERATE_SEARCH_RESULTS
);
results
=
getSearchResults
();
expect
(
results
.
length
).
toEqual
(
GENERATE_SEARCH_RESULTS
.
length
);
// Check the first row of results
expect
(
results
[
0
][
0
]).
toEqual
(
GENERATE_SEARCH_RESULTS
[
0
].
course_key
);
expect
(
results
[
0
][
1
]).
toEqual
(
GENERATE_SEARCH_RESULTS
[
0
].
type
);
expect
(
results
[
0
][
2
]).
toEqual
(
GENERATE_SEARCH_RESULTS
[
0
].
status
);
expect
(
results
[
0
][
3
]).
toContain
(
'Not available'
);
expect
(
results
[
0
][
4
]).
toEqual
(
GENERATE_SEARCH_RESULTS
[
0
].
grade
);
expect
(
results
[
0
][
5
]).
toEqual
(
GENERATE_SEARCH_RESULTS
[
0
].
modified
);
});
it
(
'searches for certificates and displays a message when there are no results'
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
),
results
=
[];
searchFor
(
'student@example.com'
,
requests
,
[]);
searchFor
(
'student@example.com'
,
''
,
requests
,
[]);
results
=
getSearchResults
();
// Expect that no results are found
...
...
@@ -118,30 +158,30 @@ define([
expect
(
$
(
'.certificates-results'
).
text
()).
toContain
(
'No results'
);
});
it
(
'automatically searches for an initial
query
if one is provided'
,
function
()
{
it
(
'automatically searches for an initial
filter
if one is provided'
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
),
results
=
[];
// Re-render the view, this time providing an initial
query
.
// Re-render the view, this time providing an initial
filter
.
view
=
new
CertificatesView
({
el
:
$
(
'.certificates-content'
),
user
Query
:
'student@example.com'
user
Filter
:
'student@example.com'
}).
render
();
// Simulate a response from the server
AjaxHelpers
.
expectJsonRequest
(
requests
,
'GET'
,
'/certificates/search?
query
=student@example.com'
);
AjaxHelpers
.
respondWithJson
(
requests
,
SEARCH_RESULTS
);
AjaxHelpers
.
expectJsonRequest
(
requests
,
'GET'
,
'/certificates/search?
user
=student@example.com'
);
AjaxHelpers
.
respondWithJson
(
requests
,
REGENERATE_
SEARCH_RESULTS
);
// Check the search results
results
=
getSearchResults
();
expect
(
results
.
length
).
toEqual
(
SEARCH_RESULTS
.
length
);
expect
(
results
.
length
).
toEqual
(
REGENERATE_
SEARCH_RESULTS
.
length
);
});
it
(
'regenerates a certificate for a student'
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
);
// Trigger a search
searchFor
(
'student@example.com'
,
requests
,
SEARCH_RESULTS
);
searchFor
(
'student@example.com'
,
''
,
requests
,
REGENERATE_
SEARCH_RESULTS
);
// Click the button to regenerate certificates for a user
regenerateCerts
(
'student'
,
'course-v1:edX+DemoX+Demo_Course'
);
...
...
@@ -159,5 +199,29 @@ define([
// Respond with success
AjaxHelpers
.
respondWithJson
(
requests
,
''
);
});
it
(
'generate a certificate for a student'
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
);
// Trigger a search
searchFor
(
'student@example.com'
,
'edx/test1/2016'
,
requests
,
GENERATE_SEARCH_RESULTS
);
// Click the button to generate certificates for a user
generateCerts
(
'student'
,
'edx/test1/2016'
);
// Expect a request to the server
AjaxHelpers
.
expectPostRequest
(
requests
,
'/certificates/generate'
,
$
.
param
({
username
:
'student'
,
course_key
:
'edx/test1/2016'
})
);
// Respond with success
AjaxHelpers
.
respondWithJson
(
requests
,
''
);
});
});
});
lms/djangoapps/support/static/support/js/views/certificates.js
View file @
37aebaa7
...
...
@@ -12,24 +12,27 @@
return
Backbone
.
View
.
extend
({
events
:
{
'submit .certificates-form'
:
'search'
,
'click .btn-cert-regenerate'
:
'regenerateCertificate'
'click .btn-cert-regenerate'
:
'regenerateCertificate'
,
'click .btn-cert-generate'
:
'generateCertificate'
},
initialize
:
function
(
options
)
{
_
.
bindAll
(
this
,
'search'
,
'updateCertificates'
,
'regenerateCertificate'
,
'handleSearchError'
);
this
.
certificates
=
new
CertCollection
({});
this
.
initialQuery
=
options
.
userQuery
||
null
;
this
.
initialFilter
=
options
.
userFilter
||
null
;
this
.
courseFilter
=
options
.
courseFilter
||
null
;
},
render
:
function
()
{
this
.
$el
.
html
(
_
.
template
(
certificatesTpl
));
// If there is an initial
query
, then immediately trigger a search.
// If there is an initial
filter
, then immediately trigger a search.
// This is useful because it allows users to share search results:
// if the URL contains ?query="foo" then anyone who loads that URL
// will automatically search for "foo".
if
(
this
.
initialQuery
)
{
this
.
setUserQuery
(
this
.
initialQuery
);
// if the URL contains ?user_filter="foo"&course_id="xyz" then anyone who loads that URL
// will automatically search for "foo" and course "xyz".
if
(
this
.
initialFilter
)
{
this
.
setUserFilter
(
this
.
initialFilter
);
this
.
setCourseFilter
(
this
.
courseFilter
);
this
.
triggerSearch
();
}
...
...
@@ -38,7 +41,7 @@
renderResults
:
function
()
{
var
context
=
{
certificates
:
this
.
certificates
,
certificates
:
this
.
certificates
};
this
.
setResults
(
_
.
template
(
resultsTpl
,
context
));
...
...
@@ -52,26 +55,57 @@
search
:
function
(
event
)
{
// Fetch the certificate collection for the given user
var
query
=
this
.
getUserQuery
(),
url
=
'/support/certificates?query='
+
query
;
var
url
=
'/support/certificates?user='
+
this
.
getUserFilter
();
//course id is optional.
if
(
this
.
getCourseFilter
())
{
url
+=
'&course_id='
+
this
.
getCourseFilter
();
}
// Prevent form submission, since we're handling it ourselves.
event
.
preventDefault
();
// Push a URL into history with the search
query
as a GET parameter.
// Push a URL into history with the search
filter
as a GET parameter.
// That way, if the user reloads the page or sends someone the link
// then the same search will be performed on page load.
window
.
history
.
pushState
({},
window
.
document
.
title
,
url
);
// Perform a search for the user's certificates.
this
.
disableButtons
();
this
.
certificates
.
setUserQuery
(
query
);
this
.
certificates
.
setUserFilter
(
this
.
getUserFilter
());
this
.
certificates
.
setCourseFilter
(
this
.
getCourseFilter
());
this
.
certificates
.
fetch
({
success
:
this
.
updateCertificates
,
error
:
this
.
handleSearchError
});
},
generateCertificate
:
function
(
event
)
{
var
$button
=
$
(
event
.
target
);
// Generate certificates for a particular user and course.
// If this is successful, reload the certificate results so they show
// the updated status.
this
.
disableButtons
();
$
.
ajax
({
url
:
'/certificates/generate'
,
type
:
'POST'
,
data
:
{
username
:
$button
.
data
(
'username'
),
course_key
:
$button
.
data
(
'course-key'
)
},
context
:
this
,
success
:
function
()
{
this
.
certificates
.
fetch
({
success
:
this
.
updateCertificates
,
error
:
this
.
handleSearchError
});
},
error
:
this
.
handleGenerationsError
});
},
regenerateCertificate
:
function
(
event
)
{
var
$button
=
$
(
event
.
target
);
...
...
@@ -84,16 +118,16 @@
type
:
'POST'
,
data
:
{
username
:
$button
.
data
(
'username'
),
course_key
:
$button
.
data
(
'course-key'
)
,
course_key
:
$button
.
data
(
'course-key'
)
},
context
:
this
,
success
:
function
()
{
this
.
certificates
.
fetch
({
success
:
this
.
updateCertificates
,
error
:
this
.
handleSearchError
,
error
:
this
.
handleSearchError
});
},
error
:
this
.
handle
Regenerate
Error
error
:
this
.
handle
Generations
Error
});
},
...
...
@@ -102,12 +136,12 @@
this
.
enableButtons
();
},
handleSearchError
:
function
(
jqxhr
)
{
this
.
renderError
(
jqxhr
.
responseText
);
handleSearchError
:
function
(
jqxhr
,
response
)
{
this
.
renderError
(
response
.
responseText
);
this
.
enableButtons
();
},
handle
Regenerate
Error
:
function
(
jqxhr
)
{
handle
Generations
Error
:
function
(
jqxhr
)
{
// Since there are multiple "regenerate" buttons on the page,
// it's difficult to show the error message in the UI.
// Since this page is used only by internal staff, I think the
...
...
@@ -120,12 +154,20 @@
$
(
'.certificates-form'
).
submit
();
},
getUserQuery
:
function
()
{
return
$
(
'.certificates-form input[name="query"]'
).
val
();
getUserFilter
:
function
()
{
return
$
(
'.certificates-form > #certificate-user-filter-input'
).
val
();
},
setUserFilter
:
function
(
filter
)
{
$
(
'.certificates-form > #certificate-user-filter-input'
).
val
(
filter
);
},
getCourseFilter
:
function
()
{
return
$
(
'.certificates-form > #certificate-course-filter-input'
).
val
();
},
set
UserQuery
:
function
(
query
)
{
$
(
'.certificates-form
input[name="query"]'
).
val
(
query
);
set
CourseFilter
:
function
(
course_id
)
{
$
(
'.certificates-form
> #certificate-course-filter-input'
).
val
(
course_id
);
},
setResults
:
function
(
html
)
{
...
...
lms/djangoapps/support/static/support/templates/certificates.underscore
View file @
37aebaa7
<div class="certificates-search">
<form class="certificates-form">
<label class="sr" for="certificate-
query
-input"><%- gettext("Search") %></label>
<label class="sr" for="certificate-
user-filter
-input"><%- gettext("Search") %></label>
<input
id="certificate-
query
-input"
id="certificate-
user-filter
-input"
type="text"
name="query"
value=""
placeholder="<%- gettext("username or email") %>">
</input>
<input
id="certificate-course-filter-input"
type="text"
name="query"
value=""
placeholder="<%- gettext("course id") %>">
</input>
<input type="submit" value="<%- gettext("Search") %>" class="btn-disable-on-submit"></input>
</form>
</div>
...
...
lms/djangoapps/support/static/support/templates/certificates_results.underscore
View file @
37aebaa7
...
...
@@ -29,12 +29,21 @@
<td><%- cert.get("grade") %></td>
<td><%- cert.get("modified") %></td>
<td>
<% if (cert.get("regenerate")) { %>
<button
class="btn-cert-regenerate btn-disable-on-submit"
data-username="<%- cert.get("username") %>"
data-course-key="<%- cert.get("course_key") %>"
><%- gettext("Regenerate") %></button>
<span class="sr"><%- gettext("Regenerate the user's certificate") %></span>
<% } else { %>
<button
class="btn-cert-generate btn-disable-on-submit"
data-username="<%- cert.get("username") %>"
data-course-key="<%- cert.get("course_key") %>"
><%- gettext("Generate") %></button>
<span class="sr"><%- gettext("Generate the user's certificate") %></span>
<% } %>
</td>
</tr>
<% } %>
...
...
lms/djangoapps/support/tests/test_views.py
View file @
37aebaa7
...
...
@@ -9,7 +9,6 @@ import json
import
re
import
ddt
from
django.test
import
TestCase
from
django.core.urlresolvers
import
reverse
from
pytz
import
UTC
...
...
@@ -21,9 +20,10 @@ from student.roles import GlobalStaff, SupportStaffRole
from
student.tests.factories
import
UserFactory
,
CourseEnrollmentFactory
from
xmodule.modulestore.tests.django_utils
import
SharedModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
class
SupportViewTestCase
(
TestCase
):
class
SupportViewTestCase
(
ModuleStore
TestCase
):
"""
Base class for support view tests.
"""
...
...
@@ -36,6 +36,7 @@ class SupportViewTestCase(TestCase):
"""Create a user and log in. """
super
(
SupportViewTestCase
,
self
)
.
setUp
()
self
.
user
=
UserFactory
(
username
=
self
.
USERNAME
,
email
=
self
.
EMAIL
,
password
=
self
.
PASSWORD
)
self
.
course
=
CourseFactory
.
create
()
success
=
self
.
client
.
login
(
username
=
self
.
USERNAME
,
password
=
self
.
PASSWORD
)
self
.
assertTrue
(
success
,
msg
=
"Could not log in"
)
...
...
@@ -129,16 +130,23 @@ class SupportViewCertificatesTests(SupportViewTestCase):
super
(
SupportViewCertificatesTests
,
self
)
.
setUp
()
SupportStaffRole
()
.
add_users
(
self
.
user
)
def
test_certificates_no_
query
(
self
):
# Check that an empty initial
query
is passed to the JavaScript client correctly.
def
test_certificates_no_
filter
(
self
):
# Check that an empty initial
filter
is passed to the JavaScript client correctly.
response
=
self
.
client
.
get
(
reverse
(
"support:certificates"
))
self
.
assertContains
(
response
,
"user
Query
: ''"
)
self
.
assertContains
(
response
,
"user
Filter
: ''"
)
def
test_certificates_with_
query
(
self
):
# Check that an initial
query
is passed to the JavaScript client.
url
=
reverse
(
"support:certificates"
)
+
"?
query
=student@example.com"
def
test_certificates_with_
user_filter
(
self
):
# Check that an initial
filter
is passed to the JavaScript client.
url
=
reverse
(
"support:certificates"
)
+
"?
user
=student@example.com"
response
=
self
.
client
.
get
(
url
)
self
.
assertContains
(
response
,
"userQuery: 'student@example.com'"
)
self
.
assertContains
(
response
,
"userFilter: 'student@example.com'"
)
def
test_certificates_along_with_course_filter
(
self
):
# Check that an initial filter is passed to the JavaScript client.
url
=
reverse
(
"support:certificates"
)
+
"?user=student@example.com&course_id="
+
unicode
(
self
.
course
.
id
)
response
=
self
.
client
.
get
(
url
)
self
.
assertContains
(
response
,
"userFilter: 'student@example.com'"
)
self
.
assertContains
(
response
,
"courseFilter: '"
+
unicode
(
self
.
course
.
id
)
+
"'"
)
@ddt.ddt
...
...
lms/djangoapps/support/views/certificate.py
View file @
37aebaa7
...
...
@@ -30,6 +30,7 @@ class CertificatesSupportView(View):
def
get
(
self
,
request
):
"""Render the certificates support view. """
context
=
{
"user_query"
:
request
.
GET
.
get
(
"query"
,
""
)
"user_filter"
:
request
.
GET
.
get
(
"user"
,
""
),
"course_filter"
:
request
.
GET
.
get
(
"course_id"
,
""
)
}
return
render_to_response
(
"support/certificates.html"
,
context
)
lms/static/sass/views/_support.scss
View file @
37aebaa7
...
...
@@ -3,11 +3,14 @@
// ===================================================================
.certificates-search
,
.enrollment-search
{
margin
:
40px
0
;
input
[
name
=
"query"
]
{
width
:
476px
;
}
margin
:
40px
0
;
input
[
name
=
"query"
]
{
width
:
350px
;
}
.certificates-form
{
max-width
:
850px
;
margin
:
0
auto
;
}
}
...
...
@@ -31,6 +34,10 @@
font-size
:
12px
;
}
.btn-cert-generate
{
font-size
:
12px
;
}
.enrollment-modal-wrapper.is-shown
{
position
:
fixed
;
top
:
0
;
...
...
lms/templates/support/certificates.html
View file @
37aebaa7
...
...
@@ -9,7 +9,8 @@ from django.utils.translation import ugettext as _
<
%
block
name=
"js_extra"
>
<
%
static:require_module
module_name=
"support/js/certificates_factory"
class_name=
"CertificatesFactory"
>
new CertificatesFactory({
userQuery: '${ user_query }'
userFilter: '${ user_filter }',
courseFilter: '${course_filter}'
});
</
%
static:require
_module
>
</
%
block>
...
...
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