Commit 8fa2c233 by Eric Fischer Committed by J. Cliff Dyer

TNL-5005 Persistent Grades Data Model

Introduces database models and simple CRUD interface for robust grading
usage.
parent 8adf87f9
......@@ -14,7 +14,7 @@ from .new.course_grade import CourseGradeFactory
log = getLogger(__name__)
GradeResult = namedtuple('StudentGrade', ['student', 'gradeset', 'err_msg'])
GradeResult = namedtuple('GradeResult', ['student', 'gradeset', 'err_msg'])
def iterate_grades_for(course_or_id, students):
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import coursewarehistoryextended.fields
import django.utils.timezone
import model_utils.fields
import xmodule_django.models
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='PersistentSubsectionGrade',
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()),
('course_id', xmodule_django.models.CourseKeyField(max_length=255)),
('usage_key', xmodule_django.models.UsageKeyField(max_length=255)),
('subtree_edited_date', models.DateTimeField(verbose_name=b'last content edit timestamp')),
('course_version', models.CharField(max_length=255, verbose_name=b'guid of latest course version', blank=True)),
('earned_all', models.FloatField()),
('possible_all', models.FloatField()),
('earned_graded', models.FloatField()),
('possible_graded', models.FloatField()),
],
),
migrations.CreateModel(
name='VisibleBlocks',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('blocks_json', models.TextField()),
('hashed', models.CharField(unique=True, max_length=100)),
],
),
migrations.AddField(
model_name='persistentsubsectiongrade',
name='visible_blocks',
field=models.ForeignKey(to='grades.VisibleBlocks', db_column=b'visible_blocks_hash', to_field=b'hashed'),
),
migrations.AlterUniqueTogether(
name='persistentsubsectiongrade',
unique_together=set([('course_id', 'user_id', 'usage_key')]),
),
]
"""
Unit tests for grades models.
"""
from base64 import b64encode
from collections import OrderedDict
import ddt
from hashlib import sha1
import json
from mock import patch
from django.core.exceptions import ValidationError
from django.test import TestCase
from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator
from lms.djangoapps.grades.models import BlockRecord, BlockRecordSet, PersistentSubsectionGrade, VisibleBlocks
class GradesModelTestCase(TestCase):
"""
Base class for common setup of grades model tests.
"""
def setUp(self):
super(GradesModelTestCase, self).setUp()
self.course_key = CourseLocator(
org='some_org',
course='some_course',
run='some_run'
)
self.locator_a = BlockUsageLocator(
course_key=self.course_key,
block_type='problem',
block_id='block_id_a'
)
self.locator_b = BlockUsageLocator(
course_key=self.course_key,
block_type='problem',
block_id='block_id_b'
)
self.record_a = BlockRecord(unicode(self.locator_a), 1, 10)
self.record_b = BlockRecord(unicode(self.locator_b), 1, 10)
@ddt.ddt
class BlockRecordTest(GradesModelTestCase):
"""
Test the BlockRecord model.
"""
def setUp(self):
super(BlockRecordTest, self).setUp()
def test_creation(self):
"""
Tests creation of a BlockRecord.
"""
weight = 1
max_score = 10
record = BlockRecord(
self.locator_a,
weight,
max_score,
)
self.assertEqual(record.locator, self.locator_a)
@ddt.data(
(0, 0, "0123456789abcdef"),
(1, 10, 'totally_a_real_block_key'),
("BlockRecord is", "a dumb data store", "with no validation"),
)
@ddt.unpack
def test_serialization(self, weight, max_score, block_key):
"""
Tests serialization of a BlockRecord using the to_dict() method.
"""
record = BlockRecord(block_key, weight, max_score)
expected = OrderedDict([
("locator", block_key),
("weight", weight),
("max_score", max_score),
])
self.assertEqual(expected, record._asdict())
class VisibleBlocksTest(GradesModelTestCase):
"""
Test the VisibleBlocks model.
"""
def test_creation(self):
"""
Happy path test to ensure basic create functionality works as expected.
"""
vblocks = VisibleBlocks.objects.create_from_blockrecords([self.record_a])
expected_json = json.dumps([self.record_a._asdict()], separators=(',', ':'), sort_keys=True)
expected_hash = b64encode(sha1(expected_json).digest())
self.assertEqual(expected_json, vblocks.blocks_json)
self.assertEqual(expected_hash, vblocks.hashed)
def test_ordering_does_not_matter(self):
"""
When creating new vblocks, a different ordering of blocks produces the
same record in the database.
"""
stored_vblocks = VisibleBlocks.objects.create_from_blockrecords([self.record_a, self.record_b])
repeat_vblocks = VisibleBlocks.objects.create_from_blockrecords([self.record_b, self.record_a])
new_vblocks = VisibleBlocks.objects.create_from_blockrecords([self.record_b])
self.assertEqual(stored_vblocks.pk, repeat_vblocks.pk)
self.assertEqual(stored_vblocks.hashed, repeat_vblocks.hashed)
self.assertNotEqual(stored_vblocks.pk, new_vblocks.pk)
self.assertNotEqual(stored_vblocks.hashed, new_vblocks.hashed)
def test_blocks_property(self):
"""
Ensures that, given an array of BlockRecord, creating visible_blocks and accessing visible_blocks.blocks yields
a copy of the initial array. Also, trying to set the blocks property should raise an exception.
"""
expected_blocks = [self.record_a, self.record_b]
visible_blocks = VisibleBlocks.objects.create_from_blockrecords(expected_blocks)
self.assertEqual(BlockRecordSet(expected_blocks), visible_blocks.blocks)
with self.assertRaises(AttributeError):
visible_blocks.blocks = expected_blocks
@ddt.ddt
class PersistentSubsectionGradeTest(GradesModelTestCase):
"""
Test the PersistentSubsectionGrade model.
"""
def setUp(self):
super(PersistentSubsectionGradeTest, self).setUp()
self.usage_key = BlockUsageLocator(
course_key=self.course_key,
block_type='subsection',
block_id='subsection_12345',
)
self.params = {
"user_id": 12345,
"usage_key": self.usage_key,
"course_version": "deadbeef",
"subtree_edited_date": "2016-08-01 18:53:24.354741",
"earned_all": 6,
"possible_all": 12,
"earned_graded": 6,
"possible_graded": 8,
"visible_blocks": [self.record_a, self.record_b],
}
def test_create(self):
"""
Tests model creation, and confirms error when trying to recreate model.
"""
created_grade = PersistentSubsectionGrade.objects.create(**self.params)
read_grade = PersistentSubsectionGrade.read_grade(
user_id=self.params["user_id"],
usage_key=self.params["usage_key"],
)
self.assertEqual(created_grade, read_grade)
with self.assertRaises(ValidationError):
created_grade = PersistentSubsectionGrade.objects.create(**self.params)
def test_create_bad_params(self):
"""
Confirms create will fail if params are missing.
"""
del self.params["earned_graded"]
with self.assertRaises(ValidationError):
PersistentSubsectionGrade.objects.create(**self.params)
def test_course_version_is_optional(self):
del self.params["course_version"]
PersistentSubsectionGrade.objects.create(**self.params)
def test_update_grade(self):
"""
Tests model update, and confirms error when updating a nonexistent model.
"""
with self.assertRaises(PersistentSubsectionGrade.DoesNotExist):
PersistentSubsectionGrade.update_grade(**self.params)
PersistentSubsectionGrade.objects.create(**self.params)
self.params['earned_all'] = 12
self.params['earned_graded'] = 8
PersistentSubsectionGrade.update_grade(**self.params)
read_grade = PersistentSubsectionGrade.read_grade(
user_id=self.params["user_id"],
usage_key=self.params["usage_key"],
)
self.assertEqual(read_grade.earned_all, 12)
self.assertEqual(read_grade.earned_graded, 8)
@ddt.data(True, False)
def test_save(self, already_created):
if already_created:
PersistentSubsectionGrade.objects.create(**self.params)
module_prefix = "lms.djangoapps.grades.models.PersistentSubsectionGrade."
with patch(
module_prefix + "objects.get_or_create",
wraps=PersistentSubsectionGrade.objects.get_or_create
) as mock_get_or_create:
with patch(module_prefix + "update") as mock_update:
PersistentSubsectionGrade.save_grade(**self.params)
self.assertTrue(mock_get_or_create.called)
self.assertEqual(mock_update.called, already_created)
......@@ -1891,7 +1891,7 @@ INSTALLED_APPS = (
'openedx.core.djangoapps.course_groups',
'bulk_email',
'branding',
'grades',
'lms.djangoapps.grades',
# Student support tools
'support',
......
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