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
923f51b1
Commit
923f51b1
authored
Jul 01, 2015
by
chrisndodge
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #12 from edx/muhhshoaib/PHX-43-implement-proctored-start-page
PHX-43 Implemented proctored start page
parents
d539a957
17ef189f
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
397 additions
and
65 deletions
+397
-65
edx_proctoring/api.py
+70
-19
edx_proctoring/exceptions.py
+16
-4
edx_proctoring/migrations/0002_auto__add_field_proctoredexamstudentattempt_taking_as_proctored.py
+73
-0
edx_proctoring/migrations/0003_auto__add_field_proctoredexamstudentattempt_student_name.py
+74
-0
edx_proctoring/models.py
+26
-14
edx_proctoring/static/proctoring/js/proctored_exam_model.js
+5
-1
edx_proctoring/static/proctoring/js/proctored_exam_view.js
+3
-1
edx_proctoring/templates/proctoring/seq_proctored_exam_entrance.html
+64
-2
edx_proctoring/templates/proctoring/seq_proctored_exam_footer.html
+23
-0
edx_proctoring/templates/proctoring/seq_proctored_exam_instructions.html
+4
-0
edx_proctoring/templates/proctoring/seq_timed_exam_entrance.html
+3
-4
edx_proctoring/tests/test_api.py
+10
-5
edx_proctoring/tests/test_views.py
+6
-4
edx_proctoring/views.py
+20
-11
No files found.
edx_proctoring/api.py
View file @
923f51b1
...
...
@@ -12,13 +12,22 @@ from django.template import Context, loader
from
django.core.urlresolvers
import
reverse
from
edx_proctoring.exceptions
import
(
ProctoredExamAlreadyExists
,
ProctoredExamNotFoundException
,
StudentExamAttemptAlreadyExistsException
,
StudentExamAttemptDoesNotExistsException
)
ProctoredExamAlreadyExists
,
ProctoredExamNotFoundException
,
StudentExamAttemptAlreadyExistsException
,
StudentExamAttemptDoesNotExistsException
,
StudentExamAttemptedAlreadyStarted
,
)
from
edx_proctoring.models
import
(
ProctoredExam
,
ProctoredExamStudentAllowance
,
ProctoredExamStudentAttempt
ProctoredExam
,
ProctoredExamStudentAllowance
,
ProctoredExamStudentAttempt
,
)
from
edx_proctoring.serializers
import
(
ProctoredExamSerializer
,
ProctoredExamStudentAttemptSerializer
,
ProctoredExamStudentAllowanceSerializer
,
)
from
edx_proctoring.serializers
import
ProctoredExamSerializer
,
ProctoredExamStudentAttemptSerializer
,
\
ProctoredExamStudentAllowanceSerializer
from
edx_proctoring.utils
import
humanized_time
...
...
@@ -139,31 +148,70 @@ def get_exam_attempt(exam_id, user_id):
"""
Return an existing exam attempt for the given student
"""
exam_attempt_obj
=
ProctoredExamStudentAttempt
.
get_
student_
exam_attempt
(
exam_id
,
user_id
)
exam_attempt_obj
=
ProctoredExamStudentAttempt
.
get_exam_attempt
(
exam_id
,
user_id
)
return
exam_attempt_obj
.
__dict__
if
exam_attempt_obj
else
None
def
start_exam_attempt
(
exam_id
,
user_id
,
external_id
):
def
create_exam_attempt
(
exam_id
,
user_id
,
external_id
):
"""
Creates an exam attempt for user_id against exam_id. There should only be
one exam_attempt per user per exam. Multiple attempts by user will be archived
in a separate table
"""
if
ProctoredExamStudentAttempt
.
get_exam_attempt
(
exam_id
,
user_id
):
err_msg
=
(
'Cannot create new exam attempt for exam_id = {exam_id} and '
'user_id = {user_id} because it already exists!'
)
.
format
(
exam_id
=
exam_id
,
user_id
=
user_id
)
raise
StudentExamAttemptAlreadyExistsException
(
err_msg
)
attempt
=
ProctoredExamStudentAttempt
.
create_exam_attempt
(
exam_id
,
user_id
,
''
,
# student name is TBD
external_id
)
return
attempt
.
id
def
start_exam_attempt
(
exam_id
,
user_id
):
"""
Signals the beginning of an exam attempt for a given
exam_id. If one already exists, then an exception should be thrown.
Returns: exam_attempt_id (PK)
"""
exam_attempt_obj
=
ProctoredExamStudentAttempt
.
start_exam_attempt
(
exam_id
,
user_id
,
external_id
)
if
exam_attempt_obj
is
None
:
raise
StudentExamAttemptAlreadyExistsException
else
:
return
exam_attempt_obj
.
id
existing_attempt
=
ProctoredExamStudentAttempt
.
get_exam_attempt
(
exam_id
,
user_id
)
if
not
existing_attempt
:
err_msg
=
(
'Cannot start exam attempt for exam_id = {exam_id} '
'and user_id = {user_id} because it does not exist!'
)
.
format
(
exam_id
=
exam_id
,
user_id
=
user_id
)
raise
StudentExamAttemptDoesNotExistsException
(
err_msg
)
if
existing_attempt
.
started_at
:
# cannot restart an attempt
err_msg
=
(
'Cannot start exam attempt for exam_id = {exam_id} '
'and user_id = {user_id} because it has already started!'
)
.
format
(
exam_id
=
exam_id
,
user_id
=
user_id
)
raise
StudentExamAttemptedAlreadyStarted
(
err_msg
)
existing_attempt
.
start_exam_attempt
()
def
stop_exam_attempt
(
exam_id
,
user_id
):
"""
Marks the exam attempt as completed (sets the completed_at field and updates the record)
"""
exam_attempt_obj
=
ProctoredExamStudentAttempt
.
get_
student_
exam_attempt
(
exam_id
,
user_id
)
exam_attempt_obj
=
ProctoredExamStudentAttempt
.
get_exam_attempt
(
exam_id
,
user_id
)
if
exam_attempt_obj
is
None
:
raise
StudentExamAttemptDoesNotExistsException
raise
StudentExamAttemptDoesNotExistsException
(
'Error. Trying to stop an exam that is not in progress.'
)
else
:
exam_attempt_obj
.
completed_at
=
datetime
.
now
(
pytz
.
UTC
)
exam_attempt_obj
.
save
()
...
...
@@ -191,7 +239,7 @@ def get_active_exams_for_user(user_id, course_id=None):
"""
result
=
[]
student_active_exams
=
ProctoredExamStudentAttempt
.
get_active_student_
exam
s
(
user_id
,
course_id
)
student_active_exams
=
ProctoredExamStudentAttempt
.
get_active_student_
attempt
s
(
user_id
,
course_id
)
for
active_exam
in
student_active_exams
:
# convert the django orm objects
# into the serialized form.
...
...
@@ -241,17 +289,20 @@ def get_student_view(user_id, course_id, content_id, context):
)
attempt
=
get_exam_attempt
(
exam_id
,
user_id
)
has_started_exam
=
attempt
is
not
None
if
attempt
:
has_started_exam
=
attempt
and
attempt
.
get
(
'started_at'
)
if
has_started_exam
:
now_utc
=
datetime
.
now
(
pytz
.
UTC
)
expires_at
=
attempt
[
'started_at'
]
+
timedelta
(
minutes
=
context
[
'default_time_limit_mins'
])
has_time_expired
=
now_utc
>
expires_at
if
not
has_started_exam
:
# determine whether to show a timed exam only entrace screen
# determine whether to show a timed exam only entra
n
ce screen
# or a screen regarding proctoring
if
is_proctored
:
student_view_template
=
'proctoring/seq_proctored_exam_entrance.html'
if
not
attempt
:
student_view_template
=
'proctoring/seq_proctored_exam_entrance.html'
else
:
student_view_template
=
'proctoring/seq_proctored_exam_instructions.html'
else
:
student_view_template
=
'proctoring/seq_timed_exam_entrance.html'
elif
has_finished_exam
:
...
...
edx_proctoring/exceptions.py
View file @
923f51b1
...
...
@@ -3,25 +3,37 @@ Specialized exceptions for the Notification subsystem
"""
class
ProctoredExamAlreadyExists
(
Exception
):
class
ProctoredBaseException
(
Exception
):
"""
A common base class for all exceptions
"""
class
ProctoredExamAlreadyExists
(
ProctoredBaseException
):
"""
Raised when trying to create an Exam that already exists.
"""
class
ProctoredExamNotFoundException
(
Exception
):
class
ProctoredExamNotFoundException
(
ProctoredBase
Exception
):
"""
Raised when a look up fails.
"""
class
StudentExamAttemptAlreadyExistsException
(
Exception
):
class
StudentExamAttemptAlreadyExistsException
(
ProctoredBase
Exception
):
"""
Raised when trying to start an exam when an Exam Attempt already exists.
"""
class
StudentExamAttemptDoesNotExistsException
(
Exception
):
class
StudentExamAttemptDoesNotExistsException
(
ProctoredBase
Exception
):
"""
Raised when trying to stop an exam attempt where the Exam Attempt doesn't exist.
"""
class
StudentExamAttemptedAlreadyStarted
(
ProctoredBaseException
):
"""
Raised when the same exam attempt is being started twice
"""
edx_proctoring/migrations/0002_auto__add_field_proctoredexamstudentattempt_taking_as_proctored.py
0 → 100644
View file @
923f51b1
# -*- coding: utf-8 -*-
from
south.utils
import
datetime_utils
as
datetime
from
south.db
import
db
from
south.v2
import
SchemaMigration
from
django.db
import
models
class
Migration
(
SchemaMigration
):
def
forwards
(
self
,
orm
):
# Adding field 'ProctoredExamStudentAttempt.taking_as_proctored'
db
.
add_column
(
'proctoring_proctoredexamstudentattempt'
,
'taking_as_proctored'
,
self
.
gf
(
'django.db.models.fields.BooleanField'
)(
default
=
False
),
keep_default
=
False
)
def
backwards
(
self
,
orm
):
# Deleting field 'ProctoredExamStudentAttempt.taking_as_proctored'
db
.
delete_column
(
'proctoring_proctoredexamstudentattempt'
,
'taking_as_proctored'
)
models
=
{
'edx_proctoring.proctoredexam'
:
{
'Meta'
:
{
'unique_together'
:
"(('course_id', 'content_id'),)"
,
'object_name'
:
'ProctoredExam'
,
'db_table'
:
"'proctoring_proctoredexam'"
},
'content_id'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'course_id'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'created'
:
(
'model_utils.fields.AutoCreatedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'exam_name'
:
(
'django.db.models.fields.TextField'
,
[],
{}),
'external_id'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'null'
:
'True'
,
'db_index'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'is_active'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'is_proctored'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'modified'
:
(
'model_utils.fields.AutoLastModifiedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'time_limit_mins'
:
(
'django.db.models.fields.IntegerField'
,
[],
{})
},
'edx_proctoring.proctoredexamstudentallowance'
:
{
'Meta'
:
{
'unique_together'
:
"(('user_id', 'proctored_exam', 'key'),)"
,
'object_name'
:
'ProctoredExamStudentAllowance'
,
'db_table'
:
"'proctoring_proctoredexamstudentallowance'"
},
'created'
:
(
'model_utils.fields.AutoCreatedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'key'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
}),
'modified'
:
(
'model_utils.fields.AutoLastModifiedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'proctored_exam'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['edx_proctoring.ProctoredExam']"
}),
'user_id'
:
(
'django.db.models.fields.IntegerField'
,
[],
{}),
'value'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
})
},
'edx_proctoring.proctoredexamstudentallowancehistory'
:
{
'Meta'
:
{
'object_name'
:
'ProctoredExamStudentAllowanceHistory'
,
'db_table'
:
"'proctoring_proctoredexamstudentallowancehistory'"
},
'allowance_id'
:
(
'django.db.models.fields.IntegerField'
,
[],
{}),
'created'
:
(
'model_utils.fields.AutoCreatedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'key'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
}),
'modified'
:
(
'model_utils.fields.AutoLastModifiedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'proctored_exam'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['edx_proctoring.ProctoredExam']"
}),
'user_id'
:
(
'django.db.models.fields.IntegerField'
,
[],
{}),
'value'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
})
},
'edx_proctoring.proctoredexamstudentattempt'
:
{
'Meta'
:
{
'object_name'
:
'ProctoredExamStudentAttempt'
,
'db_table'
:
"'proctoring_proctoredexamstudentattempt'"
},
'completed_at'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'null'
:
'True'
}),
'created'
:
(
'model_utils.fields.AutoCreatedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'external_id'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'null'
:
'True'
,
'db_index'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'modified'
:
(
'model_utils.fields.AutoLastModifiedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'proctored_exam'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['edx_proctoring.ProctoredExam']"
}),
'started_at'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'null'
:
'True'
}),
'status'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'64'
}),
'taking_as_proctored'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'user_id'
:
(
'django.db.models.fields.IntegerField'
,
[],
{
'db_index'
:
'True'
})
}
}
complete_apps
=
[
'edx_proctoring'
]
\ No newline at end of file
edx_proctoring/migrations/0003_auto__add_field_proctoredexamstudentattempt_student_name.py
0 → 100644
View file @
923f51b1
# -*- coding: utf-8 -*-
from
south.utils
import
datetime_utils
as
datetime
from
south.db
import
db
from
south.v2
import
SchemaMigration
from
django.db
import
models
class
Migration
(
SchemaMigration
):
def
forwards
(
self
,
orm
):
# Adding field 'ProctoredExamStudentAttempt.student_name'
db
.
add_column
(
'proctoring_proctoredexamstudentattempt'
,
'student_name'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
max_length
=
255
,
null
=
True
),
keep_default
=
False
)
def
backwards
(
self
,
orm
):
# Deleting field 'ProctoredExamStudentAttempt.student_name'
db
.
delete_column
(
'proctoring_proctoredexamstudentattempt'
,
'student_name'
)
models
=
{
'edx_proctoring.proctoredexam'
:
{
'Meta'
:
{
'unique_together'
:
"(('course_id', 'content_id'),)"
,
'object_name'
:
'ProctoredExam'
,
'db_table'
:
"'proctoring_proctoredexam'"
},
'content_id'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'course_id'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'created'
:
(
'model_utils.fields.AutoCreatedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'exam_name'
:
(
'django.db.models.fields.TextField'
,
[],
{}),
'external_id'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'null'
:
'True'
,
'db_index'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'is_active'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'is_proctored'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'modified'
:
(
'model_utils.fields.AutoLastModifiedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'time_limit_mins'
:
(
'django.db.models.fields.IntegerField'
,
[],
{})
},
'edx_proctoring.proctoredexamstudentallowance'
:
{
'Meta'
:
{
'unique_together'
:
"(('user_id', 'proctored_exam', 'key'),)"
,
'object_name'
:
'ProctoredExamStudentAllowance'
,
'db_table'
:
"'proctoring_proctoredexamstudentallowance'"
},
'created'
:
(
'model_utils.fields.AutoCreatedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'key'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
}),
'modified'
:
(
'model_utils.fields.AutoLastModifiedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'proctored_exam'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['edx_proctoring.ProctoredExam']"
}),
'user_id'
:
(
'django.db.models.fields.IntegerField'
,
[],
{}),
'value'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
})
},
'edx_proctoring.proctoredexamstudentallowancehistory'
:
{
'Meta'
:
{
'object_name'
:
'ProctoredExamStudentAllowanceHistory'
,
'db_table'
:
"'proctoring_proctoredexamstudentallowancehistory'"
},
'allowance_id'
:
(
'django.db.models.fields.IntegerField'
,
[],
{}),
'created'
:
(
'model_utils.fields.AutoCreatedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'key'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
}),
'modified'
:
(
'model_utils.fields.AutoLastModifiedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'proctored_exam'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['edx_proctoring.ProctoredExam']"
}),
'user_id'
:
(
'django.db.models.fields.IntegerField'
,
[],
{}),
'value'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
})
},
'edx_proctoring.proctoredexamstudentattempt'
:
{
'Meta'
:
{
'object_name'
:
'ProctoredExamStudentAttempt'
,
'db_table'
:
"'proctoring_proctoredexamstudentattempt'"
},
'completed_at'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'null'
:
'True'
}),
'created'
:
(
'model_utils.fields.AutoCreatedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'external_id'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'null'
:
'True'
,
'db_index'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'modified'
:
(
'model_utils.fields.AutoLastModifiedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'proctored_exam'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['edx_proctoring.ProctoredExam']"
}),
'started_at'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'null'
:
'True'
}),
'status'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'64'
}),
'student_name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'null'
:
'True'
}),
'taking_as_proctored'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'user_id'
:
(
'django.db.models.fields.IntegerField'
,
[],
{
'db_index'
:
'True'
})
}
}
complete_apps
=
[
'edx_proctoring'
]
\ No newline at end of file
edx_proctoring/models.py
View file @
923f51b1
...
...
@@ -85,6 +85,12 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
# what is the status of this attempt
status
=
models
.
CharField
(
max_length
=
64
)
# if the user is attempting this as a proctored exam
# in case there is an option to opt-out
taking_as_proctored
=
models
.
BooleanField
()
student_name
=
models
.
CharField
(
max_length
=
255
)
class
Meta
:
""" Meta class for this Django model """
db_table
=
'proctoring_proctoredexamstudentattempt'
...
...
@@ -96,23 +102,29 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
return
self
.
started_at
and
not
self
.
completed_at
@classmethod
def
start_exam_attempt
(
cls
,
exam_id
,
user_id
,
external_id
):
def
create_exam_attempt
(
cls
,
exam_id
,
user_id
,
student_name
,
external_id
):
"""
create and return an exam attempt entry for a given
exam_id. If one already exists, then returns None
.
Create a new exam attempt entry for a given exam_id and
user_id
.
"""
if
cls
.
get_student_exam_attempt
(
exam_id
,
user_id
)
is
None
:
return
cls
.
objects
.
create
(
proctored_exam_id
=
exam_id
,
user_id
=
user_id
,
external_id
=
external_id
,
started_at
=
datetime
.
now
(
pytz
.
UTC
)
)
else
:
return
None
return
cls
.
objects
.
create
(
proctored_exam_id
=
exam_id
,
user_id
=
user_id
,
student_name
=
student_name
,
external_id
=
external_id
)
def
start_exam_attempt
(
self
):
"""
sets the model's state when an exam attempt has started
"""
self
.
started_at
=
datetime
.
now
(
pytz
.
UTC
)
self
.
save
()
@classmethod
def
get_
student_
exam_attempt
(
cls
,
exam_id
,
user_id
):
def
get_exam_attempt
(
cls
,
exam_id
,
user_id
):
"""
Returns the Student Exam Attempt object if found
else Returns None.
...
...
@@ -124,7 +136,7 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
return
exam_attempt_obj
@classmethod
def
get_active_student_
exam
s
(
cls
,
user_id
,
course_id
=
None
):
def
get_active_student_
attempt
s
(
cls
,
user_id
,
course_id
=
None
):
"""
Returns the active student exams (user in-progress exams)
"""
...
...
edx_proctoring/static/proctoring/js/proctored_exam_model.js
View file @
923f51b1
...
...
@@ -17,10 +17,14 @@
var
currentTime
=
(
new
Date
()).
getTime
();
var
lastFetched
=
this
.
get
(
'lastFetched'
).
getTime
();
var
totalSeconds
=
this
.
get
(
'time_remaining_seconds'
)
-
(
currentTime
-
lastFetched
)
/
1000
;
return
(
totalSeconds
>
0
)
?
totalSeconds
:
0
;
return
totalSeconds
;
},
getFormattedRemainingTime
:
function
()
{
var
totalSeconds
=
this
.
getRemainingSeconds
();
/* since we can have a small grace period, we can end in the negative numbers */
if
(
totalSeconds
<
0
)
totalSeconds
=
0
;
var
hours
=
parseInt
(
totalSeconds
/
3600
)
%
24
;
var
minutes
=
parseInt
(
totalSeconds
/
60
)
%
60
;
var
seconds
=
Math
.
floor
(
totalSeconds
%
60
);
...
...
edx_proctoring/static/proctoring/js/proctored_exam_view.js
View file @
923f51b1
...
...
@@ -13,6 +13,8 @@ var edx = edx || {};
this
.
templateId
=
options
.
proctored_template
;
this
.
template
=
null
;
this
.
timerId
=
null
;
/* give an extra 5 seconds where the timer holds at 00:00 before page refreshes */
this
.
grace_period_secs
=
5
;
var
template_html
=
$
(
this
.
templateId
).
text
();
if
(
template_html
!==
null
)
{
...
...
@@ -47,7 +49,7 @@ var edx = edx || {};
self
.
$el
.
find
(
'div.exam-timer'
).
removeClass
(
"low-time warning critical"
);
self
.
$el
.
find
(
'div.exam-timer'
).
addClass
(
self
.
model
.
getRemainingTimeState
());
self
.
$el
.
find
(
'span#time_remaining_id b'
).
html
(
self
.
model
.
getFormattedRemainingTime
());
if
(
self
.
model
.
getRemainingSeconds
()
<=
0
)
{
if
(
self
.
model
.
getRemainingSeconds
()
<=
-
self
.
grace_period_secs
)
{
clearInterval
(
self
.
timerId
);
// stop the timer once the time finishes.
// refresh the page when the timer expired
location
.
reload
();
...
...
edx_proctoring/templates/proctoring/seq_proctored_exam_entrance.html
View file @
923f51b1
{% load i18n %}
<div
class=
"sequence"
data-exam-id=
"{{exam_id}}"
>
This is to be developed!
</div>
{% include 'proctoring/seq_timed_exam_footer.html' %}
<div
class=
"sequence proctored-exam entrance"
data-exam-id=
"{{exam_id}}"
>
<h3>
{% blocktrans %}
Would you Like to take {{ display_name }} as Proctored Exam?
{% endblocktrans %}
</h3>
<p>
{% blocktrans %}
Since you're enrolled in this course as a verified student, you have the option to take this exam
with online proctoring. Online proctoring is one requirement towards being eligible for credit.
{% endblocktrans %}
</p>
<div
class=
"gated-sequence"
>
<span><i
class=
"fa fa-lock"
></i></span>
<a
class=
"start-timed-exam"
data-ajax-url=
"{{enter_exam_endpoint}}"
data-exam-id=
"{{exam_id}}"
data-attempt-proctored=
true
>
{% trans "Yes, take this exam as a proctored exam (and be eligible for credit)" %}
</a>
<p>
{% blocktrans %}
You will need to
<strong>
download and install edX-approved software
</strong>
for the online proctoring
of your exam. After successful installation, you will be
<strong>
guided through setting up your
proctored session and begin the exam immediately
</strong>
afterwards.
</p>
{% endblocktrans %}
<i
class=
"fa fa-arrow-circle-right start-timed-exam"
data-ajax-url=
"{{enter_exam_endpoint}}"
data-exam-id=
"{{exam_id}}"
data-attempt-proctored=
true
></i>
</div>
<div
class=
"gated-sequence"
>
<span><i
class=
"fa fa-unlock"
></i></span>
<a
class=
"start-timed-exam"
data-ajax-url=
"{{enter_exam_endpoint}}"
data-exam-id=
"{{exam_id}}"
data-attempt-proctored=
false
>
{% trans "No, take this exam as an open exam (and not be eligible for credit)" %}
</a>
<p>
{% blocktrans %}
You may proceed and begin the exam at your leisure, but
<strong>
you will not be able to apply for college
credit
</strong>
upon completing the exam or this course in general.
{% endblocktrans %}
</p>
<i
class=
"fa fa-arrow-circle-right start-timed-exam"
data-ajax-url=
"{{enter_exam_endpoint}}"
data-exam-id=
"{{exam_id}}"
data-attempt-proctored=
false
></i>
</div>
</div>
{% include 'proctoring/seq_proctored_exam_footer.html' %}
<script
type=
"text/javascript"
>
$
(
'.start-timed-exam'
).
click
(
function
(
event
)
{
var
action_url
=
$
(
this
).
data
(
'ajax-url'
);
var
exam_id
=
$
(
this
).
data
(
'exam-id'
);
var
attempt_proctored
=
$
(
this
).
data
(
'attempt-proctored'
);
if
(
typeof
action_url
===
"undefined"
)
{
return
false
;
}
$
.
post
(
action_url
,
{
"exam_id"
:
exam_id
,
"attempt_proctored"
:
attempt_proctored
,
"start_clock"
:
false
},
function
(
data
)
{
// reload the page, because we've unlocked it
location
.
reload
();
}
);
}
);
</script>
edx_proctoring/templates/proctoring/seq_proctored_exam_footer.html
0 → 100644
View file @
923f51b1
{% load i18n %}
<div
class=
"footer-sequence"
>
<h4>
{% trans "Why i am seeing these options?" %}
</h4>
<p>
{% blocktrans %}
Text to be added here.
{% endblocktrans %}
</p>
</div>
<div
class=
"faq-proctoring-exam"
>
<h4>
{% trans "See Also" %}
</h4>
<p>
{% blocktrans %}
<a
class=
"footer-link"
href=
"#"
>
Frequently asked questions about proctoring and earning college credit.
</a>
<a
class=
"footer-link"
href=
"#"
>
Technical Requirements for taking a proctored exam
</a>
{% endblocktrans %}
</p>
</div>
\ No newline at end of file
edx_proctoring/templates/proctoring/seq_proctored_exam_instructions.html
0 → 100644
View file @
923f51b1
{% load i18n %}
<div
class=
"sequence"
data-exam-id=
"{{exam_id}}"
>
How to launch the proctored exam content goes here
</div>
edx_proctoring/templates/proctoring/seq_timed_exam_entrance.html
View file @
923f51b1
...
...
@@ -10,9 +10,7 @@
<strong>
{% trans "In order to successfully pass this exam you will have to answer the following questions and problems in the time allotted." %}
</strong>
{% blocktrans %}
Once you proceed, you'll start both the exam and the {{total_time|lower}} given to you.
{% endblocktrans %}
{% trans "Once you proceed, you'll start both the exam and the "%} {{total_time|lower}} {% trans " given to you." %}
</p>
<div
class=
"gated-sequence"
>
<a
class=
'start-timed-exam'
data-ajax-url=
"{{enter_exam_endpoint}}"
data-exam-id=
"{{exam_id}}"
>
...
...
@@ -34,7 +32,8 @@
$
.
post
(
action_url
,
{
"exam_id"
:
exam_id
"exam_id"
:
exam_id
,
"start_clock"
:
true
},
function
(
data
)
{
// reload the page, because we've unlocked it
...
...
edx_proctoring/tests/test_api.py
View file @
923f51b1
...
...
@@ -13,7 +13,8 @@ from edx_proctoring.api import (
start_exam_attempt
,
stop_exam_attempt
,
get_active_exams_for_user
,
get_exam_attempt
get_exam_attempt
,
create_exam_attempt
)
from
edx_proctoring.exceptions
import
(
ProctoredExamAlreadyExists
,
...
...
@@ -187,11 +188,11 @@ class ProctoredExamApiTests(LoggedInTestCase):
remove_allowance_for_user
(
student_allowance
.
proctored_exam
.
id
,
self
.
user_id
,
self
.
key
)
self
.
assertEqual
(
len
(
ProctoredExamStudentAllowance
.
objects
.
filter
()),
0
)
def
test_
start
_an_exam_attempt
(
self
):
def
test_
create
_an_exam_attempt
(
self
):
"""
Start an exam attempt.
"""
attempt_id
=
start_exam_attempt
(
self
.
proctored_exam_id
,
self
.
user_id
,
self
.
external_id
)
attempt_id
=
create_exam_attempt
(
self
.
proctored_exam_id
,
self
.
user_id
,
''
)
self
.
assertGreater
(
attempt_id
,
0
)
def
test_get_exam_attempt
(
self
):
...
...
@@ -211,7 +212,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
"""
proctored_exam_student_attempt
=
self
.
_create_student_exam_attempt
()
with
self
.
assertRaises
(
StudentExamAttemptAlreadyExistsException
):
start
_exam_attempt
(
proctored_exam_student_attempt
.
proctored_exam
,
self
.
user_id
,
self
.
external_id
)
create
_exam_attempt
(
proctored_exam_student_attempt
.
proctored_exam
,
self
.
user_id
,
self
.
external_id
)
def
test_stop_exam_attempt
(
self
):
"""
...
...
@@ -244,11 +245,15 @@ class ProctoredExamApiTests(LoggedInTestCase):
exam_name
=
'Final Test Exam'
,
time_limit_mins
=
self
.
default_time_limit
)
start
_exam_attempt
(
create
_exam_attempt
(
exam_id
=
exam_id
,
user_id
=
self
.
user_id
,
external_id
=
self
.
external_id
)
start_exam_attempt
(
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'
)
student_active_exams
=
get_active_exams_for_user
(
self
.
user_id
,
self
.
course_id
)
...
...
edx_proctoring/tests/test_views.py
View file @
923f51b1
...
...
@@ -350,8 +350,8 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
)
attempt_data
=
{
'exam_id'
:
proctored_exam
.
id
,
'
user_id'
:
self
.
student_taking_exam
.
id
,
'
external_id'
:
proctored_exam
.
external_id
'
external_id'
:
proctored_exam
.
external_
id
,
'
start_clock'
:
True
,
}
response
=
self
.
client
.
post
(
reverse
(
'edx_proctoring.proctored_exam.attempt'
),
...
...
@@ -376,7 +376,6 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
)
attempt_data
=
{
'exam_id'
:
proctored_exam
.
id
,
'user_id'
:
self
.
student_taking_exam
.
id
,
'external_id'
:
proctored_exam
.
external_id
}
response
=
self
.
client
.
post
(
...
...
@@ -394,7 +393,10 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
)
self
.
assertEqual
(
response
.
status_code
,
400
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response_data
[
'detail'
],
'Error. Trying to start an exam that has already started.'
)
self
.
assertEqual
(
response_data
[
'detail'
],
'Cannot create new exam attempt for exam_id = 1 and user_id = 1 because it already exists!'
)
def
test_stop_exam_attempt
(
self
):
"""
...
...
edx_proctoring/views.py
View file @
923f51b1
...
...
@@ -19,10 +19,13 @@ from edx_proctoring.api import (
stop_exam_attempt
,
add_allowance_for_user
,
remove_allowance_for_user
,
get_active_exams_for_user
get_active_exams_for_user
,
create_exam_attempt
)
from
edx_proctoring.exceptions
import
(
ProctoredBaseException
,
ProctoredExamNotFoundException
,
)
from
edx_proctoring.exceptions
import
ProctoredExamNotFoundException
,
\
StudentExamAttemptAlreadyExistsException
,
StudentExamAttemptDoesNotExistsException
from
edx_proctoring.serializers
import
ProctoredExamSerializer
from
.utils
import
AuthenticatedAPIView
...
...
@@ -236,20 +239,26 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
def
post
(
self
,
request
):
"""
HTTP POST handler. To
start
an exam.
HTTP POST handler. To
create
an exam.
"""
start_immediately
=
request
.
DATA
.
get
(
'start_clock'
,
'false'
)
.
lower
()
==
'true'
exam_id
=
request
.
DATA
.
get
(
'exam_id'
,
None
)
try
:
exam_attempt_id
=
start
_exam_attempt
(
exam_id
=
request
.
DATA
.
get
(
'exam_id'
,
None
)
,
exam_attempt_id
=
create
_exam_attempt
(
exam_id
=
exam_id
,
user_id
=
request
.
user
.
id
,
external_id
=
request
.
DATA
.
get
(
'external_id'
,
None
)
external_id
=
request
.
DATA
.
get
(
'external_id'
,
None
)
,
)
if
start_immediately
:
start_exam_attempt
(
exam_id
,
request
.
user
.
id
)
return
Response
({
'exam_attempt_id'
:
exam_attempt_id
})
except
StudentExamAttemptAlreadyExistsException
:
except
ProctoredBaseException
,
ex
:
return
Response
(
status
=
status
.
HTTP_400_BAD_REQUEST
,
data
=
{
"detail"
:
"Error. Trying to start an exam that has already started."
}
data
=
{
"detail"
:
str
(
ex
)
}
)
def
put
(
self
,
request
):
...
...
@@ -263,10 +272,10 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
)
return
Response
({
"exam_attempt_id"
:
exam_attempt_id
})
except
StudentExamAttemptDoesNotExistsException
:
except
ProctoredBaseException
,
ex
:
return
Response
(
status
=
status
.
HTTP_400_BAD_REQUEST
,
data
=
{
"detail"
:
"Error. Trying to stop an exam that is not in progress."
}
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