SQL model for course grades
Includes unit tests For TNL-5310
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
# -*- 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')]),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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.assertEquals(stored_vblocks.pk, same_order_vblocks.pk)
|
||||
self.assertEquals(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.assertEquals(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.assertEquals(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"])
|
||||
|
||||
Reference in New Issue
Block a user