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
edc4d2a1
Commit
edc4d2a1
authored
Jul 16, 2015
by
chrisndodge
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #27 from edx/cdodge/punchlist1
Cdodge/punchlist1
parents
c272b6d8
1b1a309f
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
126 additions
and
18 deletions
+126
-18
README.md
+1
-1
edx_proctoring/api.py
+11
-1
edx_proctoring/serializers.py
+1
-1
edx_proctoring/static/proctoring/clippy.swf
+0
-0
edx_proctoring/static/proctoring/js/models/proctored_exam_model.js
+5
-5
edx_proctoring/static/proctoring/js/views/proctored_exam_view.js
+24
-1
edx_proctoring/templates/proctoring/seq_proctored_exam_instructions.html
+49
-1
edx_proctoring/templates/proctoring/seq_timed_exam_expired.html
+1
-1
edx_proctoring/tests/test_views.py
+2
-0
edx_proctoring/views.py
+32
-7
No files found.
README.md
View file @
edc4d2a1
...
@@ -2,6 +2,6 @@ edx-proctoring [:
...
@@ -446,10 +446,20 @@ def get_student_view(user_id, course_id, content_id, context):
template
=
loader
.
get_template
(
student_view_template
)
template
=
loader
.
get_template
(
student_view_template
)
django_context
=
Context
(
context
)
django_context
=
Context
(
context
)
total_time
=
humanized_time
(
context
[
'default_time_limit_mins'
])
total_time
=
humanized_time
(
context
[
'default_time_limit_mins'
])
progress_page_url
=
''
try
:
progress_page_url
=
reverse
(
'courseware.views.progress'
,
args
=
[
course_id
]
)
except
NoReverseMatch
:
pass
django_context
.
update
({
django_context
.
update
({
'platform_name'
:
settings
.
PLATFORM_NAME
,
'platform_name'
:
settings
.
PLATFORM_NAME
,
'total_time'
:
total_time
,
'total_time'
:
total_time
,
'exam_id'
:
exam_id
,
'exam_id'
:
exam_id
,
'progress_page_url'
:
progress_page_url
,
'enter_exam_endpoint'
:
reverse
(
'edx_proctoring.proctored_exam.attempt.collection'
),
'enter_exam_endpoint'
:
reverse
(
'edx_proctoring.proctored_exam.attempt.collection'
),
'exam_started_poll_url'
:
reverse
(
'exam_started_poll_url'
:
reverse
(
'edx_proctoring.proctored_exam.attempt'
,
'edx_proctoring.proctored_exam.attempt'
,
...
...
edx_proctoring/serializers.py
View file @
edc4d2a1
...
@@ -78,7 +78,7 @@ class ProctoredExamStudentAttemptSerializer(serializers.ModelSerializer):
...
@@ -78,7 +78,7 @@ class ProctoredExamStudentAttemptSerializer(serializers.ModelSerializer):
fields
=
(
fields
=
(
"id"
,
"created"
,
"modified"
,
"user_id"
,
"started_at"
,
"completed_at"
,
"id"
,
"created"
,
"modified"
,
"user_id"
,
"started_at"
,
"completed_at"
,
"external_id"
,
"status"
,
"proctored_exam_id"
,
"allowed_time_limit_mins"
,
"external_id"
,
"status"
,
"proctored_exam_id"
,
"allowed_time_limit_mins"
,
"attempt_code"
,
"is_sample_attempt"
"attempt_code"
,
"is_sample_attempt"
,
"taking_as_proctored"
)
)
...
...
edx_proctoring/static/proctoring/clippy.swf
0 → 100644
View file @
edc4d2a1
File added
edx_proctoring/static/proctoring/js/models/proctored_exam_model.js
View file @
edc4d2a1
...
@@ -5,12 +5,12 @@
...
@@ -5,12 +5,12 @@
defaults
:
{
defaults
:
{
in_timed_exam
:
false
,
in_timed_exam
:
false
,
i
s_proctored
:
false
,
taking_a
s_proctored
:
false
,
exam_display_name
:
''
,
exam_display_name
:
''
,
exam_url_path
:
''
,
exam_url_path
:
''
,
time_remaining_seconds
:
0
,
time_remaining_seconds
:
0
,
low_threshold
:
0
,
low_threshold
_sec
:
0
,
critically_low_threshold
:
0
,
critically_low_threshold
_sec
:
0
,
lastFetched
:
new
Date
()
lastFetched
:
new
Date
()
},
},
getRemainingSeconds
:
function
()
{
getRemainingSeconds
:
function
()
{
...
@@ -35,10 +35,10 @@
...
@@ -35,10 +35,10 @@
},
},
getRemainingTimeState
:
function
()
{
getRemainingTimeState
:
function
()
{
var
totalSeconds
=
this
.
getRemainingSeconds
();
var
totalSeconds
=
this
.
getRemainingSeconds
();
if
(
totalSeconds
>
this
.
get
(
'low_threshold'
))
{
if
(
totalSeconds
>
this
.
get
(
'low_threshold
_sec
'
))
{
return
""
;
return
""
;
}
}
else
if
(
totalSeconds
<=
this
.
get
(
'low_threshold
'
)
&&
totalSeconds
>
this
.
get
(
'critically_low_threshold
'
))
{
else
if
(
totalSeconds
<=
this
.
get
(
'low_threshold
_sec'
)
&&
totalSeconds
>
this
.
get
(
'critically_low_threshold_sec
'
))
{
return
"low-time warning"
;
return
"low-time warning"
;
}
}
else
{
else
{
...
...
edx_proctoring/static/proctoring/js/views/proctored_exam_view.js
View file @
edc4d2a1
...
@@ -16,6 +16,10 @@ var edx = edx || {};
...
@@ -16,6 +16,10 @@ var edx = edx || {};
/* 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
;
// we need to keep a copy here because the model will
// get destroyed before onbeforeunload is called
this
.
taking_as_proctored
=
false
;
var
template_html
=
$
(
this
.
templateId
).
text
();
var
template_html
=
$
(
this
.
templateId
).
text
();
if
(
template_html
!==
null
)
{
if
(
template_html
!==
null
)
{
/* don't assume this backbone view is running on a page with the underscore templates */
/* don't assume this backbone view is running on a page with the underscore templates */
...
@@ -24,12 +28,24 @@ var edx = edx || {};
...
@@ -24,12 +28,24 @@ var edx = edx || {};
/* 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
);
$
(
window
).
unbind
(
'beforeunload'
,
this
.
unloadMessage
);
/* 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
()
{
// if we are a proctored exam, then we need to alert user that he/she
// should not leave the exam
if
(
this
.
model
.
get
(
'taking_as_proctored'
)
&&
this
.
model
.
get
(
'time_remaining_seconds'
)
>
0
)
{
$
(
window
).
bind
(
'beforeunload'
,
this
.
unloadMessage
);
}
else
{
// remove callback on unload event
$
(
window
).
unbind
(
'beforeunload'
,
this
.
unloadMessage
);
}
this
.
render
();
this
.
render
();
},
},
render
:
function
()
{
render
:
function
()
{
...
@@ -44,13 +60,20 @@ var edx = edx || {};
...
@@ -44,13 +60,20 @@ var edx = edx || {};
}
}
return
this
;
return
this
;
},
},
unloadMessage
:
function
()
{
return
"As you are currently taking a proctored exam,
\
n"
+
"you should not be navigation away from the exam.
\
n"
+
"This may be considered as a violation of the
\
n"
+
"proctored exam and you may be disqualified for
\
n"
+
"credit eligibility in this course.
\
n"
;
},
updateRemainingTime
:
function
(
self
)
{
updateRemainingTime
:
function
(
self
)
{
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
());
if
(
self
.
model
.
getRemainingSeconds
()
<=
-
self
.
grace_period_secs
)
{
if
(
self
.
model
.
getRemainingSeconds
()
<=
-
self
.
grace_period_secs
)
{
clearInterval
(
self
.
timerId
);
// stop the timer once the time finishes.
clearInterval
(
self
.
timerId
);
// stop the timer once the time finishes.
$
(
window
).
unbind
(
'beforeunload'
,
this
.
unloadMessage
);
// refresh the page when the timer expired
// refresh the page when the timer expired
location
.
reload
();
location
.
reload
();
}
}
...
...
edx_proctoring/templates/proctoring/seq_proctored_exam_instructions.html
View file @
edc4d2a1
...
@@ -17,7 +17,7 @@
...
@@ -17,7 +17,7 @@
Here is your unique exam code. You'll be asked for it during the setup.
Here is your unique exam code. You'll be asked for it during the setup.
{% endblocktrans %}
{% endblocktrans %}
</h3>
</h3>
<h
1>
{{exam_code}}
</h1
>
<h
2>
{{exam_code}}
<span
class=
'copy-to-clipboard'
></span></h2
>
<p>
<p>
{% blocktrans %}
{% blocktrans %}
Please do not share this code. It can only be used once and it tied to your {{platform_name}} account.
Please do not share this code. It can only be used once and it tied to your {{platform_name}} account.
...
@@ -43,6 +43,48 @@
...
@@ -43,6 +43,48 @@
<script
type=
"text/javascript"
>
<script
type=
"text/javascript"
>
$
(
document
).
ready
(
function
(){
$
(
document
).
ready
(
function
(){
var
hasFlash
=
false
;
try
{
var
fo
=
new
ActiveXObject
(
'ShockwaveFlash.ShockwaveFlash'
);
if
(
fo
)
{
hasFlash
=
true
;
}
}
catch
(
e
)
{
if
(
navigator
.
mimeTypes
&&
navigator
.
mimeTypes
[
'application/x-shockwave-flash'
]
!=
undefined
&&
navigator
.
mimeTypes
[
'application/x-shockwave-flash'
].
enabledPlugin
)
{
hasFlash
=
true
;
}
}
if
(
hasFlash
)
{
$
(
'.copy-to-clipboard'
).
html
(
'<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"
\
width="110"
\
height="14"
\
id="clippy" >
\
<param name="movie" value="/static/proctoring/clippy.swf"/>
\
<param name="allowScriptAccess" value="always" />
\
<param name="quality" value="high" />
\
<param name="scale" value="noscale" />
\
<param NAME="FlashVars" value="text={{exam_code}}">
\
<param name="bgcolor" value="#F2F4F5">
\
<embed src="/static/proctoring/clippy.swf"
\
width="110"
\
height="14"
\
name="clippy"
\
quality="high"
\
allowScriptAccess="always"
\
type="application/x-shockwave-flash"
\
pluginspage="http://www.macromedia.com/go/getflashplayer"
\
FlashVars="text={{exam_code}}"
\
bgcolor="#F2F4F5"
\
/>
\
</object>'
);
}
setInterval
(
setInterval
(
poll_exam_started
,
poll_exam_started
,
5000
5000
...
@@ -53,6 +95,12 @@
...
@@ -53,6 +95,12 @@
var
url
=
$
(
'.instructions'
).
data
(
'exam-started-poll-url'
)
var
url
=
$
(
'.instructions'
).
data
(
'exam-started-poll-url'
)
$
.
ajax
(
url
).
success
(
function
(
data
){
$
.
ajax
(
url
).
success
(
function
(
data
){
if
(
data
.
started_at
!==
null
)
{
if
(
data
.
started_at
!==
null
)
{
// Let the student know exam has started and clock is running.
// this may or may not bring the browser window back to the
// foreground (depending on browser as well as user settings)
alert
(
'{% trans "Your proctored exam has started, please click OK to enter into your exam." %}'
)
// Reloading page will reflect the new state of the attempt
location
.
reload
()
location
.
reload
()
}
}
});
});
...
...
edx_proctoring/templates/proctoring/seq_timed_exam_expired.html
View file @
edc4d2a1
...
@@ -15,7 +15,7 @@
...
@@ -15,7 +15,7 @@
<div
class=
"proctored-exam-message"
>
<div
class=
"proctored-exam-message"
>
<p>
<p>
{% blocktrans %}
{% blocktrans %}
Please see
<a
href=
"
#
"
>
your progress in this course
</a>
for your general course credit worthiness.
Please see
<a
href=
"
{{progress_page_url}}
"
>
your progress in this course
</a>
for your general course credit worthiness.
{% endblocktrans %}
{% endblocktrans %}
</p>
</p>
</div>
</div>
...
...
edx_proctoring/tests/test_views.py
View file @
edc4d2a1
...
@@ -633,6 +633,8 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
...
@@ -633,6 +633,8 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
data
=
json
.
loads
(
response
.
content
)
data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
data
[
'exam_display_name'
],
'Test Exam'
)
self
.
assertEqual
(
data
[
'exam_display_name'
],
'Test Exam'
)
self
.
assertEqual
(
data
[
'low_threshold_sec'
],
1080
)
self
.
assertEqual
(
data
[
'critically_low_threshold_sec'
],
270
)
def
test_get_expired_attempt
(
self
):
def
test_get_expired_attempt
(
self
):
"""
"""
...
...
edx_proctoring/views.py
View file @
edc4d2a1
...
@@ -7,6 +7,8 @@ import pytz
...
@@ -7,6 +7,8 @@ import pytz
from
datetime
import
datetime
,
timedelta
from
datetime
import
datetime
,
timedelta
from
django.utils.decorators
import
method_decorator
from
django.utils.decorators
import
method_decorator
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
,
NoReverseMatch
from
rest_framework
import
status
from
rest_framework
import
status
from
rest_framework.response
import
Response
from
rest_framework.response
import
Response
...
@@ -364,10 +366,13 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView):
...
@@ -364,10 +366,13 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView):
exams
=
get_active_exams_for_user
(
request
.
user
.
id
)
exams
=
get_active_exams_for_user
(
request
.
user
.
id
)
if
exams
:
if
exams
:
exam
=
exams
[
0
]
exam_info
=
exams
[
0
]
exam
=
exam_info
[
'exam'
]
attempt
=
exam_info
[
'attempt'
]
# need to adjust for allowances
# need to adjust for allowances
expires_at
=
exam
[
'attempt'
][
'started_at'
]
+
timedelta
(
minutes
=
exam
[
'attempt'
]
[
'allowed_time_limit_mins'
])
expires_at
=
attempt
[
'started_at'
]
+
timedelta
(
minutes
=
attempt
[
'allowed_time_limit_mins'
])
now_utc
=
datetime
.
now
(
pytz
.
UTC
)
now_utc
=
datetime
.
now
(
pytz
.
UTC
)
if
expires_at
>
now_utc
:
if
expires_at
>
now_utc
:
...
@@ -375,14 +380,34 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView):
...
@@ -375,14 +380,34 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView):
else
:
else
:
time_remaining_seconds
=
0
time_remaining_seconds
=
0
proctoring_settings
=
getattr
(
settings
,
'PROCTORING_SETTINGS'
,
{})
low_threshold_pct
=
proctoring_settings
.
get
(
'low_threshold_pct'
,
.
2
)
critically_low_threshold_pct
=
proctoring_settings
.
get
(
'critically_low_threshold_pct'
,
.
05
)
low_threshold
=
int
(
low_threshold_pct
*
float
(
attempt
[
'allowed_time_limit_mins'
])
*
60
)
critically_low_threshold
=
int
(
critically_low_threshold_pct
*
float
(
attempt
[
'allowed_time_limit_mins'
])
*
60
)
exam_url_path
=
''
try
:
# resolve the LMS url, note we can't assume we're running in
# a same process as the LMS
exam_url_path
=
reverse
(
'courseware.views.jump_to'
,
args
=
[
exam
[
'course_id'
],
exam
[
'content_id'
]]
)
except
NoReverseMatch
:
pass
response_dict
=
{
response_dict
=
{
'in_timed_exam'
:
True
,
'in_timed_exam'
:
True
,
'
is_proctored'
:
True
,
'
taking_as_proctored'
:
attempt
[
'taking_as_proctored'
]
,
'exam_display_name'
:
exam
[
'exam
'
][
'exam
_name'
],
'exam_display_name'
:
exam
[
'exam_name'
],
'exam_url_path'
:
''
,
'exam_url_path'
:
exam_url_path
,
'time_remaining_seconds'
:
time_remaining_seconds
,
'time_remaining_seconds'
:
time_remaining_seconds
,
'low_threshold
'
:
30
,
'low_threshold
_sec'
:
low_threshold
,
'critically_low_threshold
'
:
15
,
'critically_low_threshold
_sec'
:
critically_low_threshold
,
}
}
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