Commit 9d71ac2e by Sanford Student

SQL model for course grades

Includes unit tests
For TNL-5310
parent 1e1b7e1a
# -*- 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"])
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment