Commit 3ee49f6b by chrisndodge

Merge pull request #18 from edx/muhhshoaib/PHX-49-add-allowance-work

(WIP) PHX-49 initial work
parents 97b5957c a1841147
......@@ -128,11 +128,19 @@ def get_exam_by_content_id(course_id, content_id):
return serialized_exam_object.data
def add_allowance_for_user(exam_id, user_id, key, value):
def add_allowance_for_user(exam_id, user_info, key, value):
"""
Adds (or updates) an allowance for a user within a given exam
"""
ProctoredExamStudentAllowance.add_allowance_for_user(exam_id, user_id, key, value)
ProctoredExamStudentAllowance.add_allowance_for_user(exam_id, user_info, key, value)
def get_allowances_for_course(course_id):
"""
Get all the allowances for the course.
"""
student_allowances = ProctoredExamStudentAllowance.get_allowances_for_course(course_id)
return [ProctoredExamStudentAllowanceSerializer(allowance).data for allowance in student_allowances]
def remove_allowance_for_user(exam_id, user_id, key):
......@@ -230,6 +238,24 @@ def get_all_exams_for_course(course_id):
This method will return all exams for a course. This will return a list
of dictionaries, whose schema is the same as what is returned in
get_exam_by_id
Returns a list containing dictionary version of the Django ORM object
e.g.
[{
"course_id": "edX/DemoX/Demo_Course",
"content_id": "123",
"external_id": "",
"exam_name": "Midterm",
"time_limit_mins": 90,
"is_proctored": true,
"is_active": true
},
{
...: ...,
...: ...
},
..
]
"""
exams = ProctoredExam.get_all_exams_for_course(course_id)
......
......@@ -37,3 +37,9 @@ class StudentExamAttemptedAlreadyStarted(ProctoredBaseException):
"""
Raised when the same exam attempt is being started twice
"""
class UserNotFoundException(ProctoredBaseException):
"""
Raised when the user not found.
"""
......@@ -10,6 +10,7 @@ from django.dispatch import receiver
from model_utils.models import TimeStampedModel
from django.contrib.auth.models import User
from edx_proctoring.exceptions import UserNotFoundException
class ProctoredExam(TimeStampedModel):
......@@ -201,6 +202,13 @@ class ProctoredExamStudentAllowance(TimeStampedModel):
verbose_name = 'proctored allowance'
@classmethod
def get_allowances_for_course(cls, course_id):
"""
Returns all the allowances for a course.
"""
return cls.objects.filter(proctored_exam__course_id=course_id)
@classmethod
def get_allowance_for_user(cls, exam_id, user_id, key):
"""
Returns an allowance for a user within a given exam
......@@ -219,16 +227,26 @@ class ProctoredExamStudentAllowance(TimeStampedModel):
return cls.objects.filter(proctored_exam_id=exam_id, user_id=user_id)
@classmethod
def add_allowance_for_user(cls, exam_id, user_id, key, value):
def add_allowance_for_user(cls, exam_id, user_info, key, value):
"""
Add or (Update) an allowance for a user within a given exam
"""
users = User.objects.filter(username=user_info)
if not users.exists():
users = User.objects.filter(email=user_info)
if not users.exists():
err_msg = (
'Cannot find user against {user_info}'
).format(user_info=user_info)
raise UserNotFoundException(err_msg)
try:
student_allowance = cls.objects.get(proctored_exam_id=exam_id, user_id=user_id, key=key)
student_allowance = cls.objects.get(proctored_exam_id=exam_id, user_id=users[0].id, key=key)
student_allowance.value = value
student_allowance.save()
except cls.DoesNotExist: # pylint: disable=no-member
cls.objects.create(proctored_exam_id=exam_id, user_id=user_id, key=key, value=value)
cls.objects.create(proctored_exam_id=exam_id, user_id=users[0].id, key=key, value=value)
class ProctoredExamStudentAllowanceHistory(TimeStampedModel):
......
"""Defines serializers used by the Proctoring API."""
from rest_framework import serializers
from django.contrib.auth.models import User
from edx_proctoring.models import ProctoredExam, ProctoredExamStudentAttempt, ProctoredExamStudentAllowance
......@@ -42,12 +43,31 @@ class ProctoredExamSerializer(serializers.ModelSerializer):
)
class UserSerializer(serializers.ModelSerializer):
"""
Serializer for the User Model.
"""
id = serializers.IntegerField(required=False)
username = serializers.CharField(required=True)
email = serializers.CharField(required=True)
class Meta:
"""
Meta Class
"""
model = User
fields = (
"id", "username", "email"
)
class ProctoredExamStudentAttemptSerializer(serializers.ModelSerializer):
"""
Serializer for the ProctoredExamStudentAttempt Model.
"""
proctored_exam_id = serializers.IntegerField(source="proctored_exam_id")
user_id = serializers.IntegerField(source='user_id')
user_id = serializers.IntegerField(required=False)
class Meta:
"""
......@@ -65,11 +85,14 @@ class ProctoredExamStudentAllowanceSerializer(serializers.ModelSerializer):
"""
Serializer for the ProctoredExamStudentAllowance Model.
"""
proctored_exam = ProctoredExamSerializer()
user = UserSerializer()
class Meta:
"""
Meta Class
"""
model = ProctoredExamStudentAllowance
fields = (
"id", "created", "modified", "user", "key", "value"
"id", "created", "modified", "user", "key", "value", "proctored_exam"
)
......@@ -2,7 +2,6 @@
A wrapper class around all methods exposed in api.py
"""
from edx_proctoring import api as edx_proctoring_api
import types
......@@ -29,7 +28,7 @@ class ProctoringService(object):
Class initializer, which just inspects the libraries and exposes the same functions
as a direct pass through
"""
from edx_proctoring import api as edx_proctoring_api
self._bind_to_module_functions(edx_proctoring_api)
def _bind_to_module_functions(self, module):
......
var edx = edx || {};
(function(Backbone) {
edx.instructor_dashboard = edx.instructor_dashboard || {};
edx.instructor_dashboard.proctoring = edx.instructor_dashboard.proctoring || {};
edx.instructor_dashboard.proctoring.ProctoredExamAllowanceCollection = Backbone.Collection.extend({
/* model for a collection of ProctoredExamAllowance */
model: edx.instructor_dashboard.proctoring.ProctoredExamAllowanceModel,
url: '/api/edx_proctoring/v1/proctored_exam/'
});
this.edx.instructor_dashboard.proctoring.ProctoredExamAllowanceCollection = edx.instructor_dashboard.proctoring.ProctoredExamAllowanceCollection;
}).call(this, Backbone);
\ No newline at end of file
var edx = edx || {};
(function(Backbone) {
edx.instructor_dashboard = edx.instructor_dashboard || {};
edx.instructor_dashboard.proctoring = edx.instructor_dashboard.proctoring || {};
edx.instructor_dashboard.proctoring.ProctoredExamCollection = Backbone.Collection.extend({
/* model for a collection of ProctoredExamAllowance */
model: ProctoredExamModel,
url: '/api/edx_proctoring/v1/proctored_exam/exam/course_id/'
});
this.edx.instructor_dashboard.proctoring.ProctoredExamCollection = edx.instructor_dashboard.proctoring.ProctoredExamCollection;
}).call(this, Backbone);
\ No newline at end of file
var edx = edx || {};
(function(Backbone) {
'use strict';
edx.instructor_dashboard = edx.instructor_dashboard || {};
edx.instructor_dashboard.proctoring = edx.instructor_dashboard.proctoring || {};
edx.instructor_dashboard.proctoring.ProctoredExamAllowanceModel = Backbone.Model.extend({
url: '/api/edx_proctoring/v1/proctored_exam/allowance'
});
this.edx.instructor_dashboard.proctoring.ProctoredExamAllowanceModel = edx.instructor_dashboard.proctoring.ProctoredExamAllowanceModel;
}).call(this, Backbone);
var edx = edx || {};
(function (Backbone, $, _, gettext) {
'use strict';
edx.instructor_dashboard = edx.instructor_dashboard || {};
edx.instructor_dashboard.proctoring = edx.instructor_dashboard.proctoring || {};
edx.instructor_dashboard.proctoring.AddAllowanceView = Backbone.ModalView.extend({
name: "AddAllowanceView",
template: null,
template_url: '/static/proctoring/templates/add-new-allowance.underscore',
initialize: function (options) {
this.proctored_exams = options.proctored_exams;
this.proctored_exam_allowance_view = options.proctored_exam_allowance_view;
this.course_id = options.course_id;
this.model = new edx.instructor_dashboard.proctoring.ProctoredExamAllowanceModel();
_.bindAll(this, "render");
this.loadTemplateData();
//Backbone.Validation.bind( this, {valid:this.hideError, invalid:this.showError});
},
events: {
"submit form": "addAllowance"
},
loadTemplateData: function () {
var self = this;
$.ajax({url: self.template_url, dataType: "html"})
.error(function (jqXHR, textStatus, errorThrown) {
})
.done(function (template_data) {
self.template = _.template(template_data);
self.render();
self.showModal();
self.updateCss();
});
},
updateCss: function() {
var $el = $(this.el);
$el.find('.modal-header').css({
"color": "#1580b0",
"font-size": "20px",
"font-weight": "600",
"line-height": "normal",
"padding": "10px 15px",
"border-bottom": "1px solid #ccc"
});
$el.find('form').css({
"padding": "15px"
});
$el.find('form table.compact td').css({
"vertical-align": "middle",
"padding": "4px 8px"
});
$el.find('form label').css({
"display": "block",
"font-size": "14px",
"margin": 0
});
$el.find('form input[type="text"]').css({
"height": "26px",
"padding": "5px 8px"
});
$el.find('form input[type="submit"]').css({
"margin-top": "10px",
"padding": "2px 32px"
});
$el.find('.error-message').css({
"color": "#ff0000",
"line-height": "normal",
"font-size": "14px"
});
$el.find('.error-response').css({
"color": "#ff0000",
"line-height": "normal",
"font-size": "14px",
"padding": "0px 10px 5px 7px"
});
},
getCurrentFormValues: function () {
return {
proctored_exam: $("select#proctored_exam").val(),
allowance_type: $("select#allowance_type").val(),
allowance_value: $("#allowance_value").val(),
user_info: $("#user_info").val()
};
},
hideError: function (view, attr, selector) {
var $element = view.$form[attr];
$element.removeClass("error");
$element.parent().find(".error-message").empty();
},
showError: function (view, attr, errorMessage, selector) {
var $element = view.$form[attr];
$element.addClass("error");
var $errorMessage = $element.parent().find(".error-message");
if ($errorMessage.length == 0) {
$errorMessage = $("<div class='error-message'></div>");
$element.parent().append($errorMessage);
}
$errorMessage.empty().append(errorMessage);
this.updateCss();
},
addAllowance: function (event) {
event.preventDefault();
var error_response = $('.error-response');
error_response.html();
var values = this.getCurrentFormValues();
var formHasErrors = false;
var self = this;
$.each(values, function(key, value) {
if (value==="") {
formHasErrors = true;
self.showError(self, key, gettext("Required field"));
}
else {
self.hideError(self, key);
}
});
if (!formHasErrors) {
self.model.fetch({
headers: {
"X-CSRFToken": self.proctored_exam_allowance_view.getCSRFToken()
},
type: 'PUT',
data: {
'exam_id': values.proctored_exam,
'user_info': values.user_info,
'key': values.allowance_type,
'value': values.allowance_value
},
success: function () {
// fetch the allowances again.
error_response.html();
self.proctored_exam_allowance_view.collection.url = self.proctored_exam_allowance_view.initial_url + self.course_id + '/allowance';
self.proctored_exam_allowance_view.hydrate();
self.hideModal();
},
error: function(self, response, options) {
var data = $.parseJSON(response.responseText);
error_response.html(gettext(data.detail));
}
});
}
},
render: function () {
var allowance_types = ['Additional time (minutes)'];
$(this.el).html(this.template({
proctored_exams: this.proctored_exams,
allowance_types: allowance_types
}));
this.$form = {
proctored_exam: this.$("select#proctored_exam"),
allowance_type: this.$("select#allowance_type"),
allowance_value: this.$("#allowance_value"),
user_info: this.$("#user_info")
};
return this;
}
});
}).call(this, Backbone, $, _, gettext);
var edx = edx || {};
(function (Backbone, $, _) {
'use strict';
edx.instructor_dashboard = edx.instructor_dashboard || {};
edx.instructor_dashboard.proctoring = edx.instructor_dashboard.proctoring || {};
edx.instructor_dashboard.proctoring.ProctoredExamAllowanceView = Backbone.View.extend({
initialize: function (options) {
this.collection = new edx.instructor_dashboard.proctoring.ProctoredExamAllowanceCollection();
this.proctoredExamCollection = new edx.instructor_dashboard.proctoring.ProctoredExamCollection();
/* 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');
/* this should be moved to a 'data' attribute in HTML */
this.tempate_url = '/static/proctoring/templates/course_allowances.underscore';
this.template = null;
this.initial_url = this.collection.url;
this.allowance_url = this.initial_url + 'allowance';
/* re-render if the model changes */
this.listenTo(this.collection, 'change', this.collectionChanged);
/* Load the static template for rendering. */
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'
},
getCSRFToken: function () {
var cookieValue = null;
var name = 'csrftoken';
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
},
removeAllowance: function (event) {
var element = $(event.currentTarget);
var userID = element.data('user-id');
var examID = element.data('exam-id');
var key = element.data('key-name');
var self = this;
self.collection.url = this.allowance_url;
self.collection.fetch(
{
headers: {
"X-CSRFToken": this.getCSRFToken()
},
type: 'DELETE',
data: {
'exam_id': examID,
'user_id': userID,
'key': key
},
success: function () {
// fetch the allowances again.
self.collection.url = self.initial_url + self.course_id + '/allowance';
self.hydrate();
}
}
);
event.stopPropagation();
event.preventDefault();
},
/*
This entry point is required for Instructor Dashboard
See setup_instructor_dashboard_sections() in
instructor_dashboard.coffee (in edx-platform)
*/
constructor: function (section) {
/* the Instructor Dashboard javascript expects this to be set up */
$(section).data('wrapper', this);
this.initialize({});
},
onClickTitle: function () {
// called when this is selected in the instructor dashboard
return;
},
loadTemplateData: function () {
var self = this;
$.ajax({url: self.tempate_url, dataType: "html"})
.error(function (jqXHR, textStatus, errorThrown) {
})
.done(function (template_data) {
self.template = _.template(template_data);
self.hydrate();
});
},
hydrate: function () {
/* This function will load the bound collection */
/* add and remove a class when we do the initial loading */
/* we might - at some point - add a visual element to the */
/* loading, like a spinner */
var self = this;
self.collection.fetch({
success: function () {
self.render();
}
});
},
collectionChanged: function () {
this.hydrate();
},
render: function () {
if (this.template !== null) {
var html = this.template({proctored_exam_allowances: this.collection.toJSON()});
this.$el.html(html);
this.$el.show();
}
},
showAddModal: function (event) {
var self = this;
self.proctoredExamCollection.fetch({
success: function () {
var add_allowance_view = new edx.instructor_dashboard.proctoring.AddAllowanceView({
course_id: self.course_id,
proctored_exams: self.proctoredExamCollection.toJSON(),
proctored_exam_allowance_view: self
});
}
});
event.stopPropagation();
event.preventDefault();
}
});
}).call(this, Backbone, $, _);
var edx = edx || {};
(function(Backbone, $, _) {
(function (Backbone, $, _) {
'use strict';
edx.coursware = edx.coursware || {};
edx.coursware.proctored_exam = {};
edx.coursware.proctored_exam = edx.coursware.proctored_exam || {};
edx.coursware.proctored_exam.ProctoredExamView = Backbone.View.extend({
initialize: function (options) {
......@@ -22,14 +22,14 @@ var edx = edx || {};
this.template = _.template(template_html);
}
/* re-render if the model changes */
this.listenTo(this.model,'change', this.modelChanged);
this.listenTo(this.model, 'change', this.modelChanged);
/* make the async call to the backend REST API */
/* after it loads, the listenTo event will file and */
/* will call into the rendering */
this.model.fetch();
},
modelChanged: function() {
modelChanged: function () {
this.render();
},
render: function () {
......
<div class='modal-header'><%- gettext("Add a new Allowance") %></div>
<form>
<h3 class='error-response'><h3>
<table class='compact'>
<tr>
<td>
<label><%- gettext("Proctored Exam") %></label>
</td>
<td>
<select id='proctored_exam'>
<% _.each(proctored_exams, function(proctored_exam){ %>
<option value="<%= proctored_exam.id %>">
<%- interpolate(gettext(' %(exam_display_name)s '), { exam_display_name: proctored_exam.exam_name }, true) %>
</option>
<% }); %>
</select>
</td>
</tr>
<tr>
<td>
<label><%- gettext("Allowance Type") %></label>
</td>
<td>
<select id="allowance_type">
<% _.each(allowance_types, function(allowance_type){ %>
<option value="<%= allowance_type %>">
<%- interpolate(gettext(' %(allowance_type)s '), { allowance_type: allowance_type }, true) %>
</option>
<% }); %>
</select>
</td>
</tr>
<tr>
<td>
<label><%- gettext("Value") %></label>
</td>
<td>
<input type="text" id="allowance_value" />
</td>
</tr>
<tr>
<td>
<label><%- gettext("Username or Email") %></label>
</td>
<td>
<input type="text" id="user_info" />
</td>
</tr>
<tr>
<td></td>
<td>
<input id='addNewAllowance' type='submit' value='Save' />
</td>
</tr>
</table>
</form>
\ No newline at end of file
<span class="tip"> <%- gettext("Add Allowance for User: ") %>
<span>
<a id="add-allowance" href="#" class="add blue-button">+ <%- gettext("Add Allowance") %></a>
</span>
</span>
<% var is_allowances = proctored_exam_allowances.length !== 0 %>
<% if (is_allowances) { %>
<div class="wrapper-content wrapper">
<section class="content">
<table class="allowance-table">
<thead>
<tr class="allowance-headings">
<th class="exam-name"><%- gettext("Exam Name") %></th>
<th class="username"><%- gettext("Username") %></th>
<th class="email"><%- gettext("Email") %></th>
<th class="allowance-name"><%- gettext("Allowance Name") %> </th>
<th class="allowance-value"><%- gettext("Allowance Value") %></th>
<th class="c_action"><%- gettext("Actions") %> </th>
</tr>
</thead>
<tbody>
<% _.each(proctored_exam_allowances, function(proctored_exam_allowance){ %>
<tr class="allowance-items">
<td>
<%- interpolate(gettext(' %(exam_display_name)s '), { exam_display_name: proctored_exam_allowance.proctored_exam.exam_name }, true) %>
</td>
<% if (proctored_exam_allowance.user){ %>
<td>
<%- interpolate(gettext(' %(username)s '), { username: proctored_exam_allowance.user.username }, true) %>
</td>
<td>
<%- interpolate(gettext(' %(email)s '), { email: proctored_exam_allowance.user.email }, true) %>
</td>
<% }else{ %>
<td>N/A</td>
<td>N/A</td>
<% } %>
<td>
<%- interpolate(gettext(' %(allowance_name)s '), { allowance_name: proctored_exam_allowance.key }, true) %>
</td>
<td><%= proctored_exam_allowance.value %></td>
<td>
<a data-exam-id="<%= proctored_exam_allowance.proctored_exam.id %>"
data-key-name="<%= proctored_exam_allowance.key %>"
data-user-id="<%= proctored_exam_allowance.user.id %>"
class="remove_allowance" href="#">[x]</a>
</td>
</tr>
<% }); %>
</tbody>
</table>
</section>
</div>
<% } %>
......@@ -16,7 +16,8 @@ from edx_proctoring.api import (
get_exam_attempt,
create_exam_attempt,
get_student_view,
get_all_exams_for_course,
get_allowances_for_course,
get_all_exams_for_course
)
from edx_proctoring.exceptions import (
ProctoredExamAlreadyExists,
......@@ -24,6 +25,7 @@ from edx_proctoring.exceptions import (
StudentExamAttemptAlreadyExistsException,
StudentExamAttemptDoesNotExistsException,
StudentExamAttemptedAlreadyStarted,
UserNotFoundException
)
from edx_proctoring.models import (
ProctoredExam,
......@@ -188,19 +190,26 @@ class ProctoredExamApiTests(LoggedInTestCase):
"""
Test to add allowance for user.
"""
add_allowance_for_user(self.proctored_exam_id, self.user_id, self.key, self.value)
add_allowance_for_user(self.proctored_exam_id, self.user.username, self.key, self.value)
student_allowance = ProctoredExamStudentAllowance.get_allowance_for_user(
self.proctored_exam_id, self.user_id, self.key
)
self.assertIsNotNone(student_allowance)
def test_add_invalid_allowance(self):
"""
Test to add allowance for invalid user.
"""
with self.assertRaises(UserNotFoundException):
add_allowance_for_user(self.proctored_exam_id, 'invalid_user', self.key, self.value)
def test_update_existing_allowance(self):
"""
Test updation to the allowance that already exists.
"""
student_allowance = self._add_allowance_for_user()
add_allowance_for_user(student_allowance.proctored_exam.id, self.user_id, self.key, 'new_value')
add_allowance_for_user(student_allowance.proctored_exam.id, self.user.username, self.key, 'new_value')
student_allowance = ProctoredExamStudentAllowance.get_allowance_for_user(
student_allowance.proctored_exam.id, self.user_id, self.key
......@@ -208,6 +217,15 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.assertIsNotNone(student_allowance)
self.assertEqual(student_allowance.value, 'new_value')
def test_get_allowances_for_course(self):
"""
Test to get all the allowances for a course.
"""
allowance = self._add_allowance_for_user()
course_allowances = get_allowances_for_course(self.course_id)
self.assertEqual(len(course_allowances), 1)
self.assertEqual(course_allowances[0]['proctored_exam']['course_id'], allowance.proctored_exam.course_id)
def test_get_non_existing_allowance(self):
"""
Test to get an allowance which does not exist.
......@@ -316,8 +334,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
exam_id=exam_id,
user_id=self.user_id,
)
add_allowance_for_user(self.proctored_exam_id, self.user_id, self.key, self.value)
add_allowance_for_user(self.proctored_exam_id, self.user_id, 'new_key', 'new_value')
add_allowance_for_user(self.proctored_exam_id, self.user.username, self.key, self.value)
add_allowance_for_user(self.proctored_exam_id, self.user.username, 'new_key', 'new_value')
student_active_exams = get_active_exams_for_user(self.user_id, self.course_id)
self.assertEqual(len(student_active_exams), 2)
self.assertEqual(len(student_active_exams[0]['allowances']), 2)
......
......@@ -43,8 +43,11 @@ class ProctoredExamsApiTests(LoggedInTestCase):
try:
response = self.client.get(reverse(urlpattern.name, args=[0]))
except NoReverseMatch:
# some require 2 args.
response = self.client.get(reverse(urlpattern.name, args=["0/0/0", 0]))
try:
response = self.client.get(reverse(urlpattern.name, args=["0/0/0"]))
except NoReverseMatch:
# some require 2 args.
response = self.client.get(reverse(urlpattern.name, args=["0/0/0", 0]))
self.assertEqual(response.status_code, 403)
......@@ -273,6 +276,32 @@ class ProctoredExamViewTests(LoggedInTestCase):
self.assertEqual(response_data['external_id'], proctored_exam.external_id)
self.assertEqual(response_data['time_limit_mins'], proctored_exam.time_limit_mins)
def test_get_exam_by_course_id(self):
"""
Tests the Get Exam by course id endpoint
"""
# Create an exam.
proctored_exam = ProctoredExam.objects.create(
course_id='a/b/c',
content_id='test_content',
exam_name='Test Exam',
external_id='123aXqe3',
time_limit_mins=90
)
response = self.client.get(
reverse('edx_proctoring.proctored_exam.exams_by_course_id', kwargs={
'course_id': proctored_exam.course_id
})
)
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content)
self.assertEqual(response_data[0]['course_id'], proctored_exam.course_id)
self.assertEqual(response_data[0]['exam_name'], proctored_exam.exam_name)
self.assertEqual(response_data[0]['content_id'], proctored_exam.content_id)
self.assertEqual(response_data[0]['external_id'], proctored_exam.external_id)
self.assertEqual(response_data[0]['time_limit_mins'], proctored_exam.time_limit_mins)
def test_get_exam_by_bad_content_id(self):
"""
Tests the Get Exam by content id endpoint
......@@ -568,7 +597,7 @@ class TestExamAllowanceView(LoggedInTestCase):
)
allowance_data = {
'exam_id': proctored_exam.id,
'user_id': self.student_taking_exam.id,
'user_info': self.student_taking_exam.username,
'key': 'a_key',
'value': '30'
}
......@@ -578,6 +607,33 @@ class TestExamAllowanceView(LoggedInTestCase):
)
self.assertEqual(response.status_code, 200)
def test_add_invalid_allowance(self):
"""
Add allowance for a invalid user_info.
"""
# Create an exam.
proctored_exam = ProctoredExam.objects.create(
course_id='a/b/c',
content_id='test_content',
exam_name='Test Exam',
external_id='123aXqe3',
time_limit_mins=90
)
allowance_data = {
'exam_id': proctored_exam.id,
'user_info': 'invalid_user',
'key': 'a_key',
'value': '30'
}
response = self.client.put(
reverse('edx_proctoring.proctored_exam.allowance'),
allowance_data
)
self.assertEqual(response.status_code, 400)
response_data = json.loads(response.content)
self.assertEqual(len(response_data), 1)
self.assertEqual(response_data['detail'], u"Cannot find user against invalid_user")
def test_remove_allowance_for_user(self):
"""
Remove allowance for a user for an exam.
......@@ -592,7 +648,7 @@ class TestExamAllowanceView(LoggedInTestCase):
)
allowance_data = {
'exam_id': proctored_exam.id,
'user_id': self.student_taking_exam.id,
'user_info': self.student_taking_exam.email,
'key': 'a_key',
'value': '30'
}
......@@ -610,6 +666,40 @@ class TestExamAllowanceView(LoggedInTestCase):
)
self.assertEqual(response.status_code, 200)
def test_get_allowances_for_course(self):
"""
Remove allowance for a user for an exam.
"""
# Create an exam.
proctored_exam = ProctoredExam.objects.create(
course_id='a/b/c',
content_id='test_content',
exam_name='Test Exam',
external_id='123aXqe3',
time_limit_mins=90
)
allowance_data = {
'exam_id': proctored_exam.id,
'user_info': self.student_taking_exam.username,
'key': 'a_key',
'value': '30'
}
response = self.client.put(
reverse('edx_proctoring.proctored_exam.allowance'),
allowance_data
)
self.assertEqual(response.status_code, 200)
response = self.client.get(
reverse('edx_proctoring.proctored_exam.allowance', kwargs={'course_id': proctored_exam.course_id})
)
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content)
self.assertEqual(len(response_data), 1)
self.assertEqual(response_data[0]['proctored_exam']['course_id'], proctored_exam.course_id)
self.assertEqual(response_data[0]['key'], allowance_data['key'])
class TestActiveExamsForUserView(LoggedInTestCase):
"""
......
......@@ -63,6 +63,6 @@ class LoggedInTestCase(TestCase):
"""
self.client = TestClient()
self.user = User(username='tester')
self.user = User(username='tester', email='tester@test.com')
self.user.save()
self.client.login_user(self.user)
......@@ -25,11 +25,22 @@ urlpatterns = patterns( # pylint: disable=invalid-name
name='edx_proctoring.proctored_exam.exam_by_content_id'
),
url(
r'edx_proctoring/v1/proctored_exam/exam/course_id/{}$'.format(
settings.COURSE_ID_PATTERN),
views.ProctoredExamView.as_view(),
name='edx_proctoring.proctored_exam.exams_by_course_id'
),
url(
r'edx_proctoring/v1/proctored_exam/attempt$',
views.StudentProctoredExamAttempt.as_view(),
name='edx_proctoring.proctored_exam.attempt'
),
url(
r'edx_proctoring/v1/proctored_exam/{}/allowance$'.format(settings.COURSE_ID_PATTERN),
views.ExamAllowanceView.as_view(),
name='edx_proctoring.proctored_exam.allowance'
),
url(
r'edx_proctoring/v1/proctored_exam/allowance$',
views.ExamAllowanceView.as_view(),
name='edx_proctoring.proctored_exam.allowance'
......
......@@ -20,12 +20,14 @@ from edx_proctoring.api import (
add_allowance_for_user,
remove_allowance_for_user,
get_active_exams_for_user,
create_exam_attempt
create_exam_attempt,
get_allowances_for_course,
get_all_exams_for_course
)
from edx_proctoring.exceptions import (
ProctoredBaseException,
ProctoredExamNotFoundException,
)
UserNotFoundException)
from edx_proctoring.serializers import ProctoredExamSerializer
from .utils import AuthenticatedAPIView
......@@ -176,17 +178,24 @@ class ProctoredExamView(AuthenticatedAPIView):
data={"detail": "The exam_id does not exist."}
)
else:
# get by course_id & content_id
try:
return Response(
data=get_exam_by_content_id(course_id, content_id),
status=status.HTTP_200_OK
)
except ProctoredExamNotFoundException:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={"detail": "The exam with course_id, content_id does not exist."}
)
if course_id is not None:
if content_id is not None:
# get by course_id & content_id
try:
return Response(
data=get_exam_by_content_id(course_id, content_id),
status=status.HTTP_200_OK
)
except ProctoredExamNotFoundException:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={"detail": "The exam with course_id, content_id does not exist."}
)
else:
result_set = get_all_exams_for_course(
course_id=course_id
)
return Response(result_set)
class StudentProctoredExamAttempt(AuthenticatedAPIView):
......@@ -359,16 +368,33 @@ class ExamAllowanceView(AuthenticatedAPIView):
* returns Nothing. deletes the allowance for the user proctored exam.
"""
@method_decorator(require_staff)
def get(self, request, course_id): # pylint: disable=unused-argument
"""
HTTP GET handler. Get all allowances for a course.
"""
result_set = get_allowances_for_course(
course_id=course_id
)
return Response(result_set)
@method_decorator(require_staff)
def put(self, request):
"""
HTTP GET handler. Adds or updates Allowance
"""
return Response(add_allowance_for_user(
exam_id=request.DATA.get('exam_id', None),
user_id=request.DATA.get('user_id', None),
key=request.DATA.get('key', None),
value=request.DATA.get('value', None)
))
try:
return Response(add_allowance_for_user(
exam_id=request.DATA.get('exam_id', None),
user_info=request.DATA.get('user_info', None),
key=request.DATA.get('key', None),
value=request.DATA.get('value', None)
))
except UserNotFoundException, ex:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={"detail": str(ex)}
)
@method_decorator(require_staff)
def delete(self, request):
......
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