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
da76a638
Commit
da76a638
authored
Apr 21, 2016
by
Eric Fischer
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #288 from edx/efischer/hide_timed_exams
TNL-4366 Add ability to hide timed exam after due date
parents
fcd3a1d6
ff1b3d15
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
85 additions
and
26 deletions
+85
-26
edx_proctoring/api.py
+16
-11
edx_proctoring/migrations/0005_proctoredexam_hide_after_due.py
+19
-0
edx_proctoring/models.py
+3
-0
edx_proctoring/serializers.py
+3
-1
edx_proctoring/templates/timed_exam/submitted.html
+1
-1
edx_proctoring/tests/test_api.py
+33
-8
edx_proctoring/tests/test_serializer.py
+2
-1
edx_proctoring/tests/test_views.py
+4
-2
edx_proctoring/views.py
+3
-1
setup.py
+1
-1
No files found.
edx_proctoring/api.py
View file @
da76a638
...
...
@@ -57,7 +57,7 @@ SHOW_EXPIRY_MESSAGE_DURATION = 1 * 60 # duration within which expiry message is
def
create_exam
(
course_id
,
content_id
,
exam_name
,
time_limit_mins
,
due_date
=
None
,
is_proctored
=
True
,
is_practice_exam
=
False
,
external_id
=
None
,
is_active
=
True
):
is_proctored
=
True
,
is_practice_exam
=
False
,
external_id
=
None
,
is_active
=
True
,
hide_after_due
=
False
):
"""
Creates a new ProctoredExam entity, if the course_id/content_id pair do not already exist.
If that pair already exists, then raise exception.
...
...
@@ -77,19 +77,20 @@ def create_exam(course_id, content_id, exam_name, time_limit_mins, due_date=None
due_date
=
due_date
,
is_proctored
=
is_proctored
,
is_practice_exam
=
is_practice_exam
,
is_active
=
is_active
is_active
=
is_active
,
hide_after_due
=
hide_after_due
,
)
log_msg
=
(
u'Created exam ({exam_id}) with parameters: course_id={course_id}, '
u'content_id={content_id}, exam_name={exam_name}, time_limit_mins={time_limit_mins}, '
u'is_proctored={is_proctored}, is_practice_exam={is_practice_exam}, '
u'external_id={external_id}, is_active={is_active}'
.
format
(
u'external_id={external_id}, is_active={is_active}
, hide_after_due={hide_after_due}
'
.
format
(
exam_id
=
proctored_exam
.
id
,
course_id
=
course_id
,
content_id
=
content_id
,
exam_name
=
exam_name
,
time_limit_mins
=
time_limit_mins
,
is_proctored
=
is_proctored
,
is_practice_exam
=
is_practice_exam
,
external_id
=
external_id
,
is_active
=
is_active
external_id
=
external_id
,
is_active
=
is_active
,
hide_after_due
=
hide_after_due
)
)
log
.
info
(
log_msg
)
...
...
@@ -202,7 +203,7 @@ def get_review_policy_by_exam_id(exam_id):
def
update_exam
(
exam_id
,
exam_name
=
None
,
time_limit_mins
=
None
,
due_date
=
constants
.
MINIMUM_TIME
,
is_proctored
=
None
,
is_practice_exam
=
None
,
external_id
=
None
,
is_active
=
None
):
is_proctored
=
None
,
is_practice_exam
=
None
,
external_id
=
None
,
is_active
=
None
,
hide_after_due
=
None
):
"""
Given a Django ORM id, update the existing record, otherwise raise exception if not found.
If an argument is not passed in, then do not change it's current value.
...
...
@@ -214,10 +215,10 @@ def update_exam(exam_id, exam_name=None, time_limit_mins=None, due_date=constant
u'Updating exam_id {exam_id} with parameters '
u'exam_name={exam_name}, time_limit_mins={time_limit_mins}, due_date={due_date}'
u'is_proctored={is_proctored}, is_practice_exam={is_practice_exam}, '
u'external_id={external_id}, is_active={is_active}'
.
format
(
u'external_id={external_id}, is_active={is_active}
, hide_after_due={hide_after_due}
'
.
format
(
exam_id
=
exam_id
,
exam_name
=
exam_name
,
time_limit_mins
=
time_limit_mins
,
due_date
=
due_date
,
is_proctored
=
is_proctored
,
is_practice_exam
=
is_practice_exam
,
external_id
=
external_id
,
is_active
=
is_active
external_id
=
external_id
,
is_active
=
is_active
,
hide_after_due
=
hide_after_due
)
)
log
.
info
(
log_msg
)
...
...
@@ -240,6 +241,8 @@ def update_exam(exam_id, exam_name=None, time_limit_mins=None, due_date=constant
proctored_exam
.
external_id
=
external_id
if
is_active
is
not
None
:
proctored_exam
.
is_active
=
is_active
if
hide_after_due
is
not
None
:
proctored_exam
.
hide_after_due
=
hide_after_due
proctored_exam
.
save
()
# read back exam so we can emit an event on it
...
...
@@ -1444,9 +1447,10 @@ def _get_timed_exam_view(exam, context, exam_id, user_id, course_id):
elif
attempt_status
==
ProctoredExamStudentAttemptStatus
.
ready_to_submit
:
student_view_template
=
'timed_exam/ready_to_submit.html'
elif
attempt_status
==
ProctoredExamStudentAttemptStatus
.
submitted
:
# check if the exam's due_date has passed then we return None
# If we are not hiding the exam after the due_date has passed,
# check if the exam's due_date has passed. If so, return None
# so that the user can see his exam answers in read only mode.
if
has_due_date_passed
(
exam
[
'due_date'
]):
if
not
exam
[
'hide_after_due'
]
and
has_due_date_passed
(
exam
[
'due_date'
]):
return
None
student_view_template
=
'timed_exam/submitted.html'
...
...
@@ -1500,7 +1504,7 @@ def _get_timed_exam_view(exam, context, exam_id, user_id, course_id):
django_context
.
update
({
'total_time'
:
total_time
,
'
has_due_date'
:
has_due_date
,
'
will_be_revealed'
:
has_due_date
and
not
exam
[
'hide_after_due'
]
,
'exam_id'
:
exam_id
,
'exam_name'
:
exam
[
'exam_name'
],
'progress_page_url'
:
progress_page_url
,
...
...
@@ -1816,7 +1820,8 @@ def get_student_view(user_id, course_id, content_id,
time_limit_mins
=
context
[
'default_time_limit_mins'
],
is_proctored
=
context
.
get
(
'is_proctored'
,
False
),
is_practice_exam
=
context
.
get
(
'is_practice_exam'
,
False
),
due_date
=
context
.
get
(
'due_date'
,
None
)
due_date
=
context
.
get
(
'due_date'
,
None
),
hide_after_due
=
context
.
get
(
'hide_after_due'
,
None
),
)
exam
=
get_exam_by_content_id
(
course_id
,
content_id
)
...
...
edx_proctoring/migrations/0005_proctoredexam_hide_after_due.py
0 → 100644
View file @
da76a638
# -*- coding: utf-8 -*-
from
__future__
import
unicode_literals
from
django.db
import
migrations
,
models
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'edx_proctoring'
,
'0004_auto_20160201_0523'
),
]
operations
=
[
migrations
.
AddField
(
model_name
=
'proctoredexam'
,
name
=
'hide_after_due'
,
field
=
models
.
BooleanField
(
default
=
False
),
),
]
edx_proctoring/models.py
View file @
da76a638
...
...
@@ -50,6 +50,9 @@ class ProctoredExam(TimeStampedModel):
# Whether this exam will be active.
is_active
=
models
.
BooleanField
(
default
=
False
)
# Whether to hide this exam after the due date
hide_after_due
=
models
.
BooleanField
(
default
=
False
)
class
Meta
:
""" Meta class for this Django model """
unique_together
=
((
'course_id'
,
'content_id'
),)
...
...
edx_proctoring/serializers.py
View file @
da76a638
...
...
@@ -25,6 +25,7 @@ class ProctoredExamSerializer(serializers.ModelSerializer):
is_practice_exam
=
serializers
.
BooleanField
(
required
=
True
)
is_proctored
=
serializers
.
BooleanField
(
required
=
True
)
due_date
=
serializers
.
DateTimeField
(
required
=
False
,
format
=
None
)
hide_after_due
=
serializers
.
BooleanField
(
required
=
True
)
class
Meta
:
"""
...
...
@@ -34,7 +35,8 @@ class ProctoredExamSerializer(serializers.ModelSerializer):
fields
=
(
"id"
,
"course_id"
,
"content_id"
,
"external_id"
,
"exam_name"
,
"time_limit_mins"
,
"is_proctored"
,
"is_practice_exam"
,
"is_active"
,
"due_date"
"time_limit_mins"
,
"is_proctored"
,
"is_practice_exam"
,
"is_active"
,
"due_date"
,
"hide_after_due"
)
...
...
edx_proctoring/templates/timed_exam/submitted.html
View file @
da76a638
...
...
@@ -18,7 +18,7 @@
{% blocktrans %}
Your grade for this timed exam will be immediately available on the
<a
href=
"{{progress_page_url}}"
>
Progress
</a>
page.
{% endblocktrans %}
{% if
has_due_date
%}
{% if
will_be_revealed
%}
{% blocktrans %}
After the due date has passed, you can review the exam, but you cannot change your answers.
{% endblocktrans %}
...
...
edx_proctoring/tests/test_api.py
View file @
da76a638
...
...
@@ -413,6 +413,18 @@ class ProctoredExamApiTests(LoggedInTestCase):
self
.
assertEqual
(
update_proctored_exam
.
course_id
,
'test_course'
)
self
.
assertEqual
(
update_proctored_exam
.
content_id
,
'test_content_id'
)
def
test_update_timed_exam
(
self
):
"""
test update the existing timed exam
"""
updated_timed_exam_id
=
update_exam
(
self
.
timed_exam_id
,
hide_after_due
=
True
)
self
.
assertEqual
(
self
.
timed_exam_id
,
updated_timed_exam_id
)
update_timed_exam
=
ProctoredExam
.
objects
.
get
(
id
=
updated_timed_exam_id
)
self
.
assertEqual
(
update_timed_exam
.
hide_after_due
,
True
)
def
test_update_non_existing_exam
(
self
):
"""
test to update the non-existing proctored exam
...
...
@@ -1022,7 +1034,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
context
=
{
'is_proctored'
:
True
,
'display_name'
:
self
.
exam_name
,
'default_time_limit_mins'
:
90
'default_time_limit_mins'
:
90
,
'hide_after_due'
:
False
,
}
)
self
.
assertIn
(
...
...
@@ -1041,6 +1054,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
'display_name'
:
self
.
exam_name
,
'default_time_limit_mins'
:
90
,
'is_practice_exam'
:
True
,
'hide_after_due'
:
False
,
}
)
self
.
assertIn
(
self
.
start_a_practice_exam_msg
.
format
(
exam_name
=
self
.
exam_name
),
rendered_response
)
...
...
@@ -1186,7 +1200,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
'is_proctored'
:
False
,
'is_practice_exam'
:
True
,
'display_name'
:
self
.
exam_name
,
'default_time_limit_mins'
:
90
'default_time_limit_mins'
:
90
,
'hide_after_due'
:
False
,
},
user_role
=
'student'
)
...
...
@@ -1209,7 +1224,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
'is_practice_exam'
:
False
,
'display_name'
:
self
.
exam_name
,
'default_time_limit_mins'
:
90
,
'due_date'
:
None
'due_date'
:
None
,
'hide_after_due'
:
False
,
},
user_role
=
'student'
)
...
...
@@ -1250,7 +1266,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
'is_practice_exam'
:
True
,
'display_name'
:
self
.
exam_name
,
'default_time_limit_mins'
:
90
,
'due_date'
:
None
'due_date'
:
None
,
'hide_after_due'
:
False
,
},
user_role
=
'student'
)
...
...
@@ -1436,16 +1453,19 @@ class ProctoredExamApiTests(LoggedInTestCase):
@ddt.data
(
(
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
days
=
1
),
False
),
(
datetime
.
now
(
pytz
.
UTC
)
-
timedelta
(
days
=
1
),
False
),
(
datetime
.
now
(
pytz
.
UTC
)
-
timedelta
(
days
=
1
),
True
),
)
@ddt.unpack
def
test_get_studentview_submitted_timed_exam_with_past_due_date
(
self
,
due_date
,
h
as_due_date_passed
):
def
test_get_studentview_submitted_timed_exam_with_past_due_date
(
self
,
due_date
,
h
ide_after_due
):
"""
Test for get_student_view timed exam with the due date.
"""
# exam is created with due datetime which has already passed
exam_id
=
self
.
_create_exam_with_due_time
(
is_proctored
=
False
,
due_date
=
due_date
)
if
hide_after_due
:
update_exam
(
exam_id
,
hide_after_due
=
hide_after_due
)
# now create the timed_exam attempt in the submitted state
self
.
_create_exam_attempt
(
exam_id
,
status
=
'submitted'
)
...
...
@@ -1461,10 +1481,14 @@ class ProctoredExamApiTests(LoggedInTestCase):
'due_date'
:
due_date
,
}
)
if
not
has_due_date_passed
:
if
datetime
.
now
(
pytz
.
UTC
)
<
due_date
:
self
.
assertIn
(
self
.
timed_exam_submitted
,
rendered_response
)
self
.
assertIn
(
self
.
submitted_timed_exam_msg_with_due_date
,
rendered_response
)
elif
hide_after_due
:
self
.
assertIn
(
self
.
timed_exam_submitted
,
rendered_response
)
self
.
assertNotIn
(
self
.
submitted_timed_exam_msg_with_due_date
,
rendered_response
)
else
:
self
.
assertIsNone
(
Non
e
)
self
.
assertIsNone
(
rendered_respons
e
)
def
test_proctored_exam_attempt_with_past_due_datetime
(
self
):
"""
...
...
@@ -1925,7 +1949,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
context
=
{
'is_proctored'
:
False
,
'display_name'
:
self
.
exam_name
,
'default_time_limit_mins'
:
90
'default_time_limit_mins'
:
90
,
'hide_after_due'
:
False
,
}
)
self
.
assertNotIn
(
...
...
edx_proctoring/tests/test_serializer.py
View file @
da76a638
...
...
@@ -23,7 +23,8 @@ class TestProctoredExamSerializer(unittest.TestCase):
'external_id'
:
'123'
,
'is_proctored'
:
'bla'
,
'is_practice_exam'
:
'bla'
,
'is_active'
:
'f'
'is_active'
:
'f'
,
'hide_after_due'
:
't'
,
}
serializer
=
ProctoredExamSerializer
(
data
=
data
)
...
...
edx_proctoring/tests/test_views.py
View file @
da76a638
...
...
@@ -101,7 +101,8 @@ class ProctoredExamViewTests(LoggedInTestCase):
'external_id'
:
'123'
,
'is_proctored'
:
True
,
'is_practice_exam'
:
False
,
'is_active'
:
True
'is_active'
:
True
,
'hide_after_due'
:
False
,
}
response
=
self
.
client
.
post
(
reverse
(
'edx_proctoring.proctored_exam.exam'
),
...
...
@@ -136,7 +137,8 @@ class ProctoredExamViewTests(LoggedInTestCase):
'external_id'
:
'123'
,
'is_proctored'
:
True
,
'is_practice_exam'
:
False
,
'is_active'
:
True
'is_active'
:
True
,
'hide_after_due'
:
False
,
}
response
=
self
.
client
.
post
(
reverse
(
'edx_proctoring.proctored_exam.exam'
),
...
...
edx_proctoring/views.py
View file @
da76a638
...
...
@@ -189,7 +189,8 @@ class ProctoredExamView(AuthenticatedAPIView):
is_proctored
=
request
.
data
.
get
(
'is_proctored'
,
None
),
is_practice_exam
=
request
.
data
.
get
(
'is_practice_exam'
,
None
),
external_id
=
request
.
data
.
get
(
'external_id'
,
None
),
is_active
=
request
.
data
.
get
(
'is_active'
,
None
)
is_active
=
request
.
data
.
get
(
'is_active'
,
None
),
hide_after_due
=
request
.
data
.
get
(
'hide_after_due'
,
None
),
)
return
Response
({
'exam_id'
:
exam_id
})
else
:
...
...
@@ -213,6 +214,7 @@ class ProctoredExamView(AuthenticatedAPIView):
is_practice_exam
=
request
.
data
.
get
(
'is_practice_exam'
,
None
),
external_id
=
request
.
data
.
get
(
'external_id'
,
None
),
is_active
=
request
.
data
.
get
(
'is_active'
,
None
),
hide_after_due
=
request
.
data
.
get
(
'hide_after_due'
,
None
),
)
return
Response
({
'exam_id'
:
exam_id
})
except
ProctoredExamNotFoundException
,
ex
:
...
...
setup.py
View file @
da76a638
...
...
@@ -34,7 +34,7 @@ def load_requirements(*requirements_paths):
setup
(
name
=
'edx-proctoring'
,
version
=
'0.12.1
5
'
,
version
=
'0.12.1
6
'
,
description
=
'Proctoring subsystem for Open edX'
,
long_description
=
open
(
'README.md'
)
.
read
(),
author
=
'edX'
,
...
...
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