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
1db70deb
Commit
1db70deb
authored
Oct 06, 2016
by
sanfordstudent
Committed by
GitHub
Oct 06, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #13643 from edx/sstudent/TNL-5310
SQL model for course grades
parents
42f8b4a2
9d71ac2e
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 @
1db70deb
# -*- 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 @
1db70deb
...
...
@@ -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 @
1db70deb
...
...
@@ -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