From 37aebaa73b3f5eda74b0e94c6eec3d2806ee62ca Mon Sep 17 00:00:00 2001 From: asadiqbal Date: Wed, 16 Dec 2015 19:23:59 +0500 Subject: [PATCH] SOL-1492 --- .../certificates/tests/test_support_views.py | 153 ++++++++++++++++-- lms/djangoapps/certificates/urls.py | 3 +- lms/djangoapps/certificates/views/support.py | 100 ++++++++++-- .../support/js/collections/certificate.js | 17 +- .../js/spec/views/certificates_spec.js | 120 ++++++++++---- .../static/support/js/views/certificates.js | 86 +++++++--- .../support/templates/certificates.underscore | 11 +- .../templates/certificates_results.underscore | 9 ++ lms/djangoapps/support/tests/test_views.py | 26 +-- lms/djangoapps/support/views/certificate.py | 3 +- lms/static/sass/views/_support.scss | 17 +- lms/templates/support/certificates.html | 3 +- 12 files changed, 451 insertions(+), 97 deletions(-) diff --git a/lms/djangoapps/certificates/tests/test_support_views.py b/lms/djangoapps/certificates/tests/test_support_views.py index b20c45db51..db3cd2d6d4 100644 --- a/lms/djangoapps/certificates/tests/test_support_views.py +++ b/lms/djangoapps/certificates/tests/test_support_views.py @@ -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) diff --git a/lms/djangoapps/certificates/urls.py b/lms/djangoapps/certificates/urls.py index beb2ca3b9b..74237e73f5 100644 --- a/lms/djangoapps/certificates/urls.py +++ b/lms/djangoapps/certificates/urls.py @@ -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"), ) diff --git a/lms/djangoapps/certificates/views/support.py b/lms/djangoapps/certificates/views/support.py index 801775ee8a..1c2fbc3e15 100644 --- a/lms/djangoapps/certificates/views/support.py +++ b/lms/djangoapps/certificates/views/support.py @@ -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) diff --git a/lms/djangoapps/support/static/support/js/collections/certificate.js b/lms/djangoapps/support/static/support/js/collections/certificate.js index b959ba6864..4d2260ad30 100644 --- a/lms/djangoapps/support/static/support/js/collections/certificate.js +++ b/lms/djangoapps/support/static/support/js/collections/certificate.js @@ -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; } }); }); diff --git a/lms/djangoapps/support/static/support/js/spec/views/certificates_spec.js b/lms/djangoapps/support/static/support/js/spec/views/certificates_spec.js index a4bf26909b..a4615ee8f6 100644 --- a/lms/djangoapps/support/static/support/js/spec/views/certificates_spec.js +++ b/lms/djangoapps/support/static/support/js/spec/views/certificates_spec.js @@ -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, ''); + }); + }); }); diff --git a/lms/djangoapps/support/static/support/js/views/certificates.js b/lms/djangoapps/support/static/support/js/views/certificates.js index 39e96f594c..898d13a5fa 100644 --- a/lms/djangoapps/support/static/support/js/views/certificates.js +++ b/lms/djangoapps/support/static/support/js/views/certificates.js @@ -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) { diff --git a/lms/djangoapps/support/static/support/templates/certificates.underscore b/lms/djangoapps/support/static/support/templates/certificates.underscore index e6f0ab69c6..b26e22d92d 100644 --- a/lms/djangoapps/support/static/support/templates/certificates.underscore +++ b/lms/djangoapps/support/static/support/templates/certificates.underscore @@ -1,14 +1,21 @@ diff --git a/lms/djangoapps/support/static/support/templates/certificates_results.underscore b/lms/djangoapps/support/static/support/templates/certificates_results.underscore index 72a4a43433..9ccd937239 100644 --- a/lms/djangoapps/support/static/support/templates/certificates_results.underscore +++ b/lms/djangoapps/support/static/support/templates/certificates_results.underscore @@ -29,12 +29,21 @@ <%- cert.get("grade") %> <%- cert.get("modified") %> + <% if (cert.get("regenerate")) { %> <%- gettext("Regenerate the user's certificate") %> + <% } else { %> + + <%- gettext("Generate the user's certificate") %> + <% } %> <% } %> diff --git a/lms/djangoapps/support/tests/test_views.py b/lms/djangoapps/support/tests/test_views.py index d0cc670f7e..5289e2d7d4 100644 --- a/lms/djangoapps/support/tests/test_views.py +++ b/lms/djangoapps/support/tests/test_views.py @@ -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 diff --git a/lms/djangoapps/support/views/certificate.py b/lms/djangoapps/support/views/certificate.py index 1f6fc23e0f..0cbdd13da1 100644 --- a/lms/djangoapps/support/views/certificate.py +++ b/lms/djangoapps/support/views/certificate.py @@ -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) diff --git a/lms/static/sass/views/_support.scss b/lms/static/sass/views/_support.scss index 310976a137..4d004625fc 100644 --- a/lms/static/sass/views/_support.scss +++ b/lms/static/sass/views/_support.scss @@ -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; diff --git a/lms/templates/support/certificates.html b/lms/templates/support/certificates.html index 15fa9b2139..8022e6ccd8 100644 --- a/lms/templates/support/certificates.html +++ b/lms/templates/support/certificates.html @@ -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}' });