diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 9f8ba7cdf64..79c67dd3f11 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -14,6 +14,11 @@ Change Log
Unreleased
~~~~~~~~~~
+[3.21.0] - 2021-07-23
+~~~~~~~~~~~~~~~~~~~~~
+* Added feature behind the bulk allowance waffle flag that groups allowances by users.
+* Updated the UI so allowances are under dropdown for each user
+
[3.20.6] - 2021-07-22
~~~~~~~~~~~~~~~~~~~~~
* Removed use of name field in proctored exam attempt admin.
diff --git a/edx_proctoring/__init__.py b/edx_proctoring/__init__.py
index e75ce94b16b..133ec1ee68a 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.20.6'
+__version__ = '3.21.0'
default_app_config = 'edx_proctoring.apps.EdxProctoringConfig' # pylint: disable=invalid-name
diff --git a/edx_proctoring/static/proctoring/js/views/proctored_exam_allowance_view.js b/edx_proctoring/static/proctoring/js/views/proctored_exam_allowance_view.js
index fd53dd045c6..73be7d293c4 100644
--- a/edx_proctoring/static/proctoring/js/views/proctored_exam_allowance_view.js
+++ b/edx_proctoring/static/proctoring/js/views/proctored_exam_allowance_view.js
@@ -19,9 +19,17 @@ edx = edx || {};
/* unfortunately we have to make some assumptions about what is being set up in HTML */
this.setElement($('.special-allowance-container'));
this.course_id = this.$el.data('course-id');
-
+ /* we need to check if the bulk allowance waffle flag is enabled */
+ this.enableBulkAllowance =
+ this.$el.data('enable-bulk-allowance');
+ this.enableBulkAllowance = this.enableBulkAllowance &&
+ this.enableBulkAllowance.toLowerCase() === 'true';
/* this should be moved to a 'data' attribute in HTML */
- this.template_url = '/static/proctoring/templates/course_allowances.underscore';
+ if (this.enableBulkAllowance) {
+ this.template_url = '/static/proctoring/templates/course_grouped_allowances.underscore';
+ } else {
+ this.template_url = '/static/proctoring/templates/course_allowances.underscore';
+ }
this.template = null;
this.initial_url = this.collection.url;
this.allowance_url = this.initial_url + 'allowance';
@@ -32,11 +40,11 @@ edx = edx || {};
this.loadTemplateData();
this.proctoredExamCollection.url = this.proctoredExamCollection.url + this.course_id;
- this.collection.url = this.initial_url + this.course_id + '/allowance';
},
events: {
'click #add-allowance': 'showAddModal',
- 'click .remove_allowance': 'removeAllowance'
+ 'click .remove_allowance': 'removeAllowance',
+ 'click .accordion-trigger': 'toggleAllowanceAccordion'
},
getCSRFToken: function() {
var cookieValue = null;
@@ -75,7 +83,6 @@ edx = edx || {};
},
success: function() {
// fetch the allowances again.
- self.collection.url = self.initial_url + self.course_id + '/allowance';
self.hydrate();
}
}
@@ -113,6 +120,11 @@ edx = edx || {};
/* we might - at some point - add a visual element to the */
/* loading, like a spinner */
var self = this;
+ if (self.enableBulkAllowance) {
+ self.collection.url = self.initial_url + self.course_id + '/grouped/allowance';
+ } else {
+ self.collection.url = self.initial_url + self.course_id + '/allowance';
+ }
self.collection.fetch({
success: function() {
self.render();
@@ -126,31 +138,32 @@ edx = edx || {};
var self = this;
var key, i, html;
if (this.template !== null) {
- this.collection.each(function(item) {
- key = item.get('key');
- for (i = 0; i < self.allowance_types.length; i += 1) {
- if (key === self.allowance_types[i][0]) {
- item.set('key_display_name', self.allowance_types[i][1]);
- break;
+ if (!this.enableBulkAllowance) {
+ this.collection.each(function(item) {
+ key = item.get('key');
+ for (i = 0; i < self.allowance_types.length; i += 1) {
+ if (key === self.allowance_types[i][0]) {
+ item.set('key_display_name', self.allowance_types[i][1]);
+ break;
+ }
}
- }
- if (!item.has('key_display_name')) {
- item.set('key_display_name', key);
- }
- });
- html = this.template({proctored_exam_allowances: this.collection.toJSON()});
+ if (!item.has('key_display_name')) {
+ item.set('key_display_name', key);
+ }
+ });
+ html = this.template({proctored_exam_allowances: this.collection.toJSON()});
+ } else {
+ html = this.template({proctored_exam_allowances: this.collection.toJSON()[0],
+ allowance_types: self.allowance_types});
+ }
this.$el.html(html);
}
},
showAddModal: function(event) {
var self = this;
- var enableBulkAllowance =
- self.$el.data('enable-bulk-allowance');
- enableBulkAllowance = enableBulkAllowance &&
- enableBulkAllowance.toLowerCase() === 'true';
self.proctoredExamCollection.fetch({
success: function() {
- if (!enableBulkAllowance) {
+ if (!self.enableBulkAllowance) {
// eslint-disable-next-line no-new
new edx.instructor_dashboard.proctoring.AddAllowanceView({
course_id: self.course_id,
@@ -171,6 +184,28 @@ edx = edx || {};
});
event.stopPropagation();
event.preventDefault();
+ },
+ toggleAllowanceAccordion: function(event) {
+ // based on code from openedx/features/course_experience/static/course_experience/js/CourseOutline.js
+ // but modified to better fit this feature's needs
+ var accordionRow, isExpanded, $toggleChevron, $contentPanel;
+ accordionRow = event.currentTarget;
+ if (accordionRow.classList.contains('accordion-trigger')) {
+ isExpanded = accordionRow.getAttribute('aria-expanded') === 'true';
+ if (!isExpanded) {
+ $toggleChevron = $(accordionRow).find('.fa-chevron-right');
+ $contentPanel = $('#' + accordionRow.innerText.trim());
+ $contentPanel.show();
+ $toggleChevron.addClass('fa-rotate-90');
+ accordionRow.setAttribute('aria-expanded', 'true');
+ } else {
+ $toggleChevron = $(accordionRow).find('.fa-chevron-right');
+ $contentPanel = $('#' + accordionRow.innerText.trim());
+ $contentPanel.hide();
+ $toggleChevron.removeClass('fa-rotate-90');
+ accordionRow.setAttribute('aria-expanded', 'false');
+ }
+ }
}
});
this.edx.instructor_dashboard.proctoring.ProctoredExamAllowanceView =
diff --git a/edx_proctoring/static/proctoring/spec/proctored_exam_bulk_allowance_spec.js b/edx_proctoring/static/proctoring/spec/proctored_exam_bulk_allowance_spec.js
new file mode 100644
index 00000000000..4d2f217cdcb
--- /dev/null
+++ b/edx_proctoring/static/proctoring/spec/proctored_exam_bulk_allowance_spec.js
@@ -0,0 +1,100 @@
+describe('ProctoredExamAllowanceView', function() {
+ 'use strict';
+
+ var html = '';
+ var expectedProctoredAllowanceJson = [{
+ student: [{
+ created: '2015-08-10T09:15:45Z',
+ id: 1,
+ modified: '2015-08-10T09:15:45Z',
+ key: 'Additional time (minutes)',
+ value: '1',
+ proctored_exam: {
+ content_id: 'i4x://edX/DemoX/sequential/9f5e9b018a244ea38e5d157e0019e60c',
+ course_id: 'edX/DemoX/Demo_Course',
+ exam_name: 'Test Exam',
+ external_id: null,
+ id: 17,
+ is_active: true,
+ is_practice_exam: false,
+ is_proctored: true,
+ time_limit_mins: 1
+ },
+ user: {
+ username: 'testuser1',
+ email: 'testuser1@test.com'
+ }
+ }]}
+ ];
+
+ beforeEach(function() {
+ // eslint-disable-next-line max-len
+ html = ' <%- gettext("Allowances") %>\n \n + <%- gettext("Add Allowance") %>\n \n\n<% var is_allowances = proctored_exam_allowances.length !== 0 %>\n<% if (is_allowances) { %>\n\n
\n
\n <% _.each(proctored_exam_allowances, function(student){ %>\n \n \n <%=student[0].user.username %>\n
\n \n \n \n <%- gettext("Exam Name") %> | \n <%- gettext("Email") %> | \n <%- gettext("Allowance Type") %> | \n <%- gettext("Allowance Value") %> | \n <%- gettext("Actions") %> | \n
\n <% _.each(student, function(proctored_exam_allowance){ %>\n <% var key = proctored_exam_allowance.key; %>\n <% for (i = 0; i < allowance_types.length; i += 1) { %>\n <% if (key === allowance_types[i][0]) { %>\n <% proctored_exam_allowance.key_display_name = allowance_types[i][1]; %>\n <% break; %>\n <% }} %>\n <% if (!proctored_exam_allowance.key_display_name) { %>\n <% proctored_exam_allowance.key_display_name = key;} %>\n \n \n <%- proctored_exam_allowance.proctored_exam.exam_name %>\n | \n \n <% if (proctored_exam_allowance.user){ %>\n <%= proctored_exam_allowance.user.email %>\n | \n <% }else{ %>\n N/A | \n N/A | \n <% } %>\n \n <%= proctored_exam_allowance.key_display_name %>\n | \n \n <%- proctored_exam_allowance.value %> | \n \n [x]\n | \n
\n <% }); %>\n \n
\n <% }); %>\n \n
\n<% } %>\n';
+ this.server = sinon.fakeServer.create();
+ this.server.autoRespond = true;
+ setFixtures('');
+
+ // load the underscore template response before calling the proctored exam allowance view.
+ this.server.respondWith('GET', '/static/proctoring/templates/course_grouped_allowances.underscore',
+ [
+ 200,
+ {'Content-Type': 'text/html'},
+ html
+ ]
+ );
+ });
+
+ afterEach(function() {
+ this.server.restore();
+ });
+ it('should render the proctored exam allowance view properly', function() {
+ this.server.respondWith('GET', '/api/edx_proctoring/v1/proctored_exam/test_course_id/grouped/allowance',
+ [
+ 200,
+ {
+ 'Content-Type': 'application/json'
+ },
+ JSON.stringify(expectedProctoredAllowanceJson)
+ ]
+ );
+
+ this.proctored_exam_allowance = new edx.instructor_dashboard.proctoring.ProctoredExamAllowanceView();
+ this.server.respond();
+ this.server.respond();
+ expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html())
+ .toContain('testuser1');
+ expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html())
+ .toContain('testuser1@test.com');
+ expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html())
+ .toContain('Additional time (minutes)');
+ expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html())
+ .toContain('Test Exam');
+ });
+ //
+ it('should remove the proctored exam allowance', function() {
+ this.server.respondWith('GET', '/api/edx_proctoring/v1/proctored_exam/test_course_id/grouped/allowance',
+ [
+ 200,
+ {
+ 'Content-Type': 'application/json'
+ },
+ JSON.stringify(expectedProctoredAllowanceJson)
+ ]
+ );
+
+ this.proctored_exam_allowance = new edx.instructor_dashboard.proctoring.ProctoredExamAllowanceView();
+
+ this.server.respond();
+ this.server.respond();
+
+ expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html())
+ .toContain('testuser1');
+ expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html())
+ .toContain('testuser1@test.com');
+ expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html())
+ .toContain('Additional time (minutes)');
+ expect(this.proctored_exam_allowance.$el.find('tr.allowance-items').html())
+ .toContain('Test Exam');
+ });
+});
diff --git a/edx_proctoring/static/proctoring/templates/course_grouped_allowances.underscore b/edx_proctoring/static/proctoring/templates/course_grouped_allowances.underscore
new file mode 100644
index 00000000000..d0333ba9cb0
--- /dev/null
+++ b/edx_proctoring/static/proctoring/templates/course_grouped_allowances.underscore
@@ -0,0 +1,64 @@
+ <%- gettext("Allowances") %>
+
+ + <%- gettext("Add Allowance") %>
+
+
+<% var is_allowances = proctored_exam_allowances.length !== 0 %>
+<% if (is_allowances) { %>
+
+
+
+ <% _.each(proctored_exam_allowances, function(student){ %>
+
+
+ <%=student[0].user.username %>
+
+
+
+
+ <%- gettext("Exam Name") %> |
+ <%- gettext("Email") %> |
+ <%- gettext("Allowance Type") %> |
+ <%- gettext("Allowance Value") %> |
+ <%- gettext("Actions") %> |
+
+ <% _.each(student, function(proctored_exam_allowance){ %>
+ <% var key = proctored_exam_allowance.key; %>
+ <% for (i = 0; i < allowance_types.length; i += 1) { %>
+ <% if (key === allowance_types[i][0]) { %>
+ <% proctored_exam_allowance.key_display_name = allowance_types[i][1]; %>
+ <% break; %>
+ <% }} %>
+ <% if (!proctored_exam_allowance.key_display_name) { %>
+ <% proctored_exam_allowance.key_display_name = key;} %>
+
+
+ <%- proctored_exam_allowance.proctored_exam.exam_name %>
+ |
+
+ <% if (proctored_exam_allowance.user){ %>
+ <%= proctored_exam_allowance.user.email %>
+ |
+ <% }else{ %>
+ N/A |
+ N/A |
+ <% } %>
+
+ <%= proctored_exam_allowance.key_display_name %>
+ |
+
+ <%- proctored_exam_allowance.value %> |
+
+ [x]
+ |
+
+ <% }); %>
+
+
+ <% }); %>
+
+
+<% } %>
diff --git a/edx_proctoring/tests/test_views.py b/edx_proctoring/tests/test_views.py
index b23c1113402..16fc6e119dd 100644
--- a/edx_proctoring/tests/test_views.py
+++ b/edx_proctoring/tests/test_views.py
@@ -5305,10 +5305,9 @@ def test_get_grouped_allowances(self):
'additional_time_granted')
third_serialized_allowance = ProctoredExamStudentAllowanceSerializer(third_user_allowance).data
expected_response = {
- 'grouped_allowances': {str(first_user): [first_serialized_allowance],
- str(second_user): [second_serialized_allowance],
- str(third_user): [third_serialized_allowance]}
- }
+ user_list[0].username: [first_serialized_allowance],
+ user_list[1].username: [second_serialized_allowance],
+ user_list[2].username: [third_serialized_allowance]}
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content.decode('utf-8'))
@@ -5371,9 +5370,7 @@ def test_get_grouped_allowances_course_no_allowances(self):
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), 1)
- grouped_allowances = response_data['grouped_allowances']
- self.assertEqual(len(grouped_allowances), 0)
+ self.assertEqual(len(response_data), 0)
def test_get_grouped_allowances_non_global_staff(self):
"""
@@ -5418,9 +5415,7 @@ def test_get_grouped_allowances_non_global_staff(self):
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), 1)
- grouped_allowances = response_data['grouped_allowances']
- self.assertEqual(len(grouped_allowances), 3)
+ self.assertEqual(len(response_data), 3)
class TestActiveExamsForUserView(LoggedInTestCase):
diff --git a/edx_proctoring/urls.py b/edx_proctoring/urls.py
index 24e7e7ba09a..175cf1f294a 100644
--- a/edx_proctoring/urls.py
+++ b/edx_proctoring/urls.py
@@ -91,7 +91,7 @@
name='proctored_exam.bulk_allowance'
),
url(
- r'edx_proctoring/v1/proctored_exam/allowance/grouped/course_id/{}$'.format(settings.COURSE_ID_PATTERN),
+ r'edx_proctoring/v1/proctored_exam/{}/grouped/allowance$'.format(settings.COURSE_ID_PATTERN),
views.GroupedExamAllowancesByStudent.as_view(),
name='proctored_exam.allowance.grouped.course'
),
diff --git a/edx_proctoring/views.py b/edx_proctoring/views.py
index 997c8d90184..3692db478e3 100644
--- a/edx_proctoring/views.py
+++ b/edx_proctoring/views.py
@@ -1587,17 +1587,17 @@ class GroupedExamAllowancesByStudent(ProctoredAPIView):
HTTP GET:
The response will contain a dictionary with the allowances of a course grouped by student.
For example:
- {'grouped_allowances':{'4':
- [{'id': 4, 'created': '2021-06-21T14:47:17.847221Z',
- 'modified': '2021-06-21T14:47:17.847221Z',
- 'user': {'id': 4, 'username': 'student1',
- 'email': 'student1@test.com'},
- 'key': 'additional_time_granted', 'value': '30',
- 'proctored_exam': {'id': 2, 'course_id': 'a/b/c', 'content_id': 'test_content2',
- 'external_id': None, 'exam_name': 'Test Exam2', 'time_limit_mins': 90,
- 'is_proctored': False, 'is_practice_exam': False,
- 'is_active': True, 'due_date': None,
- 'hide_after_due': False, 'backend': None}}}
+ {{'student1':
+ [{'id': 4, 'created': '2021-06-21T14:47:17.847221Z',
+ 'modified': '2021-06-21T14:47:17.847221Z',
+ 'user': {'id': 4, 'username': 'student1',
+ 'email': 'student1@test.com'},
+ 'key': 'additional_time_granted', 'value': '30',
+ 'proctored_exam': {'id': 2, 'course_id': 'a/b/c', 'content_id': 'test_content2',
+ 'external_id': None, 'exam_name': 'Test Exam2', 'time_limit_mins': 90,
+ 'is_proctored': False, 'is_practice_exam': False,
+ 'is_active': True, 'due_date': None,
+ 'hide_after_due': False, 'backend': None}}}
**Exceptions**
HTTP GET:
@@ -1615,13 +1615,13 @@ def get(self, request, course_id):
grouped_allowances = {}
- # Process allowances so they are grouped by user id
+ # Process allowances so they are grouped by username
for allowance in all_allowances:
serialied_allowance = ProctoredExamStudentAllowanceSerializer(allowance).data
- user_id = serialied_allowance['user']['id']
- grouped_allowances.setdefault(user_id, []).append(serialied_allowance)
+ username = serialied_allowance['user']['username']
+ grouped_allowances.setdefault(username, []).append(serialied_allowance)
- response_data = {'grouped_allowances': grouped_allowances}
+ response_data = grouped_allowances
return Response(response_data)
diff --git a/package.json b/package.json
index e0ab1a6cd76..48027fe82db 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "@edx/edx-proctoring",
"//": "Note that the version format is slightly different than that of the Python version when using prereleases.",
- "version": "3.20.6",
+ "version": "3.21.0",
"main": "edx_proctoring/static/index.js",
"scripts": {
"test": "gulp test"