From f4e1dd81d7a41027e6db7ecda01b500543899aa8 Mon Sep 17 00:00:00 2001 From: Bianca Severino Date: Thu, 25 Mar 2021 09:50:50 -0400 Subject: [PATCH] Roll out exam resume and grouped attempt features --- CHANGELOG.rst | 4 + edx_proctoring/__init__.py | 2 +- edx_proctoring/settings/common.py | 1 - .../proctored_exam_attempt_collection.js | 15 -- .../js/views/proctored_exam_attempt_view.js | 57 ++--- .../spec/proctored_exam_attempt_spec.js | 241 ++++-------------- ...student-proctored-exam-attempts.underscore | 197 -------------- edx_proctoring/tests/test_views.py | 202 +++++++-------- edx_proctoring/urls.py | 11 - edx_proctoring/views.py | 45 ---- npm-shrinkwrap.json | 76 +----- package.json | 2 +- 12 files changed, 168 insertions(+), 685 deletions(-) delete mode 100644 edx_proctoring/static/proctoring/js/collections/proctored_exam_attempt_collection.js delete mode 100644 edx_proctoring/static/proctoring/templates/student-proctored-exam-attempts.underscore diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fca423c1f07..875f75dec5f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,10 @@ Change Log Unreleased ~~~~~~~~~~ +[3.8.0] - 2021-03-31 +~~~~~~~~~~~~~~~~~~~~~ +* Remove exam resume waffle flag references and fully roll out exam resume and grouped attempt features. + [3.7.16] - 2021-03-30 ~~~~~~~~~~~~~~~~~~~~~ * Reduce time for ping interval from 120 to 30 seconds. diff --git a/edx_proctoring/__init__.py b/edx_proctoring/__init__.py index 9732d844dc8..9a5c9cc4897 100644 --- a/edx_proctoring/__init__.py +++ b/edx_proctoring/__init__.py @@ -3,6 +3,6 @@ """ # Be sure to update the version number in edx_proctoring/package.json -__version__ = '3.7.16' +__version__ = '3.8.0' default_app_config = 'edx_proctoring.apps.EdxProctoringConfig' # pylint: disable=invalid-name diff --git a/edx_proctoring/settings/common.py b/edx_proctoring/settings/common.py index f8ab2470924..b04e79fef77 100644 --- a/edx_proctoring/settings/common.py +++ b/edx_proctoring/settings/common.py @@ -19,7 +19,6 @@ def plugin_settings(settings): 'proctoring/js/models/proctored_exam_model.js', 'proctoring/js/models/learner_onboarding_model.js', 'proctoring/js/collections/proctored_exam_allowance_collection.js', - 'proctoring/js/collections/proctored_exam_attempt_collection.js', 'proctoring/js/collections/proctored_exam_attempt_grouped_collection.js', 'proctoring/js/collections/proctored_exam_onboarding_collection.js', 'proctoring/js/collections/proctored_exam_collection.js', diff --git a/edx_proctoring/static/proctoring/js/collections/proctored_exam_attempt_collection.js b/edx_proctoring/static/proctoring/js/collections/proctored_exam_attempt_collection.js deleted file mode 100644 index f76ac3bb5cd..00000000000 --- a/edx_proctoring/static/proctoring/js/collections/proctored_exam_attempt_collection.js +++ /dev/null @@ -1,15 +0,0 @@ -edx = edx || {}; -(function(Backbone) { - 'use strict'; - - edx.instructor_dashboard = edx.instructor_dashboard || {}; - edx.instructor_dashboard.proctoring = edx.instructor_dashboard.proctoring || {}; - - edx.instructor_dashboard.proctoring.ProctoredExamAttemptCollection = Backbone.Collection.extend({ - /* model for a collection of ProctoredExamAttempt */ - model: edx.instructor_dashboard.proctoring.ProctoredExamAttemptModel, - url: '/api/edx_proctoring/v1/proctored_exam/attempt/course_id/' - }); - this.edx.instructor_dashboard.proctoring.ProctoredExamAttemptCollection = - edx.instructor_dashboard.proctoring.ProctoredExamAttemptCollection; -}).call(this, Backbone); diff --git a/edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js b/edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js index 26e81a981ef..1acbd231f10 100644 --- a/edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js +++ b/edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js @@ -45,21 +45,12 @@ edx = edx || {}; edx.instructor_dashboard.proctoring.ProctoredExamAttemptView = Backbone.View.extend({ initialize: function() { this.setElement($('.student-proctored-exam-container')); - this.enable_exam_resume_proctoring_improvements = - this.$el.data('enable-exam-resume-proctoring-improvements'); - this.enable_exam_resume_proctoring_improvements = this.enable_exam_resume_proctoring_improvements && - this.enable_exam_resume_proctoring_improvements.toLowerCase() === 'true'; this.course_id = this.$el.data('course-id'); this.template = null; this.model = new edx.instructor_dashboard.proctoring.ProctoredExamAttemptModel(); - if (!this.enable_exam_resume_proctoring_improvements) { - this.collection = new edx.instructor_dashboard.proctoring.ProctoredExamAttemptCollection(); - this.template_url = '/static/proctoring/templates/student-proctored-exam-attempts.underscore'; - } else { - this.collection = new edx.instructor_dashboard.proctoring.ProctoredExamAttemptGroupedCollection(); - this.template_url = '/static/proctoring/templates/student-proctored-exam-attempts-grouped.underscore'; - } + this.collection = new edx.instructor_dashboard.proctoring.ProctoredExamAttemptGroupedCollection(); + this.template_url = '/static/proctoring/templates/student-proctored-exam-attempts-grouped.underscore'; this.initial_url = this.collection.url; @@ -232,35 +223,21 @@ edx = edx || {}; self.model.url = this.attempt_url + attemptId; - if (!self.enable_exam_resume_proctoring_improvements) { - self.model.fetch({ - headers: { - 'X-CSRFToken': this.getCSRFToken() - }, - type: 'DELETE', - success: function() { - // fetch the attempts again. - self.hydrate(); - $('body').css('cursor', 'auto'); - } - }); - } else { - // call reset endpoint that can be used to delete all attempts for a given - // user and exam - $.ajax({ - url: '/api/edx_proctoring/v1/proctored_exam/exam_id/' + examId + - '/user_id/' + userId + '/reset_attempts', - type: 'DELETE', - headers: { - 'X-CSRFToken': this.getCSRFToken() - }, - success: function() { - // fetch the attempts again. - self.hydrate(); - $('body').css('cursor', 'auto'); - } - }); - } + // call reset endpoint that can be used to delete all attempts for a given + // user and exam + $.ajax({ + url: '/api/edx_proctoring/v1/proctored_exam/exam_id/' + examId + + '/user_id/' + userId + '/reset_attempts', + type: 'DELETE', + headers: { + 'X-CSRFToken': this.getCSRFToken() + }, + success: function() { + // fetch the attempts again. + self.hydrate(); + $('body').css('cursor', 'auto'); + } + }); }, onResumeAttempt: function(event) { var $target, attemptId, userId; diff --git a/edx_proctoring/static/proctoring/spec/proctored_exam_attempt_spec.js b/edx_proctoring/static/proctoring/spec/proctored_exam_attempt_spec.js index fb8f2f2ffe9..8d1694045fe 100644 --- a/edx_proctoring/static/proctoring/spec/proctored_exam_attempt_spec.js +++ b/edx_proctoring/static/proctoring/spec/proctored_exam_attempt_spec.js @@ -2,7 +2,6 @@ describe('ProctoredExamAttemptView', function() { 'use strict'; var html = ''; - var groupedHtml = ''; var deletedProctoredExamAttemptJson = [{ attempt_url: '/api/edx_proctoring/v1/proctored_exam/attempt/course_id/edX/DemoX/Demo_Course', proctored_exam_attempts: [], @@ -52,9 +51,41 @@ describe('ProctoredExamAttemptView', function() { time_limit_mins: 1 }, user: { + id: 1, username: 'testuser1', email: 'testuser1@test.com' - } + }, + all_attempts: [{ + allowed_time_limit_mins: 1, + attempt_code: '20C32387-372E-48BD-BCAC-A2BE9DC91E09', + completed_at: null, + created: '2015-08-10T09:15:45Z', + external_id: '40eceb15-bcc3-4791-b43f-4e843afb7ae8', + id: 43, + is_sample_attempt: false, + last_poll_ipaddr: null, + last_poll_timestamp: null, + modified: '2015-08-10T09:15:45Z', + started_at: '2015-08-10T09:15:45Z', + status: status, + taking_as_proctored: true, + proctored_exam: { + content_id: 'i4x://edX/DemoX/sequential/9f5e9b018a244ea38e5d157e0019e60c', + course_id: 'edX/DemoX/Demo_Course', + exam_name: 'Normal Exam', + external_id: null, + id: 17, + is_active: true, + is_practice_exam: isPracticeExam, + is_proctored: true, + time_limit_mins: 1 + }, + user: { + id: 1, + username: 'testuser1', + email: 'testuser1@test.com' + } + }] }] }] ); @@ -212,104 +243,6 @@ describe('ProctoredExamAttemptView', function() { '{attempt_url: attempt_url, count: pagination_info.current_page + 1}, true) %>' + '" > <% }%>
' + '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '<% if (is_proctored_attempts) { %>' + - '' + - '<% _.each(proctored_exam_attempts, function(proctored_exam_attempt, dashboard_index){' + - '%>' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - ' <% }); %> ' + - '' + - ' <% } %>' + - '
UsernameExam NameAllowed Time (Minutes)Started AtCompleted AtStatusActions
' + - ' <%= proctored_exam_attempt.user.username %> ' + - ' ' + - ' <%- interpolate(gettext(" %(exam_display_name)s "),' + - '{ exam_display_name: proctored_exam_attempt.proctored_exam.exam_name }, true) %>' + - '' + - ' <%= proctored_exam_attempt.allowed_time_limit_mins %> ' + - '' + - ' <%= getDateFormat(proctored_exam_attempt.started_at) %>' + - '' + - ' <%= getDateFormat(proctored_exam_attempt.completed_at) %>' + - '' + - ' <% if (proctored_exam_attempt.status){ %> <%= proctored_exam_attempt.status %> <% } else { %> N/A <% } %> ' + - '' + - '<% if (proctored_exam_attempt.status){ %> ' + - '[x]' + - ' <% } else { %>N/A <% } %>' + - '
' + - '<% if (!is_proctored_attempts) { %> ' + - '

No exam results found.

' + - '<% } %> ' + - ' '; - this.server = sinon.fakeServer.create(); - this.server.autoRespond = true; - setFixtures('
'); - - // load the underscore template response before calling the proctored exam attemp view. - this.server.respondWith('GET', '/static/proctoring/templates/student-proctored-exam-attempts.underscore', - [ - 200, - {'Content-Type': 'text/html'}, - html - ] - ); - - groupedHtml = '
' + - '<% var is_proctored_attempts = proctored_exam_attempts.length !== 0 %>' + - '
' + - '
' + - '
' + - ' value="<%= searchText %>" <%} %> /> ' + - '' + - '' + - '
' + - '' + - '
' + - '
' + - '' + - '
' + - '
' + - '' + '' + '' + '' + @@ -329,9 +262,7 @@ describe('ProctoredExamAttemptView', function() { '<% if (proctored_exam_attempt.all_attempts.length > 1) { %>' + 'tabindex=0 <% } %>' + '>' + - '' + + '' + ''); + expect(this.proctored_exam_attempt_view.$el.find('tr.allowance-items')).toContainHtml(''); expect(this.proctored_exam_attempt_view.$el.find('tr.allowance-items').html()).toContain('Normal Exam'); }); - it('should delete the proctored exam attempt', function() { - this.server.respondWith('GET', '/api/edx_proctoring/v1/proctored_exam/attempt/course_id/test_course_id', - [ - 200, - { - 'Content-Type': 'application/json' - }, - JSON.stringify(getExpectedProctoredExamAttemptWithAttemptStatusJson('started')) - ] - ); - this.proctored_exam_attempt_view = new edx.instructor_dashboard.proctoring.ProctoredExamAttemptView(); - - // Process all requests so far - this.server.respond(); - this.server.respond(); - - expect(this.proctored_exam_attempt_view.$el.find('tr.allowance-items')).toContainHtml(''); - expect(this.proctored_exam_attempt_view.$el.find('tr.allowance-items').html()).toContain('Normal Exam'); - - // delete the proctored exam attempt - this.server.respondWith('DELETE', '/api/edx_proctoring/v1/proctored_exam/attempt/43', - [ - 200, - { - 'Content-Type': 'application/json' - }, - JSON.stringify([]) - ] - ); - - - // again fetch the results after the proctored exam attempt deletion - this.server.respondWith('GET', '/api/edx_proctoring/v1/proctored_exam/attempt/course_id/test_course_id', - [ - 200, - { - 'Content-Type': 'application/json' - }, - JSON.stringify(deletedProctoredExamAttemptJson) - ] - ); - - spyOn(window, 'confirm').and.callFake(function() { - return true; - }); - - // trigger the remove attempt event. - spyOnEvent('.remove-attempt', 'click'); - $('.remove-attempt').trigger('click'); - - // process the deleted attempt requests. - this.server.respond(); - this.server.respond(); - - expect(this.proctored_exam_attempt_view.$el.find('tr.allowance-items').html()).not.toContain('testuser1'); - expect(this.proctored_exam_attempt_view.$el.find('tr.allowance-items').html()).not.toContain('Normal Exam'); - }); - it('should search for the proctored exam attempt', function() { var searchText = 'testuser1'; - this.server.respondWith('GET', '/api/edx_proctoring/v1/proctored_exam/attempt/course_id/test_course_id', + this.server.respondWith('GET', '/api/edx_proctoring/v1/proctored_exam/attempt/grouped/course_id/test_course_id', [ 200, { @@ -532,7 +408,7 @@ describe('ProctoredExamAttemptView', function() { this.server.respond(); this.server.respond(); - expect(this.proctored_exam_attempt_view.$el.find('tr.allowance-items')).toContainHtml(''); + expect(this.proctored_exam_attempt_view.$el.find('tr.allowance-items')).toContainHtml(''); expect(this.proctored_exam_attempt_view.$el.find('tr.allowance-items').html()).toContain('Normal Exam'); expect(this.proctored_exam_attempt_view.$el.find('#attempt-search-indicator').hasClass('hidden')) .toEqual(false); @@ -544,7 +420,7 @@ describe('ProctoredExamAttemptView', function() { // search for the proctored exam attempt this.server.respondWith( 'GET', - '/api/edx_proctoring/v1/proctored_exam/attempt/course_id/test_course_id/search/' + searchText, + '/api/edx_proctoring/v1/proctored_exam/attempt/grouped/course_id/test_course_id/search/' + searchText, [ 200, { @@ -579,7 +455,7 @@ describe('ProctoredExamAttemptView', function() { it('should clear the search for the proctored exam attempt', function() { var searchText = 'invalid_search_text'; - this.server.respondWith('GET', '/api/edx_proctoring/v1/proctored_exam/attempt/course_id/test_course_id', + this.server.respondWith('GET', '/api/edx_proctoring/v1/proctored_exam/attempt/grouped/course_id/test_course_id', [ 200, { @@ -594,7 +470,7 @@ describe('ProctoredExamAttemptView', function() { this.server.respond(); this.server.respond(); - expect(this.proctored_exam_attempt_view.$el.find('tr.allowance-items')).toContainHtml(''); + expect(this.proctored_exam_attempt_view.$el.find('tr.allowance-items')).toContainHtml(''); expect(this.proctored_exam_attempt_view.$el.find('tr.allowance-items').html()).toContain('Normal Exam'); $('#search_attempt_id').val(searchText); @@ -602,7 +478,7 @@ describe('ProctoredExamAttemptView', function() { // search the proctored exam attempt this.server.respondWith( 'GET', - '/api/edx_proctoring/v1/proctored_exam/attempt/course_id/test_course_id/search/' + searchText, + '/api/edx_proctoring/v1/proctored_exam/attempt/grouped/course_id/test_course_id/search/' + searchText, [ 200, { @@ -624,7 +500,7 @@ describe('ProctoredExamAttemptView', function() { expect(this.proctored_exam_attempt_view.$el.find('tr.allowance-items').html()).not.toContain('Normal Exam'); - this.server.respondWith('GET', '/api/edx_proctoring/v1/proctored_exam/attempt/course_id/test_course_id', + this.server.respondWith('GET', '/api/edx_proctoring/v1/proctored_exam/attempt/grouped/course_id/test_course_id', [ 200, { @@ -646,10 +522,6 @@ describe('ProctoredExamAttemptView', function() { expect(this.proctored_exam_attempt_view.$el.find('tr.allowance-items').html()).toContain('Normal Exam'); }); it('should mark exam attempt "ready_to_resume" on resume', function() { - // enable the dropdown via the enable-exam-resume-proctoring-improvements data attribute - setFixtures('
'); - this.server.respondWith('GET', '/api/edx_proctoring/v1/proctored_exam/attempt/grouped/course_id/test_course_id', [ 200, @@ -728,10 +600,6 @@ describe('ProctoredExamAttemptView', function() { }); it('should not display actions dropdown for practice exam attempts', function() { - // enable the dropdown via the enable-exam-resume-proctoring-improvements data attribute - setFixtures('
'); - this.server.respondWith('GET', '/api/edx_proctoring/v1/proctored_exam/attempt/grouped/course_id/test_course_id', [ 200, @@ -758,9 +626,6 @@ describe('ProctoredExamAttemptView', function() { it('should display grouped attempts', function() { var rows; - setFixtures('
'); - this.server.respondWith('GET', '/api/edx_proctoring/v1/proctored_exam/attempt/grouped/course_id/test_course_id', [ 200, @@ -792,10 +657,7 @@ describe('ProctoredExamAttemptView', function() { expect(rows[2].outerHTML).not.toContain('action-more'); }); - it('deletes attempts using new endpoint', function() { - setFixtures('
'); - + it('deletes attempts', function() { this.server.respondWith('GET', '/api/edx_proctoring/v1/proctored_exam/attempt/grouped/course_id/test_course_id', [ 200, @@ -865,9 +727,6 @@ describe('ProctoredExamAttemptView', function() { }); it('shows and hides accordion when toggled', function() { - setFixtures('
'); - this.server.respondWith('GET', '/api/edx_proctoring/v1/proctored_exam/attempt/grouped/course_id/test_course_id', [ 200, @@ -896,8 +755,6 @@ describe('ProctoredExamAttemptView', function() { it('searches and shows spinner for grouped attempts', function() { var searchText = 'testuser1'; - setFixtures('
'); this.server.respondWith('GET', '/api/edx_proctoring/v1/proctored_exam/attempt/grouped/course_id/test_course_id', [ diff --git a/edx_proctoring/static/proctoring/templates/student-proctored-exam-attempts.underscore b/edx_proctoring/static/proctoring/templates/student-proctored-exam-attempts.underscore deleted file mode 100644 index 15028e7f319..00000000000 --- a/edx_proctoring/static/proctoring/templates/student-proctored-exam-attempts.underscore +++ /dev/null @@ -1,197 +0,0 @@ -
- - <% var is_proctored_attempts = proctored_exam_attempts.length !== 0 %> -
-
-
- - value="<%= searchText %>" - <%} %> - /> - - -
- -
-
- -
-
    - <% if (!pagination_info.has_previous){ %> -
  • - - - -
  • - <% } else { %> -
  • - - - -
  • - <% }%> - <% for(var n = start_page; n <= end_page; n++) { %> -
  • - <%= n %> - -
  • - <% } %> - <% if (!pagination_info.has_next){ %> -
  • - - - -
  • - <% } else { %> -
  • - - - -
  • - <% }%> -
-
-
-
UsernameExam Name
' + '<% if (proctored_exam_attempt.all_attempts.length > 1) { %>' + '' + @@ -421,15 +352,18 @@ describe('ProctoredExamAttemptView', function() { '<% } %>' + '' + ''; + this.server = sinon.fakeServer.create(); + this.server.autoRespond = true; + setFixtures('
'); - // load the underscore template response before calling the proctored exam attemp view. + // load the underscore template response before calling the proctored exam attempt view. this.server.respondWith( 'GET', '/static/proctoring/templates/student-proctored-exam-attempts-grouped.underscore', [ 200, {'Content-Type': 'text/html'}, - groupedHtml + html ] ); }); @@ -438,7 +372,7 @@ describe('ProctoredExamAttemptView', function() { this.server.restore(); }); it('should render the proctored exam attempt view properly', function() { - this.server.respondWith('GET', '/api/edx_proctoring/v1/proctored_exam/attempt/course_id/test_course_id', + this.server.respondWith('GET', '/api/edx_proctoring/v1/proctored_exam/attempt/grouped/course_id/test_course_id', [ 200, { @@ -452,71 +386,13 @@ describe('ProctoredExamAttemptView', function() { this.server.respond(); this.server.respond(); - expect(this.proctored_exam_attempt_view.$el.find('tr.allowance-items')).toContainHtml('
testuser1 testuser1 testuser1 testuser1 testuser1 testuser1 testuser1
- - - - - - - - - - - - - <% if (is_proctored_attempts) { %> - - <% _.each(proctored_exam_attempts, function(proctored_exam_attempt, dashboard_index){ %> - - - - - - - - - - - <% }); %> - - <% } %> -
<%- gettext("Username") %><%- gettext("Exam Name") %><%- gettext("Time Limit") %> <%- gettext("Type") %> <%- gettext("Started At") %><%- gettext("Completed At") %> <%- gettext("Status") %> <%- gettext("Actions") %>
- <%- interpolate(gettext(' %(username)s '), { username: proctored_exam_attempt.user.username }, true) %> - - <%- interpolate(gettext(' %(exam_display_name)s '), { exam_display_name: proctored_exam_attempt.proctored_exam.exam_name }, true) %> - <%= proctored_exam_attempt.allowed_time_limit_mins %> <%= proctored_exam_attempt.exam_attempt_type %> <%= getDateFormat(proctored_exam_attempt.started_at) %> <%= getDateFormat(proctored_exam_attempt.completed_at) %> - <% if (proctored_exam_attempt.status){ %> - <%= getExamAttemptStatus(proctored_exam_attempt.status) %> - <% } else { %> - N/A - <% } %> - - <% if (proctored_exam_attempt.status){ %> - [x] - <% } else { %> - N/A - <% } %> -
- <% if (!is_proctored_attempts) { %> -

No exam results found.

- <% } %> - -
-
diff --git a/edx_proctoring/tests/test_views.py b/edx_proctoring/tests/test_views.py index aef4ba53afb..464c30a869d 100644 --- a/edx_proctoring/tests/test_views.py +++ b/edx_proctoring/tests/test_views.py @@ -2341,8 +2341,7 @@ def test_attempt_ping_failure(self, mocked_switch_is_active): attempt = get_exam_attempt_by_id(attempt_id) self.assertEqual(attempt['status'], attempt_initial_status) - @ddt.data('edx_proctoring:proctored_exam.attempts.course', 'edx_proctoring:proctored_exam.attempts.grouped.course') - def test_get_exam_attempts(self, url_name): + def test_get_exam_attempts(self): """ Test to get the exam attempts in a course. """ @@ -2362,7 +2361,10 @@ def test_get_exam_attempts(self, url_name): reverse('edx_proctoring:proctored_exam.attempt.collection'), attempt_data ) - url = reverse(url_name, kwargs={'course_id': proctored_exam.course_id}) + url = reverse( + 'edx_proctoring:proctored_exam.attempts.grouped.course', + kwargs={'course_id': proctored_exam.course_id}, + ) self.assertEqual(response.status_code, 200) response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -2380,8 +2382,7 @@ def test_get_exam_attempts(self, url_name): response_data = json.loads(response.content.decode('utf-8')) self.assertEqual(len(response_data['proctored_exam_attempts']), 1) - @ddt.data('edx_proctoring:proctored_exam.attempts.course', 'edx_proctoring:proctored_exam.attempts.grouped.course') - def test_exam_attempts_not_global_staff(self, url_name): + def test_exam_attempts_not_global_staff(self): """ Test to get both timed and proctored exam attempts in a course as a course staff @@ -2423,7 +2424,10 @@ def test_exam_attempts_not_global_staff(self, url_name): reverse('edx_proctoring:proctored_exam.attempt.collection'), attempt_data ) - url = reverse(url_name, kwargs={'course_id': proctored_exam.course_id}) + url = reverse( + 'edx_proctoring:proctored_exam.attempts.grouped.course', + kwargs={'course_id': proctored_exam.course_id}, + ) self.user.is_staff = False self.user.save() @@ -2443,8 +2447,7 @@ def test_exam_attempts_not_global_staff(self, url_name): timed_exam.is_proctored ) - @ddt.data('edx_proctoring:proctored_exam.attempts.search', 'edx_proctoring:proctored_exam.attempts.grouped.search') - def test_get_filtered_exam_attempts(self, url_name): + def test_get_filtered_exam_attempts(self): """ Test to get the exam attempts in a course. """ @@ -2479,7 +2482,7 @@ def test_get_filtered_exam_attempts(self, url_name): self.client.login_user(self.user) response = self.client.get( reverse( - url_name, + 'edx_proctoring:proctored_exam.attempts.grouped.search', kwargs={ 'course_id': proctored_exam.course_id, 'search_by': 'tester' @@ -2498,8 +2501,7 @@ def test_get_filtered_exam_attempts(self, url_name): self.assertEqual(attempt['proctored_exam']['id'], proctored_exam.id) self.assertEqual(attempt['user']['id'], self.user.id) - @ddt.data('edx_proctoring:proctored_exam.attempts.search', 'edx_proctoring:proctored_exam.attempts.grouped.search') - def test_get_filtered_timed_exam_attempts(self, url_name): # pylint: disable=invalid-name + def test_get_filtered_timed_exam_attempts(self): # pylint: disable=invalid-name """ Test to get the filtered timed exam attempts in a course. """ @@ -2537,7 +2539,7 @@ def test_get_filtered_timed_exam_attempts(self, url_name): # pylint: disable=in self.client.login_user(self.user) response = self.client.get( reverse( - url_name, + 'edx_proctoring:proctored_exam.attempts.grouped.search', kwargs={ 'course_id': timed_exm.course_id, 'search_by': 'tester' @@ -2556,8 +2558,7 @@ def test_get_filtered_timed_exam_attempts(self, url_name): # pylint: disable=in self.assertEqual(attempt['proctored_exam']['id'], timed_exm.id) self.assertEqual(attempt['user']['id'], self.user.id) - @ddt.data('edx_proctoring:proctored_exam.attempts.course', 'edx_proctoring:proctored_exam.attempts.grouped.course') - def test_paginated_exam_attempts(self, url_name): + def test_paginated_exam_attempts(self): """ Test to get the paginated exam attempts in a course. """ @@ -2581,10 +2582,8 @@ def test_paginated_exam_attempts(self, url_name): self.client.login_user(self.user) response = self.client.get( reverse( - url_name, - kwargs={ - 'course_id': proctored_exam.course_id - } + 'edx_proctoring:proctored_exam.attempts.grouped.course', + kwargs={'course_id': proctored_exam.course_id}, ) ) self.assertEqual(response.status_code, 200) @@ -2595,6 +2594,78 @@ def test_paginated_exam_attempts(self, url_name): self.assertEqual(response_data['pagination_info']['total_pages'], 4) self.assertEqual(response_data['pagination_info']['current_page'], 1) + def test_get_grouped_exam_attempts(self): + """ + Test to ensure that if there are multiple attempts on the same exam, they are grouped by user + """ + course_id = 'a/b/c' + + exam_id_1 = create_exam( + course_id=course_id, + content_id='content', + exam_name='Sample Exam', + time_limit_mins=10, + is_proctored=True + ) + + exam_id_2 = create_exam( + course_id=course_id, + content_id='content2', + exam_name='Sample Exam', + time_limit_mins=10, + is_proctored=True + ) + # create two attempts each for exam 1 + attempt_1 = create_exam_attempt(exam_id_1, self.user.id, taking_as_proctored=True) + update_attempt_status(attempt_1, ProctoredExamStudentAttemptStatus.error) + update_attempt_status(attempt_1, ProctoredExamStudentAttemptStatus.ready_to_resume) + attempt_2 = create_exam_attempt(exam_id_1, self.user.id, taking_as_proctored=True) + + attempt_3 = create_exam_attempt(exam_id_1, self.second_user.id, taking_as_proctored=True) + update_attempt_status(attempt_3, ProctoredExamStudentAttemptStatus.error) + update_attempt_status(attempt_3, ProctoredExamStudentAttemptStatus.ready_to_resume) + attempt_4 = create_exam_attempt(exam_id_1, self.second_user.id, taking_as_proctored=True) + + # create one attempt each for exam 2 + attempt_5 = create_exam_attempt(exam_id_2, self.user.id, taking_as_proctored=True) + attempt_6 = create_exam_attempt(exam_id_2, self.second_user.id, taking_as_proctored=True) + + # check that endpoint returns only four attempts, unique to user and exam + url = reverse( + 'edx_proctoring:proctored_exam.attempts.grouped.course', + kwargs={'course_id': course_id}, + ) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + response_data = json.loads(response.content.decode('utf-8')) + self.assertEqual(len(response_data['proctored_exam_attempts']), 4) + + # check that each attempt returned is structured as expected + # order of attempts should be sorted by most recently created + first_attempt = response_data['proctored_exam_attempts'][0] + self.assertEqual(first_attempt['user']['id'], self.second_user.id) + self.assertEqual(len(first_attempt['all_attempts']), 1) + self.assertEqual(first_attempt['all_attempts'][0]['id'], attempt_6) + + second_attempt = response_data['proctored_exam_attempts'][1] + self.assertEqual(second_attempt['user']['id'], self.user.id) + self.assertEqual(len(second_attempt['all_attempts']), 1) + self.assertEqual(second_attempt['all_attempts'][0]['id'], attempt_5) + + third_attempt = response_data['proctored_exam_attempts'][2] + self.assertEqual(third_attempt['user']['id'], self.second_user.id) + self.assertEqual(third_attempt['id'], attempt_4) + self.assertEqual(len(third_attempt['all_attempts']), 2) + self.assertEqual(third_attempt['all_attempts'][0]['id'], attempt_4) + self.assertEqual(third_attempt['all_attempts'][1]['id'], attempt_3) + + fourth_attempt = response_data['proctored_exam_attempts'][3] + self.assertEqual(fourth_attempt['user']['id'], self.user.id) + self.assertEqual(fourth_attempt['id'], attempt_2) + self.assertEqual(len(fourth_attempt['all_attempts']), 2) + self.assertEqual(fourth_attempt['all_attempts'][0]['id'], attempt_2) + self.assertEqual(fourth_attempt['all_attempts'][1]['id'], attempt_1) + def test_stop_others_attempt(self): """ Start an exam (create an exam attempt) @@ -3708,14 +3779,17 @@ def test_resume_exam_attempt(self): response_data = json.loads(response.content.decode('utf-8')) # GET both created attempts - url = reverse('edx_proctoring:proctored_exam.attempts.course', kwargs={'course_id': proctored_exam.course_id}) + url = reverse( + 'edx_proctoring:proctored_exam.attempts.grouped.course', + kwargs={'course_id': proctored_exam.course_id}, + ) response = self.client.get(url) self.assertEqual(response.status_code, 200) response_data = json.loads(response.content.decode('utf-8')) - self.assertEqual(len(response_data['proctored_exam_attempts']), 2) + self.assertEqual(len(response_data['proctored_exam_attempts'][0]['all_attempts']), 2) # assert that the older attempt has transitioned to the 'resumed' status self.assertEqual( - response_data['proctored_exam_attempts'][1]['status'], + response_data['proctored_exam_attempts'][0]['all_attempts'][1]['status'], ProctoredExamStudentAttemptStatus.resumed ) @@ -4671,89 +4745,3 @@ def test_course_staff_can_delete(self): ) self.assertEqual(response.status_code, 200) - - -class TestStudentProctoredGroupedExamAttemptsByCourse(LoggedInTestCase): - """ - Tests for endpoint that returns groups of attempts - """ - def setUp(self): - super().setUp() - self.user.is_staff = True - self.user.save() - self.second_user = User(username='tester2', email='tester2@test.com') - self.second_user.save() - self.client.login_user(self.user) - self.course_id = 'foo/bar/baz' - - set_runtime_service('credit', MockCreditService()) - set_runtime_service('instructor', MockInstructorService(is_user_course_staff=True)) - - def test_get_grouped_exam_attempts(self): - """ - Test new functionality to ensure that if there are multiple attempts on the same exam, they are grouped - by user - """ - - exam_id_1 = create_exam( - course_id=self.course_id, - content_id='content', - exam_name='Sample Exam', - time_limit_mins=10, - is_proctored=True - ) - - exam_id_2 = create_exam( - course_id=self.course_id, - content_id='content2', - exam_name='Sample Exam', - time_limit_mins=10, - is_proctored=True - ) - # create two attempts each for exam 1 - attempt_1 = create_exam_attempt(exam_id_1, self.user.id, taking_as_proctored=True) - update_attempt_status(attempt_1, ProctoredExamStudentAttemptStatus.error) - update_attempt_status(attempt_1, ProctoredExamStudentAttemptStatus.ready_to_resume) - attempt_2 = create_exam_attempt(exam_id_1, self.user.id, taking_as_proctored=True) - - attempt_3 = create_exam_attempt(exam_id_1, self.second_user.id, taking_as_proctored=True) - update_attempt_status(attempt_3, ProctoredExamStudentAttemptStatus.error) - update_attempt_status(attempt_3, ProctoredExamStudentAttemptStatus.ready_to_resume) - attempt_4 = create_exam_attempt(exam_id_1, self.second_user.id, taking_as_proctored=True) - - # create one attempt each for exam 2 - attempt_5 = create_exam_attempt(exam_id_2, self.user.id, taking_as_proctored=True) - attempt_6 = create_exam_attempt(exam_id_2, self.second_user.id, taking_as_proctored=True) - - # check that endpoint returns only four attempts, unique to user and exam - url = reverse('edx_proctoring:proctored_exam.attempts.grouped.course', kwargs={'course_id': self.course_id}) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - response_data = json.loads(response.content.decode('utf-8')) - self.assertEqual(len(response_data['proctored_exam_attempts']), 4) - - # check that each attempt returned is structured as expected - # order of attempts should be sorted by most recently created - first_attempt = response_data['proctored_exam_attempts'][0] - self.assertEqual(first_attempt['user']['id'], self.second_user.id) - self.assertEqual(len(first_attempt['all_attempts']), 1) - self.assertEqual(first_attempt['all_attempts'][0]['id'], attempt_6) - - second_attempt = response_data['proctored_exam_attempts'][1] - self.assertEqual(second_attempt['user']['id'], self.user.id) - self.assertEqual(len(second_attempt['all_attempts']), 1) - self.assertEqual(second_attempt['all_attempts'][0]['id'], attempt_5) - - third_attempt = response_data['proctored_exam_attempts'][2] - self.assertEqual(third_attempt['user']['id'], self.second_user.id) - self.assertEqual(third_attempt['id'], attempt_4) - self.assertEqual(len(third_attempt['all_attempts']), 2) - self.assertEqual(third_attempt['all_attempts'][0]['id'], attempt_4) - self.assertEqual(third_attempt['all_attempts'][1]['id'], attempt_3) - - fourth_attempt = response_data['proctored_exam_attempts'][3] - self.assertEqual(fourth_attempt['user']['id'], self.user.id) - self.assertEqual(fourth_attempt['id'], attempt_2) - self.assertEqual(len(fourth_attempt['all_attempts']), 2) - self.assertEqual(fourth_attempt['all_attempts'][0]['id'], attempt_2) - self.assertEqual(fourth_attempt['all_attempts'][1]['id'], attempt_1) diff --git a/edx_proctoring/urls.py b/edx_proctoring/urls.py index e2038537068..0d3c736cd85 100644 --- a/edx_proctoring/urls.py +++ b/edx_proctoring/urls.py @@ -37,17 +37,6 @@ views.StudentProctoredExamAttempt.as_view(), name='proctored_exam.attempt' ), - url( - r'edx_proctoring/v1/proctored_exam/attempt/course_id/{}$'.format(settings.COURSE_ID_PATTERN), - views.StudentProctoredExamAttemptsByCourse.as_view(), - name='proctored_exam.attempts.course' - ), - url( - r'edx_proctoring/v1/proctored_exam/attempt/course_id/{}/search/(?P.+)$'.format( - settings.COURSE_ID_PATTERN), - views.StudentProctoredExamAttemptsByCourse.as_view(), - name='proctored_exam.attempts.search' - ), url( r'edx_proctoring/v1/proctored_exam/attempt/grouped/course_id/{}$'.format(settings.COURSE_ID_PATTERN), views.StudentProctoredGroupedExamAttemptsByCourse.as_view(), diff --git a/edx_proctoring/views.py b/edx_proctoring/views.py index 3aa523ea54a..00762d1805f 100644 --- a/edx_proctoring/views.py +++ b/edx_proctoring/views.py @@ -1073,51 +1073,6 @@ def _get_first_exam_attempt_per_user(self, attempts): return exam_attempts_per_user -class StudentProctoredExamAttemptsByCourse(ProctoredAPIView): - """ - This endpoint is called by the Instructor Dashboard to get - paginated attempts in a course - - A search parameter is optional - """ - @method_decorator(require_course_or_global_staff) - def get(self, request, course_id, search_by=None): # pylint: disable=unused-argument - """ - HTTP GET Handler. Returns the status of the exam attempt. - Course and Global staff can view both timed and proctored exam attempts. - """ - if search_by is not None: - exam_attempts = ProctoredExamStudentAttempt.objects.get_filtered_exam_attempts( - course_id, search_by - ) - attempt_url = reverse('edx_proctoring:proctored_exam.attempts.search', args=[course_id, search_by]) - else: - exam_attempts = ProctoredExamStudentAttempt.objects.get_all_exam_attempts( - course_id - ) - attempt_url = reverse('edx_proctoring:proctored_exam.attempts.course', args=[course_id]) - - paginator = Paginator(exam_attempts, ATTEMPTS_PER_PAGE) - page = request.GET.get('page') - exam_attempts_page = paginator.get_page(page) - - data = { - 'proctored_exam_attempts': [ - ProctoredExamStudentAttemptSerializer(attempt).data for - attempt in exam_attempts_page.object_list - ], - 'pagination_info': { - 'has_previous': exam_attempts_page.has_previous(), - 'has_next': exam_attempts_page.has_next(), - 'current_page': exam_attempts_page.number, - 'total_pages': exam_attempts_page.paginator.num_pages, - }, - 'attempt_url': attempt_url - - } - return Response(data) - - class ExamAllowanceView(ProctoredAPIView): """ Endpoint for the Exam Allowance diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 623584cbec9..7a5f41e4af6 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "@edx/edx-proctoring", - "version": "3.7.4", + "version": "3.8.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -870,12 +870,6 @@ "osenv": "0.0.3" }, "dependencies": { - "graceful-fs": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", - "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=", - "dev": true - }, "osenv": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.0.3.tgz", @@ -1049,15 +1043,6 @@ } } }, - "graceful-fs": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.12.tgz", - "integrity": "sha512-J55gaCS4iTTJfTXIxSVw3EMQckcqkpdRv3IR7gu6sq0+tbC363Zx6KH/SEwXASK9JRbhyZmVjJEVJIOxYsB3Qg==", - "dev": true, - "requires": { - "natives": "^1.1.3" - } - }, "handlebars": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-2.0.0.tgz", @@ -1237,14 +1222,6 @@ "deep-extend": "~0.2.5", "graceful-fs": "~2.0.0", "intersect": "~0.0.3" - }, - "dependencies": { - "graceful-fs": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", - "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=", - "dev": true - } } }, "bower-logger": { @@ -1269,12 +1246,6 @@ "rimraf": "~2.2.0" }, "dependencies": { - "graceful-fs": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", - "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=", - "dev": true - }, "lru-cache": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.3.1.tgz", @@ -1787,15 +1758,6 @@ "xdg-basedir": "^1.0.0" }, "dependencies": { - "graceful-fs": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.12.tgz", - "integrity": "sha512-J55gaCS4iTTJfTXIxSVw3EMQckcqkpdRv3IR7gu6sq0+tbC363Zx6KH/SEwXASK9JRbhyZmVjJEVJIOxYsB3Qg==", - "dev": true, - "requires": { - "natives": "^1.1.3" - } - }, "object-assign": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz", @@ -2000,15 +1962,6 @@ "touch": "0.0.2" }, "dependencies": { - "graceful-fs": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.12.tgz", - "integrity": "sha512-J55gaCS4iTTJfTXIxSVw3EMQckcqkpdRv3IR7gu6sq0+tbC363Zx6KH/SEwXASK9JRbhyZmVjJEVJIOxYsB3Qg==", - "dev": true, - "requires": { - "natives": "^1.1.3" - } - }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -4500,12 +4453,6 @@ "minimatch": "~0.2.11" } }, - "graceful-fs": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", - "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", - "dev": true - }, "inherits": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", @@ -4688,12 +4635,6 @@ } } }, - "graceful-fs": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", - "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=", - "dev": true - }, "gulp-util": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-1.2.0.tgz", @@ -7172,12 +7113,6 @@ "integrity": "sha1-IKMYwwy0X3H+et+/eyHJnBRy7xE=", "dev": true }, - "natives": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.6.tgz", - "integrity": "sha512-6+TDFewD4yxY14ptjKaS63GVdtKiES1pTPyxn9Jb0rBqPMZ7VcCiooEhPNsr+mqHtMGxa/5c/HhcC4uPEUw/nA==", - "dev": true - }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -10138,15 +10073,6 @@ "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", "dev": true }, - "graceful-fs": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.12.tgz", - "integrity": "sha512-J55gaCS4iTTJfTXIxSVw3EMQckcqkpdRv3IR7gu6sq0+tbC363Zx6KH/SEwXASK9JRbhyZmVjJEVJIOxYsB3Qg==", - "dev": true, - "requires": { - "natives": "^1.1.3" - } - }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", diff --git a/package.json b/package.json index 58e1b8a1290..489a86fb955 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@edx/edx-proctoring", "//": "Be sure to update the version number in edx_proctoring/__init__.py", "//": "Note that the version format is slightly different than that of the Python version when using prereleases.", - "version": "3.7.16", + "version": "3.8.0", "main": "edx_proctoring/static/index.js", "scripts":{ "test":"gulp test"