SOL-1492
This commit is contained in:
@@ -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(ModuleStoreTestCase):
|
||||
"""
|
||||
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)
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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'),
|
||||
userQuery: 'student@example.com'
|
||||
userFilter: '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, '');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.handleRegenerateError
|
||||
error: this.handleGenerationsError
|
||||
});
|
||||
},
|
||||
|
||||
@@ -102,12 +136,12 @@
|
||||
this.enableButtons();
|
||||
},
|
||||
|
||||
handleSearchError: function(jqxhr) {
|
||||
this.renderError(jqxhr.responseText);
|
||||
handleSearchError: function(jqxhr, response) {
|
||||
this.renderError(response.responseText);
|
||||
this.enableButtons();
|
||||
},
|
||||
|
||||
handleRegenerateError: function(jqxhr) {
|
||||
handleGenerationsError: 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();
|
||||
},
|
||||
|
||||
setUserQuery: function(query) {
|
||||
$('.certificates-form input[name="query"]').val(query);
|
||||
setUserFilter: function(filter) {
|
||||
$('.certificates-form > #certificate-user-filter-input').val(filter);
|
||||
},
|
||||
|
||||
getCourseFilter: function() {
|
||||
return $('.certificates-form > #certificate-course-filter-input').val();
|
||||
},
|
||||
|
||||
setCourseFilter: function(course_id) {
|
||||
$('.certificates-form > #certificate-course-filter-input').val(course_id);
|
||||
},
|
||||
|
||||
setResults: function(html) {
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
<% } %>
|
||||
|
||||
@@ -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(ModuleStoreTestCase):
|
||||
"""
|
||||
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, "userQuery: ''")
|
||||
self.assertContains(response, "userFilter: ''")
|
||||
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user