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
fcaf59df
Commit
fcaf59df
authored
Dec 09, 2015
by
chrisndodge
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #241 from edx/hasnain-naveed/PHX-216
PHX-216 / user can visit the exam once due has passed.
parents
5825f78b
e723816a
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
290 additions
and
7 deletions
+290
-7
edx_proctoring/api.py
+34
-4
edx_proctoring/migrations/0002_proctoredexamstudentattempt_is_status_acknowledged.py
+19
-0
edx_proctoring/models.py
+4
-0
edx_proctoring/serializers.py
+1
-1
edx_proctoring/templates/proctored_exam/rejected.html
+2
-0
edx_proctoring/templates/proctored_exam/submitted.html
+2
-0
edx_proctoring/templates/proctored_exam/verified.html
+2
-0
edx_proctoring/templates/proctored_exam/visit_exam_content.html
+40
-0
edx_proctoring/tests/test_api.py
+55
-0
edx_proctoring/tests/test_views.py
+85
-1
edx_proctoring/urls.py
+5
-0
edx_proctoring/views.py
+41
-1
No files found.
edx_proctoring/api.py
View file @
fcaf59df
...
...
@@ -439,10 +439,19 @@ def update_exam_attempt(attempt_id, **kwargs):
update exam_attempt
"""
exam_attempt_obj
=
ProctoredExamStudentAttempt
.
objects
.
get_exam_attempt_by_id
(
attempt_id
)
if
not
exam_attempt_obj
:
err_msg
=
(
'Attempted to access of attempt object with attempt_id {attempt_id} but '
'it does not exist.'
.
format
(
attempt_id
=
attempt_id
)
)
raise
StudentExamAttemptDoesNotExistsException
(
err_msg
)
for
key
,
value
in
kwargs
.
items
():
# only allow a limit set of fields to update
# namely because status transitions can trigger workflow
if
key
not
in
[
'last_poll_timestamp'
,
'last_poll_ipaddr'
]:
if
key
not
in
[
'last_poll_timestamp'
,
'last_poll_ipaddr'
,
'is_status_acknowledged'
]:
err_msg
=
(
'You cannot call into update_exam_attempt to change '
'field {key}'
.
format
(
key
=
key
)
...
...
@@ -463,6 +472,13 @@ def _has_due_date_passed(due_datetime):
return
False
def
_was_review_status_acknowledged
(
is_status_acknowledged
,
due_datetime
):
"""
return True if review status has been acknowledged and due date has been passed
"""
return
is_status_acknowledged
and
_has_due_date_passed
(
due_datetime
)
def
_create_and_decline_attempt
(
exam_id
,
user_id
):
"""
It will create the exam attempt and change the attempt's status to decline.
...
...
@@ -1490,6 +1506,7 @@ def _get_proctored_exam_context(exam, attempt, course_id, is_practice_exam=False
'exam_id'
:
exam
[
'id'
],
'progress_page_url'
:
progress_page_url
,
'is_sample_attempt'
:
is_practice_exam
,
'has_due_date_passed'
:
_has_due_date_passed
(
exam
[
'due_date'
]),
'does_time_remain'
:
_does_time_remain
(
attempt
),
'enter_exam_endpoint'
:
reverse
(
'edx_proctoring.proctored_exam.attempt.collection'
),
'exam_started_poll_url'
:
reverse
(
...
...
@@ -1500,6 +1517,10 @@ def _get_proctored_exam_context(exam, attempt, course_id, is_practice_exam=False
'edx_proctoring.proctored_exam.attempt'
,
args
=
[
attempt
[
'id'
]]
)
if
attempt
else
''
,
'update_is_status_acknowledge_url'
:
reverse
(
'edx_proctoring.proctored_exam.attempt.review_status'
,
args
=
[
attempt
[
'id'
]]
)
if
attempt
else
''
,
'link_urls'
:
settings
.
PROCTORING_SETTINGS
.
get
(
'LINK_URLS'
,
{}),
}
...
...
@@ -1658,13 +1679,22 @@ def _get_proctored_exam_view(exam, context, exam_id, user_id, course_id):
raise
NotImplementedError
(
'There is no defined rendering for ProctoredExamStudentAttemptStatus.timed_out!'
)
elif
attempt_status
==
ProctoredExamStudentAttemptStatus
.
submitted
:
if
has_client_app_shutdown
(
attempt
):
student_view_template
=
'proctored_exam/submitted.html'
student_view_template
=
None
if
_was_review_status_acknowledged
(
attempt
[
'is_status_acknowledged'
],
exam
[
'due_date'
]
)
else
'proctored_exam/submitted.html'
else
:
student_view_template
=
'proctored_exam/waiting_for_app_shutdown.html'
elif
attempt_status
==
ProctoredExamStudentAttemptStatus
.
verified
:
student_view_template
=
'proctored_exam/verified.html'
student_view_template
=
None
if
_was_review_status_acknowledged
(
attempt
[
'is_status_acknowledged'
],
exam
[
'due_date'
]
)
else
'proctored_exam/verified.html'
elif
attempt_status
==
ProctoredExamStudentAttemptStatus
.
rejected
:
student_view_template
=
'proctored_exam/rejected.html'
student_view_template
=
None
if
_was_review_status_acknowledged
(
attempt
[
'is_status_acknowledged'
],
exam
[
'due_date'
]
)
else
'proctored_exam/rejected.html'
elif
attempt_status
==
ProctoredExamStudentAttemptStatus
.
ready_to_submit
:
student_view_template
=
'proctored_exam/ready_to_submit.html'
...
...
edx_proctoring/migrations/0002_proctoredexamstudentattempt_is_status_acknowledged.py
0 → 100644
View file @
fcaf59df
# -*- coding: utf-8 -*-
from
__future__
import
unicode_literals
from
django.db
import
migrations
,
models
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'edx_proctoring'
,
'0001_initial'
),
]
operations
=
[
migrations
.
AddField
(
model_name
=
'proctoredexamstudentattempt'
,
name
=
'is_status_acknowledged'
,
field
=
models
.
BooleanField
(
default
=
False
),
),
]
edx_proctoring/models.py
View file @
fcaf59df
...
...
@@ -455,6 +455,10 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
# this ID might point to a record that is in the History table
review_policy_id
=
models
.
IntegerField
(
null
=
True
)
# if student has press the button to explore the exam then true
# else always false
is_status_acknowledged
=
models
.
BooleanField
(
default
=
False
)
class
Meta
:
""" Meta class for this Django model """
db_table
=
'proctoring_proctoredexamstudentattempt'
...
...
edx_proctoring/serializers.py
View file @
fcaf59df
...
...
@@ -81,7 +81,7 @@ class ProctoredExamStudentAttemptSerializer(serializers.ModelSerializer):
"id"
,
"created"
,
"modified"
,
"user"
,
"started_at"
,
"completed_at"
,
"external_id"
,
"status"
,
"proctored_exam"
,
"allowed_time_limit_mins"
,
"attempt_code"
,
"is_sample_attempt"
,
"taking_as_proctored"
,
"last_poll_timestamp"
,
"last_poll_ipaddr"
,
"review_policy_id"
,
"student_name"
"last_poll_ipaddr"
,
"review_policy_id"
,
"student_name"
,
"is_status_acknowledged"
)
...
...
edx_proctoring/templates/proctored_exam/rejected.html
View file @
fcaf59df
...
...
@@ -6,6 +6,8 @@
{% endblocktrans %}
</h3>
{% include 'proctored_exam/visit_exam_content.html' %}
<p>
{% blocktrans %}
You are no longer eligible for academic credit for this course, regardless of your final grade.
...
...
edx_proctoring/templates/proctored_exam/submitted.html
View file @
fcaf59df
...
...
@@ -6,6 +6,8 @@
{% endblocktrans %}
</h3>
{% include 'proctored_exam/visit_exam_content.html' %}
<p>
{% blocktrans %}
•
After you quit the proctoring session, the recorded data is uploaded for review.
</br>
...
...
edx_proctoring/templates/proctored_exam/verified.html
View file @
fcaf59df
...
...
@@ -6,6 +6,8 @@
{% endblocktrans %}
</h3>
{% include 'proctored_exam/visit_exam_content.html' %}
<p>
{% blocktrans %}
You are eligible to purchase academic credit for this course if you complete all required exams
...
...
edx_proctoring/templates/proctored_exam/visit_exam_content.html
0 → 100644
View file @
fcaf59df
{% load i18n %}
{% if has_due_date_passed %}
<p>
{% blocktrans %}
If you want to view your exam questions and responses, click on the button “let me see my exam”.
When you clicked the button, you will be taken directly to the exam content. The exam’s status will
still be visible to you in the left navigation pane.
{% endblocktrans %}
</p>
<p>
<button
type=
"button"
name=
"visit-exam-content"
class=
"visit-exam-button exam-action-button btn btn-pl-primary btn-base"
data-action-url=
"{{update_is_status_acknowledge_url}}"
>
{% trans "Let me see my exam" %}
</button>
<div
class=
"clearfix"
></div>
</p>
<script
type=
"text/javascript"
>
$
(
'.visit-exam-button'
).
click
(
function
(
event
)
{
// cancel any warning messages to end user about leaving proctored exam
$
(
window
).
unbind
(
'beforeunload'
);
var
action_url
=
$
(
this
).
data
(
'action-url'
);
// Update the state of the attempt
$
.
ajax
({
url
:
action_url
,
type
:
'PUT'
,
data
:
{},
success
:
function
()
{
// Reloading page will reflect the new state of the attempt
location
.
reload
()
}
});
}
);
</script>
{% endif %}
edx_proctoring/tests/test_api.py
View file @
fcaf59df
...
...
@@ -1548,6 +1548,61 @@ class ProctoredExamApiTests(LoggedInTestCase):
)
self
.
assertIn
(
self
.
proctored_exam_submitted_msg
,
rendered_response
)
def
test_get_studentview_submitted_status_with_duedate
(
self
):
"""
Test for get_student_view proctored exam which has been submitted
And due date has passed
"""
proctored_exam
=
ProctoredExam
.
objects
.
create
(
course_id
=
'a/b/c'
,
content_id
=
'test_content'
,
exam_name
=
'Test Exam'
,
external_id
=
'123aXqe3'
,
time_limit_mins
=
30
,
is_proctored
=
True
,
is_active
=
True
,
due_date
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
minutes
=
40
)
)
exam_attempt
=
ProctoredExamStudentAttempt
.
objects
.
create
(
proctored_exam
=
proctored_exam
,
user
=
self
.
user
,
allowed_time_limit_mins
=
30
,
taking_as_proctored
=
True
,
external_id
=
proctored_exam
.
external_id
,
status
=
ProctoredExamStudentAttemptStatus
.
submitted
,
last_poll_timestamp
=
datetime
.
now
(
pytz
.
UTC
)
)
# due date is after 10 minutes
reset_time
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
minutes
=
20
)
with
freeze_time
(
reset_time
):
rendered_response
=
get_student_view
(
user_id
=
self
.
user
.
id
,
course_id
=
'a/b/c'
,
content_id
=
'test_content'
,
context
=
{
'is_proctored'
:
True
,
'display_name'
:
'Test Exam'
,
'default_time_limit_mins'
:
30
}
)
self
.
assertIn
(
self
.
proctored_exam_submitted_msg
,
rendered_response
)
exam_attempt
.
is_status_acknowledged
=
True
exam_attempt
.
save
()
rendered_response
=
get_student_view
(
user_id
=
self
.
user
.
id
,
course_id
=
'a/b/c'
,
content_id
=
'test_content'
,
context
=
{
'is_proctored'
:
True
,
'display_name'
:
'Test Exam'
,
'default_time_limit_mins'
:
30
}
)
self
.
assertIsNotNone
(
rendered_response
)
def
test_get_studentview_submitted_status_practiceexam
(
self
):
"""
Test for get_student_view practice exam which has been submitted.
...
...
edx_proctoring/tests/test_views.py
View file @
fcaf59df
...
...
@@ -22,7 +22,7 @@ from edx_proctoring.models import (
)
from
edx_proctoring.exceptions
import
(
ProctoredExamIllegalStatusTransition
,
)
StudentExamAttemptDoesNotExistsException
,
ProctoredExamPermissionDenied
)
from
edx_proctoring.views
import
require_staff
,
require_course_or_global_staff
from
edx_proctoring.api
import
(
create_exam
,
...
...
@@ -1955,6 +1955,90 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
data
[
'exam_type'
],
expected_exam_type
)
def
_create_proctored_exam_attempt_with_duedate
(
self
,
due_date
=
datetime
.
now
(
pytz
.
UTC
),
user
=
None
):
"""
Test the ProctoredExamAttemptReviewStatus view
Create the proctored exam with due date
"""
proctored_exam
=
ProctoredExam
.
objects
.
create
(
course_id
=
'a/b/c'
,
external_id
=
'123aXqe3'
,
time_limit_mins
=
30
,
is_proctored
=
True
,
due_date
=
due_date
)
return
ProctoredExamStudentAttempt
.
objects
.
create
(
proctored_exam
=
proctored_exam
,
user
=
user
if
user
else
self
.
user
,
allowed_time_limit_mins
=
30
,
taking_as_proctored
=
True
,
external_id
=
proctored_exam
.
external_id
,
status
=
ProctoredExamStudentAttemptStatus
.
started
)
def
test_attempt_review_status_callback
(
self
):
"""
Test the ProctoredExamAttemptReviewStatus view
"""
attempt
=
self
.
_create_proctored_exam_attempt_with_duedate
(
due_date
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
minutes
=
40
)
)
response
=
self
.
client
.
put
(
reverse
(
'edx_proctoring.proctored_exam.attempt.review_status'
,
args
=
[
attempt
.
id
]
),
{},
content_type
=
'application/json'
)
self
.
assertEqual
(
response
.
status_code
,
200
)
def
test_attempt_review_status_callback_with_doesnotexit_exception
(
self
):
"""
Test the ProctoredExamAttemptReviewStatus view with does not exit exception
"""
self
.
_create_proctored_exam_attempt_with_duedate
(
due_date
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
minutes
=
40
)
)
response
=
self
.
client
.
put
(
reverse
(
'edx_proctoring.proctored_exam.attempt.review_status'
,
args
=
[
'5'
]
),
{},
content_type
=
'application/json'
)
self
.
assertEqual
(
response
.
status_code
,
400
)
self
.
assertRaises
(
StudentExamAttemptDoesNotExistsException
)
def
test_attempt_review_status_callback_with_permission_exception
(
self
):
"""
Test the ProctoredExamAttemptReviewStatus view with permission exception
"""
# creating new user for creating exam attempt
user
=
User
(
username
=
'tester_'
,
email
=
'tester@test.com_'
)
user
.
save
()
attempt
=
self
.
_create_proctored_exam_attempt_with_duedate
(
due_date
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
minutes
=
40
),
user
=
user
)
response
=
self
.
client
.
put
(
reverse
(
'edx_proctoring.proctored_exam.attempt.review_status'
,
args
=
[
attempt
.
id
]
),
{},
content_type
=
'application/json'
)
self
.
assertEqual
(
response
.
status_code
,
400
)
self
.
assertRaises
(
ProctoredExamPermissionDenied
)
class
TestExamAllowanceView
(
LoggedInTestCase
):
"""
...
...
edx_proctoring/urls.py
View file @
fcaf59df
...
...
@@ -52,6 +52,11 @@ urlpatterns = patterns( # pylint: disable=invalid-name
name
=
'edx_proctoring.proctored_exam.attempt.collection'
),
url
(
r'edx_proctoring/v1/proctored_exam/attempt/(?P<attempt_id>\d+)/review_status$'
,
views
.
ProctoredExamAttemptReviewStatus
.
as_view
(),
name
=
'edx_proctoring.proctored_exam.attempt.review_status'
),
url
(
r'edx_proctoring/v1/proctored_exam/{}/allowance$'
.
format
(
settings
.
COURSE_ID_PATTERN
),
views
.
ExamAllowanceView
.
as_view
(),
name
=
'edx_proctoring.proctored_exam.allowance'
...
...
edx_proctoring/views.py
View file @
fcaf59df
...
...
@@ -29,7 +29,8 @@ from edx_proctoring.api import (
get_all_exams_for_course
,
get_exam_attempt_by_id
,
remove_exam_attempt
,
update_attempt_status
update_attempt_status
,
update_exam_attempt
)
from
edx_proctoring.exceptions
import
(
ProctoredBaseException
,
...
...
@@ -782,3 +783,42 @@ class ActiveExamsForUserView(AuthenticatedAPIView):
user_id
=
request
.
data
.
get
(
'user_id'
,
None
),
course_id
=
request
.
data
.
get
(
'course_id'
,
None
)
))
class
ProctoredExamAttemptReviewStatus
(
AuthenticatedAPIView
):
"""
Endpoint for updating exam attempt's review status to acknowledged.
edx_proctoring/v1/proctored_exam/attempt/(<attempt_id>)/review_status$
Supports:
HTTP PUT: Update the is_status_acknowledge flag
"""
def
put
(
self
,
request
,
attempt_id
):
# pylint: disable=unused-argument
"""
Update the is_status_acknowledge flag for the specific attempt
"""
try
:
attempt
=
get_exam_attempt_by_id
(
attempt_id
)
# make sure the the attempt belongs to the calling user_id
if
attempt
and
attempt
[
'user'
][
'id'
]
!=
request
.
user
.
id
:
err_msg
=
(
'Attempted to access attempt_id {attempt_id} but '
'does not have access to it.'
.
format
(
attempt_id
=
attempt_id
)
)
raise
ProctoredExamPermissionDenied
(
err_msg
)
update_exam_attempt
(
attempt_id
,
is_status_acknowledged
=
True
)
return
Response
(
status
=
status
.
HTTP_200_OK
)
except
(
StudentExamAttemptDoesNotExistsException
,
ProctoredExamPermissionDenied
)
as
ex
:
LOG
.
exception
(
ex
)
return
Response
(
status
=
status
.
HTTP_400_BAD_REQUEST
,
data
=
{
"detail"
:
str
(
ex
)}
)
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