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
487f461a
Commit
487f461a
authored
Jul 27, 2015
by
Muhammad Shoaib
Committed by
Chris Dodge
Jul 28, 2015
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
SOL-1087
parent
71caf92c
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
139 additions
and
7 deletions
+139
-7
edx_proctoring/api.py
+6
-3
edx_proctoring/serializers.py
+2
-1
edx_proctoring/static/proctoring/js/models/proctored_exam_model.js
+2
-0
edx_proctoring/static/proctoring/js/views/proctored_exam_view.js
+22
-1
edx_proctoring/templates/proctoring/seq_proctored_exam_error.html
+6
-0
edx_proctoring/tests/test_api.py
+28
-0
edx_proctoring/tests/test_views.py
+57
-1
edx_proctoring/views.py
+16
-1
No files found.
edx_proctoring/api.py
View file @
487f461a
...
@@ -525,9 +525,12 @@ def get_student_view(user_id, course_id, content_id,
...
@@ -525,9 +525,12 @@ def get_student_view(user_id, course_id, content_id,
has_started_exam
=
attempt
and
attempt
.
get
(
'started_at'
)
has_started_exam
=
attempt
and
attempt
.
get
(
'started_at'
)
has_time_expired
=
False
has_time_expired
=
False
if
has_started_exam
:
if
has_started_exam
:
now_utc
=
datetime
.
now
(
pytz
.
UTC
)
if
attempt
.
get
(
'status'
)
==
'error'
:
expires_at
=
attempt
[
'started_at'
]
+
timedelta
(
minutes
=
attempt
[
'allowed_time_limit_mins'
])
student_view_template
=
'proctoring/seq_proctored_exam_error.html'
has_time_expired
=
now_utc
>
expires_at
else
:
now_utc
=
datetime
.
now
(
pytz
.
UTC
)
expires_at
=
attempt
[
'started_at'
]
+
timedelta
(
minutes
=
attempt
[
'allowed_time_limit_mins'
])
has_time_expired
=
now_utc
>
expires_at
# make sure the attempt has been marked as timed_out, if need be
# make sure the attempt has been marked as timed_out, if need be
if
has_time_expired
and
attempt
[
'status'
]
!=
ProctoredExamStudentAttemptStatus
.
timed_out
:
if
has_time_expired
and
attempt
[
'status'
]
!=
ProctoredExamStudentAttemptStatus
.
timed_out
:
...
...
edx_proctoring/serializers.py
View file @
487f461a
...
@@ -78,7 +78,8 @@ class ProctoredExamStudentAttemptSerializer(serializers.ModelSerializer):
...
@@ -78,7 +78,8 @@ class ProctoredExamStudentAttemptSerializer(serializers.ModelSerializer):
fields
=
(
fields
=
(
"id"
,
"created"
,
"modified"
,
"user"
,
"started_at"
,
"completed_at"
,
"id"
,
"created"
,
"modified"
,
"user"
,
"started_at"
,
"completed_at"
,
"external_id"
,
"status"
,
"proctored_exam"
,
"allowed_time_limit_mins"
,
"external_id"
,
"status"
,
"proctored_exam"
,
"allowed_time_limit_mins"
,
"attempt_code"
,
"is_sample_attempt"
,
"taking_as_proctored"
"attempt_code"
,
"is_sample_attempt"
,
"taking_as_proctored"
,
"last_poll_timestamp"
,
"last_poll_ipaddr"
)
)
...
...
edx_proctoring/static/proctoring/js/models/proctored_exam_model.js
View file @
487f461a
...
@@ -5,6 +5,8 @@
...
@@ -5,6 +5,8 @@
defaults
:
{
defaults
:
{
in_timed_exam
:
false
,
in_timed_exam
:
false
,
attempt_id
:
0
,
attempt_status
:
'started'
,
taking_as_proctored
:
false
,
taking_as_proctored
:
false
,
exam_display_name
:
''
,
exam_display_name
:
''
,
exam_url_path
:
''
,
exam_url_path
:
''
,
...
...
edx_proctoring/static/proctoring/js/views/proctored_exam_view.js
View file @
487f461a
...
@@ -13,6 +13,7 @@ var edx = edx || {};
...
@@ -13,6 +13,7 @@ var edx = edx || {};
this
.
templateId
=
options
.
proctored_template
;
this
.
templateId
=
options
.
proctored_template
;
this
.
template
=
null
;
this
.
template
=
null
;
this
.
timerId
=
null
;
this
.
timerId
=
null
;
this
.
timerTick
=
0
;
/* give an extra 5 seconds where the timer holds at 00:00 before page refreshes */
/* give an extra 5 seconds where the timer holds at 00:00 before page refreshes */
this
.
grace_period_secs
=
5
;
this
.
grace_period_secs
=
5
;
...
@@ -53,7 +54,11 @@ var edx = edx || {};
...
@@ -53,7 +54,11 @@ var edx = edx || {};
},
},
render
:
function
()
{
render
:
function
()
{
if
(
this
.
template
!==
null
)
{
if
(
this
.
template
!==
null
)
{
if
(
this
.
model
.
get
(
'in_timed_exam'
)
&&
this
.
model
.
get
(
'time_remaining_seconds'
)
>
0
)
{
if
(
this
.
model
.
get
(
'in_timed_exam'
)
&&
this
.
model
.
get
(
'time_remaining_seconds'
)
>
0
&&
this
.
model
.
get
(
'attempt_status'
)
!==
'error'
)
{
var
html
=
this
.
template
(
this
.
model
.
toJSON
());
var
html
=
this
.
template
(
this
.
model
.
toJSON
());
this
.
$el
.
html
(
html
);
this
.
$el
.
html
(
html
);
this
.
$el
.
show
();
this
.
$el
.
show
();
...
@@ -71,6 +76,22 @@ var edx = edx || {};
...
@@ -71,6 +76,22 @@ var edx = edx || {};
"credit eligibility in this course.
\
n"
);
"credit eligibility in this course.
\
n"
);
},
},
updateRemainingTime
:
function
(
self
)
{
updateRemainingTime
:
function
(
self
)
{
self
.
timerTick
++
;
if
(
self
.
timerTick
%
5
==
0
){
var
url
=
self
.
model
.
url
+
'/'
+
self
.
model
.
get
(
'attempt_id'
);
$
.
ajax
(
url
).
success
(
function
(
data
)
{
if
(
data
.
status
===
'error'
)
{
// Let the student know that his exam has failed due to an error.
// This alert may or may not bring the browser window back to the
// foreground (depending on browser as well as user settings)
alert
(
gettext
(
'Your exam has failed'
));
clearInterval
(
self
.
timerId
);
// stop the timer once the time finishes.
$
(
window
).
unbind
(
'beforeunload'
,
self
.
unloadMessage
);
// refresh the page when the timer expired
location
.
reload
();
}
});
}
self
.
$el
.
find
(
'div.exam-timer'
).
removeClass
(
"low-time warning critical"
);
self
.
$el
.
find
(
'div.exam-timer'
).
removeClass
(
"low-time warning critical"
);
self
.
$el
.
find
(
'div.exam-timer'
).
addClass
(
self
.
model
.
getRemainingTimeState
());
self
.
$el
.
find
(
'div.exam-timer'
).
addClass
(
self
.
model
.
getRemainingTimeState
());
self
.
$el
.
find
(
'span#time_remaining_id b'
).
html
(
self
.
model
.
getFormattedRemainingTime
());
self
.
$el
.
find
(
'span#time_remaining_id b'
).
html
(
self
.
model
.
getFormattedRemainingTime
());
...
...
edx_proctoring/templates/proctoring/seq_proctored_exam_error.html
0 → 100644
View file @
487f461a
{% load i18n %}
<div
class=
"sequence proctored-exam error"
>
<div
class=
"gated-sequence"
>
{% trans "Your exam has been marked as failed due to an error." %}
</div>
</div>
edx_proctoring/tests/test_api.py
View file @
487f461a
...
@@ -75,6 +75,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
...
@@ -75,6 +75,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
self
.
start_an_exam_msg
=
'Would you like to take
%
s as a proctored exam?'
self
.
start_an_exam_msg
=
'Would you like to take
%
s as a proctored exam?'
self
.
timed_exam_msg
=
'
%
s is a Timed Exam'
self
.
timed_exam_msg
=
'
%
s is a Timed Exam'
self
.
exam_time_expired_msg
=
'You did not complete the exam in the allotted time'
self
.
exam_time_expired_msg
=
'You did not complete the exam in the allotted time'
self
.
exam_time_error_msg
=
'Your exam has been marked as failed due to an error.'
self
.
chose_proctored_exam_msg
=
'You have chosen to take
%
s as a proctored exam'
self
.
chose_proctored_exam_msg
=
'You have chosen to take
%
s as a proctored exam'
def
_create_proctored_exam
(
self
):
def
_create_proctored_exam
(
self
):
...
@@ -606,4 +607,31 @@ class ProctoredExamApiTests(LoggedInTestCase):
...
@@ -606,4 +607,31 @@ class ProctoredExamApiTests(LoggedInTestCase):
'default_time_limit_mins'
:
90
'default_time_limit_mins'
:
90
}
}
)
)
self
.
assertIn
(
self
.
exam_time_expired_msg
,
rendered_response
)
self
.
assertIn
(
self
.
exam_time_expired_msg
,
rendered_response
)
def
test_get_studentview_erroneous_exam
(
self
):
# pylint: disable=invalid-name
"""
Test for get_student_view proctored exam which has exam status error.
"""
ProctoredExamStudentAttempt
.
objects
.
create
(
proctored_exam_id
=
self
.
proctored_exam_id
,
user_id
=
self
.
user_id
,
external_id
=
self
.
external_id
,
started_at
=
datetime
.
now
(
pytz
.
UTC
),
allowed_time_limit_mins
=
10
,
status
=
'error'
)
rendered_response
=
get_student_view
(
user_id
=
self
.
user_id
,
course_id
=
self
.
course_id
,
content_id
=
self
.
content_id
,
context
=
{
'is_proctored'
:
True
,
'display_name'
:
self
.
exam_name
,
'default_time_limit_mins'
:
10
}
)
self
.
assertIn
(
self
.
exam_time_error_msg
,
rendered_response
)
edx_proctoring/tests/test_views.py
View file @
487f461a
...
@@ -5,9 +5,10 @@ All tests for the proctored_exams.py
...
@@ -5,9 +5,10 @@ All tests for the proctored_exams.py
import
json
import
json
import
pytz
import
pytz
from
mock
import
Mock
from
mock
import
Mock
from
freezegun
import
freeze_time
from
httmock
import
HTTMock
from
httmock
import
HTTMock
from
string
import
Template
# pylint: disable=deprecated-module
from
string
import
Template
# pylint: disable=deprecated-module
from
datetime
import
datetime
from
datetime
import
datetime
,
timedelta
from
django.test.client
import
Client
from
django.test.client
import
Client
from
django.core.urlresolvers
import
reverse
,
NoReverseMatch
from
django.core.urlresolvers
import
reverse
,
NoReverseMatch
from
django.contrib.auth.models
import
User
from
django.contrib.auth.models
import
User
...
@@ -488,6 +489,61 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
...
@@ -488,6 +489,61 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
self
.
assertIsNotNone
(
response_data
[
'started_at'
])
self
.
assertIsNotNone
(
response_data
[
'started_at'
])
self
.
assertIsNone
(
response_data
[
'completed_at'
])
self
.
assertIsNone
(
response_data
[
'completed_at'
])
def
test_attempt_status_error
(
self
):
"""
Test to confirm that attempt status is marked as error, because client
has stopped sending it's polling timestamp
"""
# 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
)
attempt_data
=
{
'exam_id'
:
proctored_exam
.
id
,
'external_id'
:
proctored_exam
.
external_id
,
'start_clock'
:
True
,
}
response
=
self
.
client
.
post
(
reverse
(
'edx_proctoring.proctored_exam.attempt.collection'
),
attempt_data
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
)
attempt_id
=
response_data
[
'exam_attempt_id'
]
self
.
assertEqual
(
attempt_id
,
1
)
response
=
self
.
client
.
get
(
reverse
(
'edx_proctoring.proctored_exam.attempt'
,
args
=
[
attempt_id
])
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response_data
[
'status'
],
'started'
)
attempt_code
=
response_data
[
'attempt_code'
]
# test the polling callback point
response
=
self
.
client
.
get
(
reverse
(
'edx_proctoring.anonymous.proctoring_poll_status'
,
args
=
[
attempt_code
]
)
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# now reset the time to 2 minutes in the future.
reset_time
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
minutes
=
2
)
with
freeze_time
(
reset_time
):
response
=
self
.
client
.
get
(
reverse
(
'edx_proctoring.proctored_exam.attempt'
,
args
=
[
attempt_id
])
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response_data
[
'status'
],
'error'
)
def
test_remove_attempt
(
self
):
def
test_remove_attempt
(
self
):
"""
"""
Confirms that an attempt can be removed
Confirms that an attempt can be removed
...
...
edx_proctoring/views.py
View file @
487f461a
...
@@ -29,7 +29,9 @@ from edx_proctoring.api import (
...
@@ -29,7 +29,9 @@ from edx_proctoring.api import (
get_exam_attempt_by_id
,
get_exam_attempt_by_id
,
get_all_exam_attempts
,
get_all_exam_attempts
,
remove_exam_attempt
,
remove_exam_attempt
,
get_filtered_exam_attempts
)
get_filtered_exam_attempts
,
update_exam_attempt
)
from
edx_proctoring.exceptions
import
(
from
edx_proctoring.exceptions
import
(
ProctoredBaseException
,
ProctoredBaseException
,
ProctoredExamNotFoundException
,
ProctoredExamNotFoundException
,
...
@@ -43,6 +45,8 @@ from .utils import AuthenticatedAPIView
...
@@ -43,6 +45,8 @@ from .utils import AuthenticatedAPIView
ATTEMPTS_PER_PAGE
=
25
ATTEMPTS_PER_PAGE
=
25
SOFTWARE_SECURE_CLIENT_TIMEOUT
=
15
LOG
=
logging
.
getLogger
(
"edx_proctoring_views"
)
LOG
=
logging
.
getLogger
(
"edx_proctoring_views"
)
...
@@ -265,6 +269,15 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
...
@@ -265,6 +269,15 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
)
)
raise
ProctoredExamPermissionDenied
(
err_msg
)
raise
ProctoredExamPermissionDenied
(
err_msg
)
# check if the last_poll_timestamp is not None
# and if it is older than SOFTWARE_SECURE_CLIENT_TIMEOUT
# then attempt status should be marked as error.
last_poll_timestamp
=
attempt
[
'last_poll_timestamp'
]
if
last_poll_timestamp
is
not
None
\
and
(
datetime
.
now
(
pytz
.
UTC
)
-
last_poll_timestamp
)
.
total_seconds
()
>
SOFTWARE_SECURE_CLIENT_TIMEOUT
:
attempt
[
'status'
]
=
'error'
update_exam_attempt
(
attempt_id
,
status
=
'error'
)
return
Response
(
return
Response
(
data
=
attempt
,
data
=
attempt
,
status
=
status
.
HTTP_200_OK
status
=
status
.
HTTP_200_OK
...
@@ -482,6 +495,8 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView):
...
@@ -482,6 +495,8 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView):
'low_threshold_sec'
:
low_threshold
,
'low_threshold_sec'
:
low_threshold
,
'critically_low_threshold_sec'
:
critically_low_threshold
,
'critically_low_threshold_sec'
:
critically_low_threshold
,
'course_id'
:
exam
[
'course_id'
],
'course_id'
:
exam
[
'course_id'
],
'attempt_id'
:
attempt
[
'id'
],
'attempt_status'
:
attempt
[
'status'
]
}
}
else
:
else
:
response_dict
=
{
response_dict
=
{
...
...
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