Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-proctoring
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
OpenEdx
edx-proctoring
Commits
3ee49f6b
Commit
3ee49f6b
authored
Jul 10, 2015
by
chrisndodge
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #18 from edx/muhhshoaib/PHX-49-add-allowance-work
(WIP) PHX-49 initial work
parents
97b5957c
a1841147
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
22 changed files
with
731 additions
and
42 deletions
+731
-42
edx_proctoring/api.py
+28
-2
edx_proctoring/exceptions.py
+6
-0
edx_proctoring/models.py
+21
-3
edx_proctoring/serializers.py
+25
-2
edx_proctoring/services.py
+1
-2
edx_proctoring/static/proctoring/close-modal-hover.png
+0
-0
edx_proctoring/static/proctoring/close-modal.png
+0
-0
edx_proctoring/static/proctoring/js/collections/proctored_exam_allowance_collection.js
+14
-0
edx_proctoring/static/proctoring/js/collections/proctored_exam_collection.js
+14
-0
edx_proctoring/static/proctoring/js/models/proctored_exam_allowance_model.js
+15
-0
edx_proctoring/static/proctoring/js/models/proctored_exam_model.js
+0
-0
edx_proctoring/static/proctoring/js/views/Backbone.ModalDialog.js
+0
-0
edx_proctoring/static/proctoring/js/views/proctored_exam_add_allowance_view.js
+170
-0
edx_proctoring/static/proctoring/js/views/proctored_exam_allowance_view.js
+144
-0
edx_proctoring/static/proctoring/js/views/proctored_exam_view.js
+4
-4
edx_proctoring/static/proctoring/templates/add-new-allowance.underscore
+57
-0
edx_proctoring/static/proctoring/templates/course_allowances.underscore
+58
-0
edx_proctoring/tests/test_api.py
+23
-5
edx_proctoring/tests/test_views.py
+94
-4
edx_proctoring/tests/utils.py
+1
-1
edx_proctoring/urls.py
+11
-0
edx_proctoring/views.py
+45
-19
No files found.
edx_proctoring/api.py
View file @
3ee49f6b
...
...
@@ -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_i
d
,
key
,
value
):
def
add_allowance_for_user
(
exam_id
,
user_i
nfo
,
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
)
...
...
edx_proctoring/exceptions.py
View file @
3ee49f6b
...
...
@@ -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.
"""
edx_proctoring/models.py
View file @
3ee49f6b
...
...
@@ -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_i
d
,
key
,
value
):
def
add_allowance_for_user
(
cls
,
exam_id
,
user_i
nfo
,
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
=
user
s
[
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
=
user
s
[
0
]
.
id
,
key
=
key
,
value
=
value
)
class
ProctoredExamStudentAllowanceHistory
(
TimeStampedModel
):
...
...
edx_proctoring/serializers.py
View file @
3ee49f6b
"""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"
)
edx_proctoring/services.py
View file @
3ee49f6b
...
...
@@ -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
):
...
...
edx_proctoring/static/proctoring/close-modal-hover.png
0 → 100644
View file @
3ee49f6b
4.41 KB
edx_proctoring/static/proctoring/close-modal.png
0 → 100644
View file @
3ee49f6b
1.91 KB
edx_proctoring/static/proctoring/js/collections/proctored_exam_allowance_collection.js
0 → 100644
View file @
3ee49f6b
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
edx_proctoring/static/proctoring/js/collections/proctored_exam_collection.js
0 → 100644
View file @
3ee49f6b
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
edx_proctoring/static/proctoring/js/models/proctored_exam_allowance_model.js
0 → 100644
View file @
3ee49f6b
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
);
edx_proctoring/static/proctoring/js/proctored_exam_model.js
→
edx_proctoring/static/proctoring/js/
models/
proctored_exam_model.js
View file @
3ee49f6b
File moved
edx_proctoring/static/proctoring/js/views/Backbone.ModalDialog.js
0 → 100644
View file @
3ee49f6b
This diff is collapsed.
Click to expand it.
edx_proctoring/static/proctoring/js/views/proctored_exam_add_allowance_view.js
0 → 100644
View file @
3ee49f6b
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
);
edx_proctoring/static/proctoring/js/views/proctored_exam_allowance_view.js
0 → 100644
View file @
3ee49f6b
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
,
$
,
_
);
edx_proctoring/static/proctoring/js/proctored_exam_view.js
→
edx_proctoring/static/proctoring/js/
views/
proctored_exam_view.js
View file @
3ee49f6b
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
()
{
...
...
edx_proctoring/static/proctoring/templates/add-new-allowance.underscore
0 → 100644
View file @
3ee49f6b
<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
edx_proctoring/static/proctoring/templates/course_allowances.underscore
0 → 100644
View file @
3ee49f6b
<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>
<% } %>
edx_proctoring/tests/test_api.py
View file @
3ee49f6b
...
...
@@ -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
)
...
...
edx_proctoring/tests/test_views.py
View file @
3ee49f6b
...
...
@@ -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_i
d'
:
self
.
student_taking_exam
.
id
,
'user_i
nfo'
:
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_i
d'
:
self
.
student_taking_exam
.
id
,
'user_i
nfo'
:
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
):
"""
...
...
edx_proctoring/tests/utils.py
View file @
3ee49f6b
...
...
@@ -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
)
edx_proctoring/urls.py
View file @
3ee49f6b
...
...
@@ -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'
...
...
edx_proctoring/views.py
View file @
3ee49f6b
...
...
@@ -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
):
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment