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
e577b8a6
Commit
e577b8a6
authored
Jun 24, 2015
by
chrisndodge
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #5 from edx/cdodge/update-models
update models
parents
92d47c45
b3d9949c
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
183 additions
and
7 deletions
+183
-7
edx_proctoring/api.py
+88
-0
edx_proctoring/migrations/0001_initial.py
+28
-2
edx_proctoring/models.py
+36
-4
edx_proctoring/tests/test_models.py
+30
-0
pylintrc
+1
-1
No files found.
edx_proctoring/api.py
View file @
e577b8a6
# pylint: disable=unused-argument
# remove pylint rule after we implement each method
"""
In-Proc API (aka Library) for the edx_proctoring subsystem. This is not to be confused with a HTTP REST
API which is in the views.py file, per edX coding standards
"""
def
create_exam
(
course_id
,
content_id
,
exam_name
,
time_limit_mins
,
is_proctored
=
True
,
external_id
=
None
,
is_active
=
True
):
"""
Creates a new ProctoredExam entity, if the course_id/content_id pair do not already exist.
If that pair already exists, then raise exception.
Returns: id (PK)
"""
def
update_exam
(
exam_id
,
exam_name
=
None
,
time_limit_mins
=
None
,
is_proctored
=
None
,
external_id
=
None
,
is_active
=
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.
Returns: id
"""
def
get_exam_by_id
(
exam_id
):
"""
Looks up exam by the Primary Key. Raises exception if not found.
Returns dictionary version of the Django ORM object
"""
def
get_exam_by_content_id
(
course_id
,
content_id
):
"""
Looks up exam by the course_id/content_id pair. Raises exception if not found.
Returns dictionary version of the Django ORM object
"""
def
add_allowance_for_user
(
exam_id
,
user_id
,
key
,
value
):
"""
Adds (or updates) an allowance for a user within a given exam
"""
def
remove_allowance_for_user
(
exam_id
,
user_id
,
key
):
"""
Deletes an allowance for a user within a given exam.
"""
def
start_exam_attempt
(
exam_id
,
user_id
,
external_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)
"""
def
stop_exam_attempt
(
exam_id
,
user
):
"""
Marks the exam attempt as completed (sets the completed_at field and updates the record)
"""
def
get_active_exams_for_user
(
user_id
,
course_id
=
None
):
"""
This method will return a list of active exams for the user,
i.e. started_at != None and completed_at == None. Theoretically there
could be more than one, but in practice it will be one active exam.
If course_id is set, then attempts only for an exam in that course_id
should be returned.
The return set should be a list of dictionary objects which are nested
[{
'exam': <exam fields as dict>,
'attempt': <student attempt fields as dict>,
'allowances': <student allowances as dict of key/value pairs
}, {}, ...]
"""
edx_proctoring/migrations/0001_initial.py
View file @
e577b8a6
...
...
@@ -11,23 +11,32 @@ class Migration(SchemaMigration):
# Adding model 'ProctoredExam'
db
.
create_table
(
'edx_proctoring_proctoredexam'
,
(
(
'id'
,
self
.
gf
(
'django.db.models.fields.AutoField'
)(
primary_key
=
True
)),
(
'created'
,
self
.
gf
(
'model_utils.fields.AutoCreatedField'
)(
default
=
datetime
.
datetime
.
now
)),
(
'modified'
,
self
.
gf
(
'model_utils.fields.AutoLastModifiedField'
)(
default
=
datetime
.
datetime
.
now
)),
(
'course_id'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
max_length
=
255
,
db_index
=
True
)),
(
'content_id'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
max_length
=
255
,
db_index
=
True
)),
(
'external_id'
,
self
.
gf
(
'django.db.models.fields.TextField'
)(
null
=
True
,
db_index
=
True
)),
(
'exam_name'
,
self
.
gf
(
'django.db.models.fields.TextField'
)()),
(
'time_limit_mins'
,
self
.
gf
(
'django.db.models.fields.IntegerField'
)()),
(
'is_proctored'
,
self
.
gf
(
'django.db.models.fields.BooleanField'
)(
default
=
False
)),
(
'is_active'
,
self
.
gf
(
'django.db.models.fields.BooleanField'
)(
default
=
False
)),
))
db
.
send_create_signal
(
'edx_proctoring'
,
[
'ProctoredExam'
])
# Adding unique constraint on 'ProctoredExam', fields ['course_id', 'content_id']
db
.
create_unique
(
'edx_proctoring_proctoredexam'
,
[
'course_id'
,
'content_id'
])
# Adding model 'ProctoredExamStudentAttempt'
db
.
create_table
(
'edx_proctoring_proctoredexamstudentattempt'
,
(
(
'id'
,
self
.
gf
(
'django.db.models.fields.AutoField'
)(
primary_key
=
True
)),
(
'created'
,
self
.
gf
(
'model_utils.fields.AutoCreatedField'
)(
default
=
datetime
.
datetime
.
now
)),
(
'modified'
,
self
.
gf
(
'model_utils.fields.AutoLastModifiedField'
)(
default
=
datetime
.
datetime
.
now
)),
(
'user_id'
,
self
.
gf
(
'django.db.models.fields.IntegerField'
)(
db_index
=
True
)),
(
'proctored_exam'
,
self
.
gf
(
'django.db.models.fields.related.ForeignKey'
)(
to
=
orm
[
'edx_proctoring.ProctoredExam'
])),
(
'started_at'
,
self
.
gf
(
'django.db.models.fields.DateTimeField'
)(
null
=
True
)),
(
'completed_at'
,
self
.
gf
(
'django.db.models.fields.DateTimeField'
)(
null
=
True
)),
(
'external_id'
,
self
.
gf
(
'django.db.models.fields.TextField'
)(
null
=
True
,
db_index
=
True
)),
(
'status'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
max_length
=
64
)),
))
db
.
send_create_signal
(
'edx_proctoring'
,
[
'ProctoredExamStudentAttempt'
])
...
...
@@ -43,11 +52,15 @@ class Migration(SchemaMigration):
))
db
.
send_create_signal
(
'edx_proctoring'
,
[
'ProctoredExamStudentAllowance'
])
# Adding unique constraint on 'ProctoredExamStudentAllowance', fields ['user_id', 'proctored_exam', 'key']
db
.
create_unique
(
'edx_proctoring_proctoredexamstudentallowance'
,
[
'user_id'
,
'proctored_exam_id'
,
'key'
])
# Adding model 'ProctoredExamStudentAllowanceHistory'
db
.
create_table
(
'edx_proctoring_proctoredexamstudentallowancehistory'
,
(
(
'id'
,
self
.
gf
(
'django.db.models.fields.AutoField'
)(
primary_key
=
True
)),
(
'created'
,
self
.
gf
(
'model_utils.fields.AutoCreatedField'
)(
default
=
datetime
.
datetime
.
now
)),
(
'modified'
,
self
.
gf
(
'model_utils.fields.AutoLastModifiedField'
)(
default
=
datetime
.
datetime
.
now
)),
(
'allowance_id'
,
self
.
gf
(
'django.db.models.fields.IntegerField'
)()),
(
'user_id'
,
self
.
gf
(
'django.db.models.fields.IntegerField'
)()),
(
'proctored_exam'
,
self
.
gf
(
'django.db.models.fields.related.ForeignKey'
)(
to
=
orm
[
'edx_proctoring.ProctoredExam'
])),
(
'key'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
max_length
=
255
)),
...
...
@@ -57,6 +70,12 @@ class Migration(SchemaMigration):
def
backwards
(
self
,
orm
):
# Removing unique constraint on 'ProctoredExamStudentAllowance', fields ['user_id', 'proctored_exam', 'key']
db
.
delete_unique
(
'edx_proctoring_proctoredexamstudentallowance'
,
[
'user_id'
,
'proctored_exam_id'
,
'key'
])
# Removing unique constraint on 'ProctoredExam', fields ['course_id', 'content_id']
db
.
delete_unique
(
'edx_proctoring_proctoredexam'
,
[
'course_id'
,
'content_id'
])
# Deleting model 'ProctoredExam'
db
.
delete_table
(
'edx_proctoring_proctoredexam'
)
...
...
@@ -72,17 +91,20 @@ class Migration(SchemaMigration):
models
=
{
'edx_proctoring.proctoredexam'
:
{
'Meta'
:
{
'object_name'
:
'ProctoredExam'
},
'Meta'
:
{
'
unique_together'
:
"(('course_id', 'content_id'),)"
,
'
object_name'
:
'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.TextField'
,
[],
{
'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'
:
{
'object_name'
:
'ProctoredExamStudentAllowance'
},
'Meta'
:
{
'
unique_together'
:
"(('user_id', 'proctored_exam', 'key'),)"
,
'
object_name'
:
'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'
}),
...
...
@@ -93,6 +115,7 @@ class Migration(SchemaMigration):
},
'edx_proctoring.proctoredexamstudentallowancehistory'
:
{
'Meta'
:
{
'object_name'
:
'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'
}),
...
...
@@ -104,10 +127,13 @@ class Migration(SchemaMigration):
'edx_proctoring.proctoredexamstudentattempt'
:
{
'Meta'
:
{
'object_name'
:
'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.TextField'
,
[],
{
'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'
}),
'user_id'
:
(
'django.db.models.fields.IntegerField'
,
[],
{
'db_index'
:
'True'
})
}
}
...
...
edx_proctoring/models.py
View file @
e577b8a6
...
...
@@ -2,12 +2,12 @@
Data models for the proctoring subsystem
"""
from
django.db
import
models
from
django.db.models.signals
import
post_save
from
django.db.models.signals
import
post_save
,
pre_delete
from
django.dispatch
import
receiver
from
model_utils.models
import
TimeStampedModel
class
ProctoredExam
(
models
.
Model
):
class
ProctoredExam
(
TimeStamped
Model
):
"""
Information about the Proctored Exam.
"""
...
...
@@ -21,6 +21,9 @@ class ProctoredExam(models.Model):
# This will be a integration specific ID - say to SoftwareSecure.
external_id
=
models
.
TextField
(
null
=
True
,
db_index
=
True
)
# This is the display name of the course
exam_name
=
models
.
TextField
()
# Time limit (in minutes) that a student can finish this exam
time_limit_mins
=
models
.
IntegerField
()
...
...
@@ -30,8 +33,12 @@ class ProctoredExam(models.Model):
# This will be a integration specific ID - say to SoftwareSecure.
is_active
=
models
.
BooleanField
()
class
Meta
:
""" Meta class for this Django model """
unique_together
=
((
'course_id'
,
'content_id'
),)
class
ProctoredExamStudentAttempt
(
models
.
Model
):
class
ProctoredExamStudentAttempt
(
TimeStamped
Model
):
"""
Information about the Student Attempt on a
Proctored Exam.
...
...
@@ -47,6 +54,14 @@ class ProctoredExamStudentAttempt(models.Model):
# This will be a integration specific ID - say to SoftwareSecure.
external_id
=
models
.
TextField
(
null
=
True
,
db_index
=
True
)
# what is the status of this attempt
status
=
models
.
CharField
(
max_length
=
64
)
@property
def
is_active
(
self
):
""" returns boolean if this attempt is considered active """
return
self
.
started_at
and
not
self
.
completed_at
class
QuerySetWithUpdateOverride
(
models
.
query
.
QuerySet
):
"""
...
...
@@ -82,6 +97,10 @@ class ProctoredExamStudentAllowance(TimeStampedModel):
value
=
models
.
CharField
(
max_length
=
255
)
class
Meta
:
""" Meta class for this Django model """
unique_together
=
((
'user_id'
,
'proctored_exam'
,
'key'
),)
class
ProctoredExamStudentAllowanceHistory
(
TimeStampedModel
):
"""
...
...
@@ -89,6 +108,9 @@ class ProctoredExamStudentAllowanceHistory(TimeStampedModel):
but will record (for audit history) all entries that have been updated.
"""
# what was the original id of the allowance
allowance_id
=
models
.
IntegerField
()
user_id
=
models
.
IntegerField
()
proctored_exam
=
models
.
ForeignKey
(
ProctoredExam
)
...
...
@@ -100,7 +122,7 @@ class ProctoredExamStudentAllowanceHistory(TimeStampedModel):
# Hook up the custom POST_UPDATE_SIGNAL signal to record updations in the ProctoredExamStudentAllowanceHistory table.
@receiver
(
post_save
,
sender
=
ProctoredExamStudentAllowance
)
def
archive_allowance_updations
(
sender
,
instance
,
created
,
**
kwargs
):
# pylint: disable=unused-argument
def
on_allowance_saved
(
sender
,
instance
,
created
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Archiving all changes made to the Student Allowance.
Will only archive on update, and not on new entries created.
...
...
@@ -110,12 +132,22 @@ def archive_allowance_updations(sender, instance, created, **kwargs): # pylint:
_make_archive_copy
(
instance
)
@receiver
(
pre_delete
,
sender
=
ProctoredExamStudentAllowance
)
def
on_allowance_deleted
(
sender
,
instance
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Archive the allowance when the item is about to be deleted
"""
_make_archive_copy
(
instance
)
def
_make_archive_copy
(
item
):
"""
Make a clone and populate in the History table
"""
archive_object
=
ProctoredExamStudentAllowanceHistory
(
allowance_id
=
item
.
id
,
user_id
=
item
.
user_id
,
proctored_exam
=
item
.
proctored_exam
,
key
=
item
.
key
,
...
...
edx_proctoring/tests/test_models.py
View file @
e577b8a6
...
...
@@ -28,6 +28,7 @@ class ProctoredExamModelTests(LoggedInTestCase):
proctored_exam
=
ProctoredExam
.
objects
.
create
(
course_id
=
'test_course'
,
content_id
=
'test_content'
,
exam_name
=
'Test Exam'
,
external_id
=
'123aXqe3'
,
time_limit_mins
=
90
)
...
...
@@ -74,3 +75,32 @@ class ProctoredExamModelTests(LoggedInTestCase):
proctored_exam_student_history
=
ProctoredExamStudentAllowanceHistory
.
objects
.
filter
(
user_id
=
1
)
self
.
assertEqual
(
len
(
proctored_exam_student_history
),
3
)
def
test_delete_proctored_exam_student_allowance_history
(
self
):
# pylint: disable=invalid-name
"""
Test to delete the proctored Exam Student Allowance object.
Upon first save, a new entry is _not_ created in the History table
However, a new entry in the History table is created every time the Student Allowance entry is updated.
"""
proctored_exam
=
ProctoredExam
.
objects
.
create
(
course_id
=
'test_course'
,
content_id
=
'test_content'
,
exam_name
=
'Test Exam'
,
external_id
=
'123aXqe3'
,
time_limit_mins
=
90
)
allowance
=
ProctoredExamStudentAllowance
.
objects
.
create
(
user_id
=
1
,
proctored_exam
=
proctored_exam
,
key
=
'allowance_key'
,
value
=
'20 minutes'
)
# No entry in the History table on creation of the Allowance entry.
proctored_exam_student_history
=
ProctoredExamStudentAllowanceHistory
.
objects
.
filter
(
user_id
=
1
)
self
.
assertEqual
(
len
(
proctored_exam_student_history
),
0
)
allowance
.
delete
()
proctored_exam_student_history
=
ProctoredExamStudentAllowanceHistory
.
objects
.
filter
(
user_id
=
1
)
self
.
assertEqual
(
len
(
proctored_exam_student_history
),
1
)
pylintrc
View file @
e577b8a6
...
...
@@ -39,7 +39,7 @@ load-plugins=
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
# I0011 locally-disabled (module-level pylint overrides)
disable=I0011,W0232,too-few-public-methods,abstract-class-little-used,abstract-class-not-used,too-many-public-methods,no-self-use,too-many-instance-attributes,duplicate-code,too-many-arguments,too-many-locals
disable=I0011,W0232,too-few-public-methods,abstract-class-little-used,abstract-class-not-used,too-many-public-methods,no-self-use,too-many-instance-attributes,duplicate-code,too-many-arguments,too-many-locals
,old-style-class
[REPORTS]
...
...
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