Commit 37aebaa7 by asadiqbal

SOL-1492

parent 7f6e8b88
...@@ -7,7 +7,6 @@ import json ...@@ -7,7 +7,6 @@ import json
import ddt import ddt
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
...@@ -22,7 +21,7 @@ FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy() ...@@ -22,7 +21,7 @@ FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy()
FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True
class CertificateSupportTestCase(TestCase): class CertificateSupportTestCase(ModuleStoreTestCase):
""" """
Base class for tests of the certificate support views. Base class for tests of the certificate support views.
""" """
...@@ -36,6 +35,9 @@ class CertificateSupportTestCase(TestCase): ...@@ -36,6 +35,9 @@ class CertificateSupportTestCase(TestCase):
STUDENT_PASSWORD = "student" STUDENT_PASSWORD = "student"
CERT_COURSE_KEY = CourseKey.from_string("edX/DemoX/Demo_Course") 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_GRADE = 0.89
CERT_STATUS = CertificateStatuses.downloadable CERT_STATUS = CertificateStatuses.downloadable
CERT_MODE = "verified" CERT_MODE = "verified"
...@@ -47,6 +49,11 @@ class CertificateSupportTestCase(TestCase): ...@@ -47,6 +49,11 @@ class CertificateSupportTestCase(TestCase):
Log in as the support team member. Log in as the support team member.
""" """
super(CertificateSupportTestCase, self).setUp() 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 # Create the support staff user
self.support = UserFactory( self.support = UserFactory(
...@@ -79,7 +86,7 @@ class CertificateSupportTestCase(TestCase): ...@@ -79,7 +86,7 @@ class CertificateSupportTestCase(TestCase):
@ddt.ddt @ddt.ddt
class CertificateSearchTests(ModuleStoreTestCase, CertificateSupportTestCase): class CertificateSearchTests(CertificateSupportTestCase):
""" """
Tests for the certificate search end-point used by the support team. Tests for the certificate search end-point used by the support team.
""" """
...@@ -137,14 +144,20 @@ class CertificateSearchTests(ModuleStoreTestCase, CertificateSupportTestCase): ...@@ -137,14 +144,20 @@ class CertificateSearchTests(ModuleStoreTestCase, CertificateSupportTestCase):
(CertificateSupportTestCase.STUDENT_EMAIL, True), (CertificateSupportTestCase.STUDENT_EMAIL, True),
("bar", False), ("bar", False),
("bar@example.com", 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 @ddt.unpack
def test_search(self, query, expect_result): def test_search(self, user_filter, expect_result, course_filter=None):
response = self._search(query) response = self._search(user_filter, course_filter)
if expect_result:
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
results = json.loads(response.content) results = json.loads(response.content)
self.assertEqual(len(results), 1 if expect_result else 0) self.assertEqual(len(results), 1)
else:
self.assertEqual(response.status_code, 400)
def test_results(self): def test_results(self):
response = self._search(self.STUDENT_USERNAME) response = self._search(self.STUDENT_USERNAME)
...@@ -184,14 +197,16 @@ class CertificateSearchTests(ModuleStoreTestCase, CertificateSupportTestCase): ...@@ -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. """ """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) return self.client.get(url)
@ddt.ddt @ddt.ddt
class CertificateRegenerateTests(ModuleStoreTestCase, CertificateSupportTestCase): class CertificateRegenerateTests(CertificateSupportTestCase):
""" """
Tests for the certificate regeneration end-point used by the support team. Tests for the certificate regeneration end-point used by the support team.
""" """
...@@ -308,3 +323,117 @@ class CertificateRegenerateTests(ModuleStoreTestCase, CertificateSupportTestCase ...@@ -308,3 +323,117 @@ class CertificateRegenerateTests(ModuleStoreTestCase, CertificateSupportTestCase
params["username"] = username params["username"] = username
return self.client.post(url, params) 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( ...@@ -27,8 +27,9 @@ urlpatterns = patterns(
# End-points used by student support # End-points used by student support
# The views in the lms/djangoapps/support use these end-points # The views in the lms/djangoapps/support use these end-points
# to retrieve certificate information and regenerate certificates. # 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'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. ...@@ -5,6 +5,7 @@ See lms/djangoapps/support for more details.
""" """
import logging import logging
import urllib
from functools import wraps from functools import wraps
from django.http import ( from django.http import (
...@@ -25,6 +26,8 @@ from student.models import User, CourseEnrollment ...@@ -25,6 +26,8 @@ from student.models import User, CourseEnrollment
from courseware.access import has_access from courseware.access import has_access
from util.json_request import JsonResponse from util.json_request import JsonResponse
from certificates import api 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__) log = logging.getLogger(__name__)
...@@ -46,11 +49,15 @@ def require_certificate_permission(func): ...@@ -46,11 +49,15 @@ def require_certificate_permission(func):
@require_GET @require_GET
@require_certificate_permission @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: Arguments:
request (HttpRequest): The request object. request (HttpRequest): The request object.
...@@ -59,7 +66,8 @@ def search_by_user(request): ...@@ -59,7 +66,8 @@ def search_by_user(request):
JsonResponse JsonResponse
Example Usage: 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 Response: 200 OK
Content-Type: application/json Content-Type: application/json
...@@ -77,27 +85,46 @@ def search_by_user(request): ...@@ -77,27 +85,46 @@ def search_by_user(request):
] ]
""" """
query = request.GET.get("query") user_filter = request.GET.get("user", "")
if not query: if not user_filter:
return JsonResponse([]) msg = _("user is not given.")
return HttpResponseBadRequest(msg)
try: 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: except User.DoesNotExist:
return JsonResponse([]) return HttpResponseBadRequest(_("user '{user}' does not exist").format(user=user_filter))
certificates = api.get_certificates_for_user(user.username) certificates = api.get_certificates_for_user(user.username)
for cert in certificates: for cert in certificates:
cert["course_key"] = unicode(cert["course_key"]) cert["course_key"] = unicode(cert["course_key"])
cert["created"] = cert["created"].isoformat() cert["created"] = cert["created"].isoformat()
cert["modified"] = cert["modified"].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) 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: Arguments:
params (QueryDict): Request parameters. params (QueryDict): Request parameters.
...@@ -149,7 +176,7 @@ def regenerate_certificate_for_user(request): ...@@ -149,7 +176,7 @@ def regenerate_certificate_for_user(request):
""" """
# Check the POST parameters, returning a 400 response if they're not valid. # 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: if response is not None:
return response return response
...@@ -186,3 +213,52 @@ def regenerate_certificate_for_user(request): ...@@ -186,3 +213,52 @@ def regenerate_certificate_for_user(request):
params["user"].id, params["course_key"] params["user"].id, params["course_key"]
) )
return HttpResponse(200) 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 @@ ...@@ -6,15 +6,24 @@
model: CertModel, model: CertModel,
initialize: function(options) { initialize: function(options) {
this.userQuery = options.userQuery || ''; this.userFilter = options.userFilter || '';
this.courseFilter = options.courseFilter || '';
}, },
setUserQuery: function(userQuery) { setUserFilter: function(userFilter) {
this.userQuery = userQuery; this.userFilter = userFilter;
},
setCourseFilter: function(courseFilter) {
this.courseFilter = courseFilter;
}, },
url: function() { 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([ ...@@ -9,7 +9,7 @@ define([
var view = null, var view = null,
SEARCH_RESULTS = [ REGENERATE_SEARCH_RESULTS = [
{ {
'username': 'student', 'username': 'student',
'status': 'notpassing', 'status': 'notpassing',
...@@ -18,7 +18,8 @@ define([ ...@@ -18,7 +18,8 @@ define([
'type': 'honor', 'type': 'honor',
'course_key': 'course-v1:edX+DemoX+Demo_Course', 'course_key': 'course-v1:edX+DemoX+Demo_Course',
'download_url': null, 'download_url': null,
'modified': '2015-08-06T19:47:07+00:00' 'modified': '2015-08-06T19:47:07+00:00',
'regenerate': true
}, },
{ {
'username': 'student', 'username': 'student',
...@@ -28,8 +29,23 @@ define([ ...@@ -28,8 +29,23 @@ define([
'type': 'verified', 'type': 'verified',
'course_key': 'edx/test/2015', 'course_key': 'edx/test/2015',
'download_url': 'http://www.example.com/certificate.pdf', '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() { getSearchResults = function() {
...@@ -49,19 +65,29 @@ define([ ...@@ -49,19 +65,29 @@ define([
return results; return results;
}, },
searchFor = function(query, requests, response) { searchFor = function(user_filter, course_filter, requests, response) {
// Enter the search term and submit // 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(); view.triggerSearch();
// Simulate a response from the server // 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); AjaxHelpers.respondWithJson(requests, response);
}, },
regenerateCerts = function(username, courseKey) { regenerateCerts = function(username, courseKey) {
var sel = '.btn-cert-regenerate[data-course-key="' + courseKey + '"]'; var sel = '.btn-cert-regenerate[data-course-key="' + courseKey + '"]';
$(sel).click(); $(sel).click();
},
generateCerts = function(username, courseKey) {
var sel = '.btn-cert-generate[data-course-key="' + courseKey + '"]';
$(sel).click();
}; };
beforeEach(function () { beforeEach(function () {
...@@ -80,35 +106,49 @@ define([ ...@@ -80,35 +106,49 @@ define([
var requests = AjaxHelpers.requests(this), var requests = AjaxHelpers.requests(this),
results = []; results = [];
searchFor('student@example.com', requests, SEARCH_RESULTS); searchFor('student@example.com', '', requests, REGENERATE_SEARCH_RESULTS);
results = getSearchResults(); results = getSearchResults();
// Expect that the results displayed on the page match the results // Expect that the results displayed on the page match the results
// returned by the server. // 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 // Check the first row of results
expect(results[0][0]).toEqual(SEARCH_RESULTS[0].course_key); expect(results[0][0]).toEqual(REGENERATE_SEARCH_RESULTS[0].course_key);
expect(results[0][1]).toEqual(SEARCH_RESULTS[0].type); expect(results[0][1]).toEqual(REGENERATE_SEARCH_RESULTS[0].type);
expect(results[0][2]).toEqual(SEARCH_RESULTS[0].status); expect(results[0][2]).toEqual(REGENERATE_SEARCH_RESULTS[0].status);
expect(results[0][3]).toContain('Not available'); expect(results[0][3]).toContain('Not available');
expect(results[0][4]).toEqual(SEARCH_RESULTS[0].grade); expect(results[0][4]).toEqual(REGENERATE_SEARCH_RESULTS[0].grade);
expect(results[0][5]).toEqual(SEARCH_RESULTS[0].modified); expect(results[0][5]).toEqual(REGENERATE_SEARCH_RESULTS[0].modified);
// Check the second row of results // Check the second row of results
expect(results[1][0]).toEqual(SEARCH_RESULTS[1].course_key); expect(results[1][0]).toEqual(REGENERATE_SEARCH_RESULTS[1].course_key);
expect(results[1][1]).toEqual(SEARCH_RESULTS[1].type); expect(results[1][1]).toEqual(REGENERATE_SEARCH_RESULTS[1].type);
expect(results[1][2]).toEqual(SEARCH_RESULTS[1].status); expect(results[1][2]).toEqual(REGENERATE_SEARCH_RESULTS[1].status);
expect(results[1][3]).toContain(SEARCH_RESULTS[1].download_url); expect(results[1][3]).toContain(REGENERATE_SEARCH_RESULTS[1].download_url);
expect(results[1][4]).toEqual(SEARCH_RESULTS[1].grade); expect(results[1][4]).toEqual(REGENERATE_SEARCH_RESULTS[1].grade);
expect(results[1][5]).toEqual(SEARCH_RESULTS[1].modified); 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() { it('searches for certificates and displays a message when there are no results', function() {
var requests = AjaxHelpers.requests(this), var requests = AjaxHelpers.requests(this),
results = []; results = [];
searchFor('student@example.com', requests, []); searchFor('student@example.com', '', requests, []);
results = getSearchResults(); results = getSearchResults();
// Expect that no results are found // Expect that no results are found
...@@ -118,30 +158,30 @@ define([ ...@@ -118,30 +158,30 @@ define([
expect($('.certificates-results').text()).toContain('No results'); 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), var requests = AjaxHelpers.requests(this),
results = []; results = [];
// Re-render the view, this time providing an initial query. // Re-render the view, this time providing an initial filter.
view = new CertificatesView({ view = new CertificatesView({
el: $('.certificates-content'), el: $('.certificates-content'),
userQuery: 'student@example.com' userFilter: 'student@example.com'
}).render(); }).render();
// Simulate a response from the server // Simulate a response from the server
AjaxHelpers.expectJsonRequest(requests, 'GET', '/certificates/search?query=student@example.com'); AjaxHelpers.expectJsonRequest(requests, 'GET', '/certificates/search?user=student@example.com');
AjaxHelpers.respondWithJson(requests, SEARCH_RESULTS); AjaxHelpers.respondWithJson(requests, REGENERATE_SEARCH_RESULTS);
// Check the search results // Check the search results
results = getSearchResults(); 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() { it('regenerates a certificate for a student', function() {
var requests = AjaxHelpers.requests(this); var requests = AjaxHelpers.requests(this);
// Trigger a search // 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 // Click the button to regenerate certificates for a user
regenerateCerts('student', 'course-v1:edX+DemoX+Demo_Course'); regenerateCerts('student', 'course-v1:edX+DemoX+Demo_Course');
...@@ -159,5 +199,29 @@ define([ ...@@ -159,5 +199,29 @@ define([
// Respond with success // Respond with success
AjaxHelpers.respondWithJson(requests, ''); 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 @@ ...@@ -12,24 +12,27 @@
return Backbone.View.extend({ return Backbone.View.extend({
events: { events: {
'submit .certificates-form': 'search', 'submit .certificates-form': 'search',
'click .btn-cert-regenerate': 'regenerateCertificate' 'click .btn-cert-regenerate': 'regenerateCertificate',
'click .btn-cert-generate': 'generateCertificate'
}, },
initialize: function(options) { initialize: function(options) {
_.bindAll(this, 'search', 'updateCertificates', 'regenerateCertificate', 'handleSearchError'); _.bindAll(this, 'search', 'updateCertificates', 'regenerateCertificate', 'handleSearchError');
this.certificates = new CertCollection({}); this.certificates = new CertCollection({});
this.initialQuery = options.userQuery || null; this.initialFilter = options.userFilter || null;
this.courseFilter = options.courseFilter || null;
}, },
render: function() { render: function() {
this.$el.html(_.template(certificatesTpl)); 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: // This is useful because it allows users to share search results:
// if the URL contains ?query="foo" then anyone who loads that URL // if the URL contains ?user_filter="foo"&course_id="xyz" then anyone who loads that URL
// will automatically search for "foo". // will automatically search for "foo" and course "xyz".
if (this.initialQuery) { if (this.initialFilter) {
this.setUserQuery(this.initialQuery); this.setUserFilter(this.initialFilter);
this.setCourseFilter(this.courseFilter);
this.triggerSearch(); this.triggerSearch();
} }
...@@ -38,7 +41,7 @@ ...@@ -38,7 +41,7 @@
renderResults: function() { renderResults: function() {
var context = { var context = {
certificates: this.certificates, certificates: this.certificates
}; };
this.setResults(_.template(resultsTpl, context)); this.setResults(_.template(resultsTpl, context));
...@@ -52,25 +55,56 @@ ...@@ -52,25 +55,56 @@
search: function(event) { search: function(event) {
// Fetch the certificate collection for the given user // Fetch the certificate collection for the given user
var query = this.getUserQuery(), var url = '/support/certificates?user=' + this.getUserFilter();
url = '/support/certificates?query=' + query;
//course id is optional.
if (this.getCourseFilter()) {
url += '&course_id=' + this.getCourseFilter();
}
// Prevent form submission, since we're handling it ourselves. // Prevent form submission, since we're handling it ourselves.
event.preventDefault(); 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 // That way, if the user reloads the page or sends someone the link
// then the same search will be performed on page load. // then the same search will be performed on page load.
window.history.pushState({}, window.document.title, url); window.history.pushState({}, window.document.title, url);
// Perform a search for the user's certificates. // Perform a search for the user's certificates.
this.disableButtons(); 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({ this.certificates.fetch({
success: this.updateCertificates, success: this.updateCertificates,
error: this.handleSearchError error: this.handleSearchError
}); });
}, },
error: this.handleGenerationsError
});
},
regenerateCertificate: function(event) { regenerateCertificate: function(event) {
var $button = $(event.target); var $button = $(event.target);
...@@ -84,16 +118,16 @@ ...@@ -84,16 +118,16 @@
type: 'POST', type: 'POST',
data: { data: {
username: $button.data('username'), username: $button.data('username'),
course_key: $button.data('course-key'), course_key: $button.data('course-key')
}, },
context: this, context: this,
success: function() { success: function() {
this.certificates.fetch({ this.certificates.fetch({
success: this.updateCertificates, success: this.updateCertificates,
error: this.handleSearchError, error: this.handleSearchError
}); });
}, },
error: this.handleRegenerateError error: this.handleGenerationsError
}); });
}, },
...@@ -102,12 +136,12 @@ ...@@ -102,12 +136,12 @@
this.enableButtons(); this.enableButtons();
}, },
handleSearchError: function(jqxhr) { handleSearchError: function(jqxhr, response) {
this.renderError(jqxhr.responseText); this.renderError(response.responseText);
this.enableButtons(); this.enableButtons();
}, },
handleRegenerateError: function(jqxhr) { handleGenerationsError: function(jqxhr) {
// Since there are multiple "regenerate" buttons on the page, // Since there are multiple "regenerate" buttons on the page,
// it's difficult to show the error message in the UI. // it's difficult to show the error message in the UI.
// Since this page is used only by internal staff, I think the // Since this page is used only by internal staff, I think the
...@@ -120,12 +154,20 @@ ...@@ -120,12 +154,20 @@
$('.certificates-form').submit(); $('.certificates-form').submit();
}, },
getUserQuery: function() { getUserFilter: function() {
return $('.certificates-form input[name="query"]').val(); 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();
}, },
setUserQuery: function(query) { setCourseFilter: function(course_id) {
$('.certificates-form input[name="query"]').val(query); $('.certificates-form > #certificate-course-filter-input').val(course_id);
}, },
setResults: function(html) { setResults: function(html) {
......
<div class="certificates-search"> <div class="certificates-search">
<form class="certificates-form"> <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 <input
id="certificate-query-input" id="certificate-user-filter-input"
type="text" type="text"
name="query" name="query"
value="" value=""
placeholder="<%- gettext("username or email") %>"> placeholder="<%- gettext("username or email") %>">
</input> </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> <input type="submit" value="<%- gettext("Search") %>" class="btn-disable-on-submit"></input>
</form> </form>
</div> </div>
......
...@@ -29,12 +29,21 @@ ...@@ -29,12 +29,21 @@
<td><%- cert.get("grade") %></td> <td><%- cert.get("grade") %></td>
<td><%- cert.get("modified") %></td> <td><%- cert.get("modified") %></td>
<td> <td>
<% if (cert.get("regenerate")) { %>
<button <button
class="btn-cert-regenerate btn-disable-on-submit" class="btn-cert-regenerate btn-disable-on-submit"
data-username="<%- cert.get("username") %>" data-username="<%- cert.get("username") %>"
data-course-key="<%- cert.get("course_key") %>" data-course-key="<%- cert.get("course_key") %>"
><%- gettext("Regenerate") %></button> ><%- gettext("Regenerate") %></button>
<span class="sr"><%- gettext("Regenerate the user's certificate") %></span> <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> </td>
</tr> </tr>
<% } %> <% } %>
......
...@@ -9,7 +9,6 @@ import json ...@@ -9,7 +9,6 @@ import json
import re import re
import ddt import ddt
from django.test import TestCase
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from pytz import UTC from pytz import UTC
...@@ -21,9 +20,10 @@ from student.roles import GlobalStaff, SupportStaffRole ...@@ -21,9 +20,10 @@ from student.roles import GlobalStaff, SupportStaffRole
from student.tests.factories import UserFactory, CourseEnrollmentFactory from student.tests.factories import UserFactory, CourseEnrollmentFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory 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. Base class for support view tests.
""" """
...@@ -36,6 +36,7 @@ class SupportViewTestCase(TestCase): ...@@ -36,6 +36,7 @@ class SupportViewTestCase(TestCase):
"""Create a user and log in. """ """Create a user and log in. """
super(SupportViewTestCase, self).setUp() super(SupportViewTestCase, self).setUp()
self.user = UserFactory(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD) 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) success = self.client.login(username=self.USERNAME, password=self.PASSWORD)
self.assertTrue(success, msg="Could not log in") self.assertTrue(success, msg="Could not log in")
...@@ -129,16 +130,23 @@ class SupportViewCertificatesTests(SupportViewTestCase): ...@@ -129,16 +130,23 @@ class SupportViewCertificatesTests(SupportViewTestCase):
super(SupportViewCertificatesTests, self).setUp() super(SupportViewCertificatesTests, self).setUp()
SupportStaffRole().add_users(self.user) SupportStaffRole().add_users(self.user)
def test_certificates_no_query(self): def test_certificates_no_filter(self):
# Check that an empty initial query is passed to the JavaScript client correctly. # Check that an empty initial filter is passed to the JavaScript client correctly.
response = self.client.get(reverse("support:certificates")) response = self.client.get(reverse("support:certificates"))
self.assertContains(response, "userQuery: ''") self.assertContains(response, "userFilter: ''")
def test_certificates_with_query(self): def test_certificates_with_user_filter(self):
# Check that an initial query is passed to the JavaScript client. # Check that an initial filter is passed to the JavaScript client.
url = reverse("support:certificates") + "?query=student@example.com" url = reverse("support:certificates") + "?user=student@example.com"
response = self.client.get(url) 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 @ddt.ddt
......
...@@ -30,6 +30,7 @@ class CertificatesSupportView(View): ...@@ -30,6 +30,7 @@ class CertificatesSupportView(View):
def get(self, request): def get(self, request):
"""Render the certificates support view. """ """Render the certificates support view. """
context = { 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) return render_to_response("support/certificates.html", context)
...@@ -4,9 +4,12 @@ ...@@ -4,9 +4,12 @@
.certificates-search, .enrollment-search { .certificates-search, .enrollment-search {
margin: 40px 0; margin: 40px 0;
input[name="query"] { input[name="query"] {
width: 476px; width: 350px;
}
.certificates-form {
max-width: 850px;
margin: 0 auto;
} }
} }
...@@ -31,6 +34,10 @@ ...@@ -31,6 +34,10 @@
font-size: 12px; font-size: 12px;
} }
.btn-cert-generate {
font-size: 12px;
}
.enrollment-modal-wrapper.is-shown { .enrollment-modal-wrapper.is-shown {
position: fixed; position: fixed;
top: 0; top: 0;
......
...@@ -9,7 +9,8 @@ from django.utils.translation import ugettext as _ ...@@ -9,7 +9,8 @@ from django.utils.translation import ugettext as _
<%block name="js_extra"> <%block name="js_extra">
<%static:require_module module_name="support/js/certificates_factory" class_name="CertificatesFactory"> <%static:require_module module_name="support/js/certificates_factory" class_name="CertificatesFactory">
new CertificatesFactory({ new CertificatesFactory({
userQuery: '${ user_query }' userFilter: '${ user_filter }',
courseFilter: '${course_filter}'
}); });
</%static:require_module> </%static:require_module>
</%block> </%block>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment