Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
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
edx
edx-platform
Commits
9d71ac2e
Commit
9d71ac2e
authored
Oct 04, 2016
by
Sanford Student
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
SQL model for course grades
Includes unit tests For TNL-5310
parent
1e1b7e1a
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
184 additions
and
7 deletions
+184
-7
lms/djangoapps/grades/migrations/0006_persistent_course_grades.py
+37
-0
lms/djangoapps/grades/models.py
+75
-1
lms/djangoapps/grades/tests/test_models.py
+72
-6
No files found.
lms/djangoapps/grades/migrations/0006_persistent_course_grades.py
0 → 100644
View file @
9d71ac2e
# -*- coding: utf-8 -*-
from
__future__
import
unicode_literals
from
django.db
import
migrations
,
models
import
django.utils.timezone
import
model_utils.fields
import
xmodule_django.models
import
coursewarehistoryextended.fields
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'grades'
,
'0005_multiple_course_flags'
),
]
operations
=
[
migrations
.
CreateModel
(
name
=
'PersistentCourseGrade'
,
fields
=
[
(
'created'
,
model_utils
.
fields
.
AutoCreatedField
(
default
=
django
.
utils
.
timezone
.
now
,
verbose_name
=
'created'
,
editable
=
False
)),
(
'modified'
,
model_utils
.
fields
.
AutoLastModifiedField
(
default
=
django
.
utils
.
timezone
.
now
,
verbose_name
=
'modified'
,
editable
=
False
)),
(
'id'
,
coursewarehistoryextended
.
fields
.
UnsignedBigIntAutoField
(
serialize
=
False
,
primary_key
=
True
)),
(
'user_id'
,
models
.
IntegerField
(
db_index
=
True
)),
(
'course_id'
,
xmodule_django
.
models
.
CourseKeyField
(
max_length
=
255
)),
(
'course_edited_timestamp'
,
models
.
DateTimeField
(
verbose_name
=
'Last content edit timestamp'
)),
(
'course_version'
,
models
.
CharField
(
max_length
=
255
,
verbose_name
=
'Course content version identifier'
,
blank
=
True
)),
(
'grading_policy_hash'
,
models
.
CharField
(
max_length
=
255
,
verbose_name
=
'Hash of grading policy'
)),
(
'percent_grade'
,
models
.
FloatField
()),
(
'letter_grade'
,
models
.
CharField
(
max_length
=
255
,
verbose_name
=
'Letter grade for course'
)),
],
),
migrations
.
AlterUniqueTogether
(
name
=
'persistentcoursegrade'
,
unique_together
=
set
([(
'course_id'
,
'user_id'
)]),
),
]
lms/djangoapps/grades/models.py
View file @
9d71ac2e
...
...
@@ -3,6 +3,9 @@ Models used for robust grading.
Robust grading allows student scores to be saved per-subsection independent
of any changes that may occur to the course after the score is achieved.
We also persist students' course-level grades, and update them whenever
a student's score or the course grading policy changes. As they are
persisted, course grades are also immune to changes in course content.
"""
from
base64
import
b64encode
...
...
@@ -212,7 +215,6 @@ class PersistentSubsectionGrade(TimeStampedModel):
# primary key will need to be large for this table
id
=
UnsignedBigIntAutoField
(
primary_key
=
True
)
# pylint: disable=invalid-name
# uniquely identify this particular grade object
user_id
=
models
.
IntegerField
(
blank
=
False
)
course_id
=
CourseKeyField
(
blank
=
False
,
max_length
=
255
)
...
...
@@ -363,3 +365,75 @@ class PersistentSubsectionGrade(TimeStampedModel):
"""
params
[
'visible_blocks_id'
]
=
params
[
'visible_blocks'
]
.
hash_value
del
params
[
'visible_blocks'
]
class
PersistentCourseGrade
(
TimeStampedModel
):
"""
A django model tracking persistent course grades.
"""
class
Meta
(
object
):
# Indices:
# (course_id, user_id) for individual grades
# (course_id) for instructors to see all course grades, implicitly created via the unique_together constraint
# (user_id) for course dashboard; explicitly declared as an index below
unique_together
=
[
(
'course_id'
,
'user_id'
),
]
# primary key will need to be large for this table
id
=
UnsignedBigIntAutoField
(
primary_key
=
True
)
# pylint: disable=invalid-name
user_id
=
models
.
IntegerField
(
blank
=
False
,
db_index
=
True
)
course_id
=
CourseKeyField
(
blank
=
False
,
max_length
=
255
)
# Information relating to the state of content when grade was calculated
course_edited_timestamp
=
models
.
DateTimeField
(
u'Last content edit timestamp'
,
blank
=
False
)
course_version
=
models
.
CharField
(
u'Course content version identifier'
,
blank
=
True
,
max_length
=
255
)
grading_policy_hash
=
models
.
CharField
(
u'Hash of grading policy'
,
blank
=
False
,
max_length
=
255
)
# Information about the course grade itself
percent_grade
=
models
.
FloatField
(
blank
=
False
)
letter_grade
=
models
.
CharField
(
u'Letter grade for course'
,
blank
=
False
,
max_length
=
255
)
def
__unicode__
(
self
):
"""
Returns a string representation of this model.
"""
return
u"{} user: {}, course version: {}, grading policy: {}, percent grade {}
%
, letter grade {}"
.
format
(
type
(
self
)
.
__name__
,
self
.
user_id
,
self
.
course_version
,
self
.
grading_policy_hash
,
self
.
percent_grade
,
self
.
letter_grade
,
)
@classmethod
def
read_course_grade
(
cls
,
user_id
,
course_id
):
"""
Reads a grade from database
Arguments:
user_id: The user associated with the desired grade
course_id: The id of the course associated with the desired grade
Raises PersistentCourseGrade.DoesNotExist if applicable
"""
return
cls
.
objects
.
get
(
user_id
=
user_id
,
course_id
=
course_id
)
@classmethod
def
update_or_create_course_grade
(
cls
,
user_id
,
course_id
,
course_version
=
None
,
**
kwargs
):
"""
Creates a course grade in the database.
Returns a PersistedCourseGrade object.
"""
if
course_version
is
None
:
course_version
=
""
grade
,
_
=
cls
.
objects
.
update_or_create
(
user_id
=
user_id
,
course_id
=
course_id
,
course_version
=
course_version
,
defaults
=
kwargs
)
return
grade
lms/djangoapps/grades/tests/test_models.py
View file @
9d71ac2e
...
...
@@ -3,6 +3,7 @@ Unit tests for grades models.
"""
from
base64
import
b64encode
from
collections
import
OrderedDict
from
datetime
import
datetime
import
ddt
from
hashlib
import
sha1
import
json
...
...
@@ -15,6 +16,7 @@ from lms.djangoapps.grades.models import (
BlockRecord
,
BlockRecordList
,
BLOCK_RECORD_LIST_VERSION
,
PersistentCourseGrade
,
PersistentSubsectionGrade
,
VisibleBlocks
)
...
...
@@ -157,8 +159,8 @@ class VisibleBlocksTest(GradesModelTestCase):
self
.
assertNotEqual
(
stored_vblocks
.
pk
,
repeat_vblocks
.
pk
)
self
.
assertNotEqual
(
stored_vblocks
.
hashed
,
repeat_vblocks
.
hashed
)
self
.
assertEqual
s
(
stored_vblocks
.
pk
,
same_order_vblocks
.
pk
)
self
.
assertEqual
s
(
stored_vblocks
.
hashed
,
same_order_vblocks
.
hashed
)
self
.
assertEqual
(
stored_vblocks
.
pk
,
same_order_vblocks
.
pk
)
self
.
assertEqual
(
stored_vblocks
.
hashed
,
same_order_vblocks
.
hashed
)
self
.
assertNotEqual
(
stored_vblocks
.
pk
,
new_vblocks
.
pk
)
self
.
assertNotEqual
(
stored_vblocks
.
hashed
,
new_vblocks
.
hashed
)
...
...
@@ -212,7 +214,7 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
usage_key
=
self
.
params
[
"usage_key"
],
)
self
.
assertEqual
(
created_grade
,
read_grade
)
self
.
assertEqual
s
(
read_grade
.
visible_blocks
.
blocks
,
self
.
block_records
)
self
.
assertEqual
(
read_grade
.
visible_blocks
.
blocks
,
self
.
block_records
)
with
self
.
assertRaises
(
IntegrityError
):
PersistentSubsectionGrade
.
create_grade
(
**
self
.
params
)
...
...
@@ -234,7 +236,71 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
self
.
params
[
"earned_all"
]
=
7
updated_grade
=
PersistentSubsectionGrade
.
update_or_create_grade
(
**
self
.
params
)
self
.
assertEqual
s
(
updated_grade
.
earned_all
,
7
)
self
.
assertEqual
(
updated_grade
.
earned_all
,
7
)
if
already_created
:
self
.
assertEquals
(
created_grade
.
id
,
updated_grade
.
id
)
self
.
assertEquals
(
created_grade
.
earned_all
,
6
)
self
.
assertEqual
(
created_grade
.
id
,
updated_grade
.
id
)
self
.
assertEqual
(
created_grade
.
earned_all
,
6
)
@ddt.ddt
class
PersistentCourseGradesTest
(
GradesModelTestCase
):
"""
Tests the PersistentCourseGrade model.
"""
def
setUp
(
self
):
super
(
PersistentCourseGradesTest
,
self
)
.
setUp
()
self
.
params
=
{
"user_id"
:
12345
,
"course_id"
:
self
.
course_key
,
"course_version"
:
"JoeMcEwing"
,
"course_edited_timestamp"
:
datetime
(
year
=
2016
,
month
=
8
,
day
=
1
,
hour
=
18
,
minute
=
53
,
second
=
24
,
microsecond
=
354741
,
),
"percent_grade"
:
77.7
,
"letter_grade"
:
"Great job"
,
}
def
test_update
(
self
):
created_grade
=
PersistentCourseGrade
.
objects
.
create
(
**
self
.
params
)
self
.
params
[
"percent_grade"
]
=
88.8
self
.
params
[
"letter_grade"
]
=
"Better job"
updated_grade
=
PersistentCourseGrade
.
update_or_create_course_grade
(
**
self
.
params
)
self
.
assertEqual
(
updated_grade
.
percent_grade
,
88.8
)
self
.
assertEqual
(
updated_grade
.
letter_grade
,
"Better job"
)
self
.
assertEqual
(
created_grade
.
id
,
updated_grade
.
id
)
def
test_create_and_read_grade
(
self
):
created_grade
=
PersistentCourseGrade
.
update_or_create_course_grade
(
**
self
.
params
)
read_grade
=
PersistentCourseGrade
.
read_course_grade
(
self
.
params
[
"user_id"
],
self
.
params
[
"course_id"
])
for
param
in
self
.
params
:
self
.
assertEqual
(
self
.
params
[
param
],
getattr
(
created_grade
,
param
))
self
.
assertEqual
(
created_grade
,
read_grade
)
def
test_course_version_optional
(
self
):
del
self
.
params
[
"course_version"
]
grade
=
PersistentCourseGrade
.
update_or_create_course_grade
(
**
self
.
params
)
self
.
assertEqual
(
""
,
grade
.
course_version
)
@ddt.data
(
(
"percent_grade"
,
"Not a float at all"
,
ValueError
),
(
"percent_grade"
,
None
,
IntegrityError
),
(
"letter_grade"
,
None
,
IntegrityError
),
(
"course_id"
,
"Not a course key at all"
,
AssertionError
),
(
"user_id"
,
None
,
IntegrityError
),
(
"grading_policy_hash"
,
None
,
IntegrityError
)
)
@ddt.unpack
def
test_update_or_create_with_bad_params
(
self
,
param
,
val
,
error
):
self
.
params
[
param
]
=
val
with
self
.
assertRaises
(
error
):
PersistentCourseGrade
.
update_or_create_course_grade
(
**
self
.
params
)
def
test_grade_does_not_exist
(
self
):
with
self
.
assertRaises
(
PersistentCourseGrade
.
DoesNotExist
):
PersistentCourseGrade
.
read_course_grade
(
self
.
params
[
"user_id"
],
self
.
params
[
"course_id"
])
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