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): ...@@ -128,11 +128,19 @@ def get_exam_by_content_id(course_id, content_id):
return serialized_exam_object.data 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 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): def remove_allowance_for_user(exam_id, user_id, key):
...@@ -230,6 +238,24 @@ def get_all_exams_for_course(course_id): ...@@ -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 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 of dictionaries, whose schema is the same as what is returned in
get_exam_by_id 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) exams = ProctoredExam.get_all_exams_for_course(course_id)
......
...@@ -37,3 +37,9 @@ class StudentExamAttemptedAlreadyStarted(ProctoredBaseException): ...@@ -37,3 +37,9 @@ class StudentExamAttemptedAlreadyStarted(ProctoredBaseException):
""" """
Raised when the same exam attempt is being started twice 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 ...@@ -10,6 +10,7 @@ from django.dispatch import receiver
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
from django.contrib.auth.models import User from django.contrib.auth.models import User
from edx_proctoring.exceptions import UserNotFoundException
class ProctoredExam(TimeStampedModel): class ProctoredExam(TimeStampedModel):
...@@ -201,6 +202,13 @@ class ProctoredExamStudentAllowance(TimeStampedModel): ...@@ -201,6 +202,13 @@ class ProctoredExamStudentAllowance(TimeStampedModel):
verbose_name = 'proctored allowance' verbose_name = 'proctored allowance'
@classmethod @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): def get_allowance_for_user(cls, exam_id, user_id, key):
""" """
Returns an allowance for a user within a given exam Returns an allowance for a user within a given exam
...@@ -219,16 +227,26 @@ class ProctoredExamStudentAllowance(TimeStampedModel): ...@@ -219,16 +227,26 @@ class ProctoredExamStudentAllowance(TimeStampedModel):
return cls.objects.filter(proctored_exam_id=exam_id, user_id=user_id) return cls.objects.filter(proctored_exam_id=exam_id, user_id=user_id)
@classmethod @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 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: 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.value = value
student_allowance.save() student_allowance.save()
except cls.DoesNotExist: # pylint: disable=no-member 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): class ProctoredExamStudentAllowanceHistory(TimeStampedModel):
......
"""Defines serializers used by the Proctoring API.""" """Defines serializers used by the Proctoring API."""
from rest_framework import serializers from rest_framework import serializers
from django.contrib.auth.models import User
from edx_proctoring.models import ProctoredExam, ProctoredExamStudentAttempt, ProctoredExamStudentAllowance from edx_proctoring.models import ProctoredExam, ProctoredExamStudentAttempt, ProctoredExamStudentAllowance
...@@ -42,12 +43,31 @@ class ProctoredExamSerializer(serializers.ModelSerializer): ...@@ -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): class ProctoredExamStudentAttemptSerializer(serializers.ModelSerializer):
""" """
Serializer for the ProctoredExamStudentAttempt Model. Serializer for the ProctoredExamStudentAttempt Model.
""" """
proctored_exam_id = serializers.IntegerField(source="proctored_exam_id") proctored_exam_id = serializers.IntegerField(source="proctored_exam_id")
user_id = serializers.IntegerField(source='user_id') user_id = serializers.IntegerField(required=False)
class Meta: class Meta:
""" """
...@@ -65,11 +85,14 @@ class ProctoredExamStudentAllowanceSerializer(serializers.ModelSerializer): ...@@ -65,11 +85,14 @@ class ProctoredExamStudentAllowanceSerializer(serializers.ModelSerializer):
""" """
Serializer for the ProctoredExamStudentAllowance Model. Serializer for the ProctoredExamStudentAllowance Model.
""" """
proctored_exam = ProctoredExamSerializer()
user = UserSerializer()
class Meta: class Meta:
""" """
Meta Class Meta Class
""" """
model = ProctoredExamStudentAllowance model = ProctoredExamStudentAllowance
fields = ( fields = (
"id", "created", "modified", "user", "key", "value" "id", "created", "modified", "user", "key", "value", "proctored_exam"
) )
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
A wrapper class around all methods exposed in api.py A wrapper class around all methods exposed in api.py
""" """
from edx_proctoring import api as edx_proctoring_api
import types import types
...@@ -29,7 +28,7 @@ class ProctoringService(object): ...@@ -29,7 +28,7 @@ class ProctoringService(object):
Class initializer, which just inspects the libraries and exposes the same functions Class initializer, which just inspects the libraries and exposes the same functions
as a direct pass through as a direct pass through
""" """
from edx_proctoring import api as edx_proctoring_api
self._bind_to_module_functions(edx_proctoring_api) self._bind_to_module_functions(edx_proctoring_api)
def _bind_to_module_functions(self, module): 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 || {}; var edx = edx || {};
(function(Backbone, $, _) { (function (Backbone, $, _) {
'use strict'; 'use strict';
edx.coursware = edx.coursware || {}; edx.coursware = edx.coursware || {};
edx.coursware.proctored_exam = {}; edx.coursware.proctored_exam = edx.coursware.proctored_exam || {};
edx.coursware.proctored_exam.ProctoredExamView = Backbone.View.extend({ edx.coursware.proctored_exam.ProctoredExamView = Backbone.View.extend({
initialize: function (options) { initialize: function (options) {
...@@ -22,14 +22,14 @@ var edx = edx || {}; ...@@ -22,14 +22,14 @@ var edx = edx || {};
this.template = _.template(template_html); this.template = _.template(template_html);
} }
/* re-render if the model changes */ /* 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 */ /* make the async call to the backend REST API */
/* after it loads, the listenTo event will file and */ /* after it loads, the listenTo event will file and */
/* will call into the rendering */ /* will call into the rendering */
this.model.fetch(); this.model.fetch();
}, },
modelChanged: function() { modelChanged: function () {
this.render(); this.render();
}, },
render: function () { 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 ( ...@@ -16,7 +16,8 @@ from edx_proctoring.api import (
get_exam_attempt, get_exam_attempt,
create_exam_attempt, create_exam_attempt,
get_student_view, get_student_view,
get_all_exams_for_course, get_allowances_for_course,
get_all_exams_for_course
) )
from edx_proctoring.exceptions import ( from edx_proctoring.exceptions import (
ProctoredExamAlreadyExists, ProctoredExamAlreadyExists,
...@@ -24,6 +25,7 @@ from edx_proctoring.exceptions import ( ...@@ -24,6 +25,7 @@ from edx_proctoring.exceptions import (
StudentExamAttemptAlreadyExistsException, StudentExamAttemptAlreadyExistsException,
StudentExamAttemptDoesNotExistsException, StudentExamAttemptDoesNotExistsException,
StudentExamAttemptedAlreadyStarted, StudentExamAttemptedAlreadyStarted,
UserNotFoundException
) )
from edx_proctoring.models import ( from edx_proctoring.models import (
ProctoredExam, ProctoredExam,
...@@ -188,19 +190,26 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -188,19 +190,26 @@ class ProctoredExamApiTests(LoggedInTestCase):
""" """
Test to add allowance for user. 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( student_allowance = ProctoredExamStudentAllowance.get_allowance_for_user(
self.proctored_exam_id, self.user_id, self.key self.proctored_exam_id, self.user_id, self.key
) )
self.assertIsNotNone(student_allowance) 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): def test_update_existing_allowance(self):
""" """
Test updation to the allowance that already exists. Test updation to the allowance that already exists.
""" """
student_allowance = self._add_allowance_for_user() 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 = ProctoredExamStudentAllowance.get_allowance_for_user(
student_allowance.proctored_exam.id, self.user_id, self.key student_allowance.proctored_exam.id, self.user_id, self.key
...@@ -208,6 +217,15 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -208,6 +217,15 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.assertIsNotNone(student_allowance) self.assertIsNotNone(student_allowance)
self.assertEqual(student_allowance.value, 'new_value') 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): def test_get_non_existing_allowance(self):
""" """
Test to get an allowance which does not exist. Test to get an allowance which does not exist.
...@@ -316,8 +334,8 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -316,8 +334,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
exam_id=exam_id, exam_id=exam_id,
user_id=self.user_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.username, 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, 'new_key', 'new_value')
student_active_exams = get_active_exams_for_user(self.user_id, self.course_id) 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), 2)
self.assertEqual(len(student_active_exams[0]['allowances']), 2) self.assertEqual(len(student_active_exams[0]['allowances']), 2)
......
...@@ -43,8 +43,11 @@ class ProctoredExamsApiTests(LoggedInTestCase): ...@@ -43,8 +43,11 @@ class ProctoredExamsApiTests(LoggedInTestCase):
try: try:
response = self.client.get(reverse(urlpattern.name, args=[0])) response = self.client.get(reverse(urlpattern.name, args=[0]))
except NoReverseMatch: except NoReverseMatch:
# some require 2 args. try:
response = self.client.get(reverse(urlpattern.name, args=["0/0/0", 0])) 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) self.assertEqual(response.status_code, 403)
...@@ -273,6 +276,32 @@ class ProctoredExamViewTests(LoggedInTestCase): ...@@ -273,6 +276,32 @@ class ProctoredExamViewTests(LoggedInTestCase):
self.assertEqual(response_data['external_id'], proctored_exam.external_id) self.assertEqual(response_data['external_id'], proctored_exam.external_id)
self.assertEqual(response_data['time_limit_mins'], proctored_exam.time_limit_mins) 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): def test_get_exam_by_bad_content_id(self):
""" """
Tests the Get Exam by content id endpoint Tests the Get Exam by content id endpoint
...@@ -568,7 +597,7 @@ class TestExamAllowanceView(LoggedInTestCase): ...@@ -568,7 +597,7 @@ class TestExamAllowanceView(LoggedInTestCase):
) )
allowance_data = { allowance_data = {
'exam_id': proctored_exam.id, 'exam_id': proctored_exam.id,
'user_id': self.student_taking_exam.id, 'user_info': self.student_taking_exam.username,
'key': 'a_key', 'key': 'a_key',
'value': '30' 'value': '30'
} }
...@@ -578,6 +607,33 @@ class TestExamAllowanceView(LoggedInTestCase): ...@@ -578,6 +607,33 @@ class TestExamAllowanceView(LoggedInTestCase):
) )
self.assertEqual(response.status_code, 200) 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): def test_remove_allowance_for_user(self):
""" """
Remove allowance for a user for an exam. Remove allowance for a user for an exam.
...@@ -592,7 +648,7 @@ class TestExamAllowanceView(LoggedInTestCase): ...@@ -592,7 +648,7 @@ class TestExamAllowanceView(LoggedInTestCase):
) )
allowance_data = { allowance_data = {
'exam_id': proctored_exam.id, 'exam_id': proctored_exam.id,
'user_id': self.student_taking_exam.id, 'user_info': self.student_taking_exam.email,
'key': 'a_key', 'key': 'a_key',
'value': '30' 'value': '30'
} }
...@@ -610,6 +666,40 @@ class TestExamAllowanceView(LoggedInTestCase): ...@@ -610,6 +666,40 @@ class TestExamAllowanceView(LoggedInTestCase):
) )
self.assertEqual(response.status_code, 200) 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): class TestActiveExamsForUserView(LoggedInTestCase):
""" """
......
...@@ -63,6 +63,6 @@ class LoggedInTestCase(TestCase): ...@@ -63,6 +63,6 @@ class LoggedInTestCase(TestCase):
""" """
self.client = TestClient() self.client = TestClient()
self.user = User(username='tester') self.user = User(username='tester', email='tester@test.com')
self.user.save() self.user.save()
self.client.login_user(self.user) self.client.login_user(self.user)
...@@ -25,11 +25,22 @@ urlpatterns = patterns( # pylint: disable=invalid-name ...@@ -25,11 +25,22 @@ urlpatterns = patterns( # pylint: disable=invalid-name
name='edx_proctoring.proctored_exam.exam_by_content_id' name='edx_proctoring.proctored_exam.exam_by_content_id'
), ),
url( 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$', r'edx_proctoring/v1/proctored_exam/attempt$',
views.StudentProctoredExamAttempt.as_view(), views.StudentProctoredExamAttempt.as_view(),
name='edx_proctoring.proctored_exam.attempt' name='edx_proctoring.proctored_exam.attempt'
), ),
url( 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$', r'edx_proctoring/v1/proctored_exam/allowance$',
views.ExamAllowanceView.as_view(), views.ExamAllowanceView.as_view(),
name='edx_proctoring.proctored_exam.allowance' name='edx_proctoring.proctored_exam.allowance'
......
...@@ -20,12 +20,14 @@ from edx_proctoring.api import ( ...@@ -20,12 +20,14 @@ from edx_proctoring.api import (
add_allowance_for_user, add_allowance_for_user,
remove_allowance_for_user, remove_allowance_for_user,
get_active_exams_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 ( from edx_proctoring.exceptions import (
ProctoredBaseException, ProctoredBaseException,
ProctoredExamNotFoundException, ProctoredExamNotFoundException,
) UserNotFoundException)
from edx_proctoring.serializers import ProctoredExamSerializer from edx_proctoring.serializers import ProctoredExamSerializer
from .utils import AuthenticatedAPIView from .utils import AuthenticatedAPIView
...@@ -176,17 +178,24 @@ class ProctoredExamView(AuthenticatedAPIView): ...@@ -176,17 +178,24 @@ class ProctoredExamView(AuthenticatedAPIView):
data={"detail": "The exam_id does not exist."} data={"detail": "The exam_id does not exist."}
) )
else: else:
# get by course_id & content_id if course_id is not None:
try: if content_id is not None:
return Response( # get by course_id & content_id
data=get_exam_by_content_id(course_id, content_id), try:
status=status.HTTP_200_OK return Response(
) data=get_exam_by_content_id(course_id, content_id),
except ProctoredExamNotFoundException: status=status.HTTP_200_OK
return Response( )
status=status.HTTP_400_BAD_REQUEST, except ProctoredExamNotFoundException:
data={"detail": "The exam with course_id, content_id does not exist."} 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): class StudentProctoredExamAttempt(AuthenticatedAPIView):
...@@ -359,16 +368,33 @@ class ExamAllowanceView(AuthenticatedAPIView): ...@@ -359,16 +368,33 @@ class ExamAllowanceView(AuthenticatedAPIView):
* returns Nothing. deletes the allowance for the user proctored exam. * returns Nothing. deletes the allowance for the user proctored exam.
""" """
@method_decorator(require_staff) @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): def put(self, request):
""" """
HTTP GET handler. Adds or updates Allowance HTTP GET handler. Adds or updates Allowance
""" """
return Response(add_allowance_for_user( try:
exam_id=request.DATA.get('exam_id', None), return Response(add_allowance_for_user(
user_id=request.DATA.get('user_id', None), exam_id=request.DATA.get('exam_id', None),
key=request.DATA.get('key', None), user_info=request.DATA.get('user_info', None),
value=request.DATA.get('value', 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) @method_decorator(require_staff)
def delete(self, request): 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