Commit d244715e by Nimisha Asthagiri

Don't sort blocks to retain order.

parent 660bc8f4
...@@ -29,13 +29,16 @@ log = logging.getLogger(__name__) ...@@ -29,13 +29,16 @@ log = logging.getLogger(__name__)
BlockRecord = namedtuple('BlockRecord', ['locator', 'weight', 'max_score']) BlockRecord = namedtuple('BlockRecord', ['locator', 'weight', 'max_score'])
class BlockRecordSet(frozenset): class BlockRecordList(tuple):
""" """
An immutable ordered collection of BlockRecord objects. An immutable ordered list of BlockRecord objects.
""" """
def __init__(self, *args, **kwargs): def __new__(cls, blocks):
super(BlockRecordSet, self).__init__(*args, **kwargs) return super(BlockRecordList, cls).__new__(cls, tuple(blocks))
def __init__(self, blocks):
super(BlockRecordList, self).__init__(blocks)
self._json = None self._json = None
self._hash = None self._hash = None
...@@ -56,8 +59,7 @@ class BlockRecordSet(frozenset): ...@@ -56,8 +59,7 @@ class BlockRecordSet(frozenset):
stable ordering. stable ordering.
""" """
if self._json is None: if self._json is None:
sorted_blocks = sorted(self, key=attrgetter('locator')) list_of_block_dicts = [block._asdict() for block in self]
list_of_block_dicts = [block._asdict() for block in sorted_blocks]
course_key_string = self._get_course_key_string() # all blocks are from the same course course_key_string = self._get_course_key_string() # all blocks are from the same course
for block_dict in list_of_block_dicts: for block_dict in list_of_block_dicts:
...@@ -77,7 +79,7 @@ class BlockRecordSet(frozenset): ...@@ -77,7 +79,7 @@ class BlockRecordSet(frozenset):
@classmethod @classmethod
def from_json(cls, blockrecord_json): def from_json(cls, blockrecord_json):
""" """
Return a BlockRecordSet from a json list. Return a BlockRecordList from a previously serialized json.
""" """
data = json.loads(blockrecord_json) data = json.loads(blockrecord_json)
course_key = data['course_key'] course_key = data['course_key']
...@@ -97,6 +99,13 @@ class BlockRecordSet(frozenset): ...@@ -97,6 +99,13 @@ class BlockRecordSet(frozenset):
) )
return cls(record_generator) return cls(record_generator)
@classmethod
def from_list(cls, blocks):
"""
Return a BlockRecordList from a list.
"""
return cls(tuple(blocks))
def to_hash(self): def to_hash(self):
""" """
Return a hashed version of the list of block records. Return a hashed version of the list of block records.
...@@ -120,24 +129,18 @@ class VisibleBlocksQuerySet(models.QuerySet): ...@@ -120,24 +129,18 @@ class VisibleBlocksQuerySet(models.QuerySet):
""" """
Creates a new VisibleBlocks model object. Creates a new VisibleBlocks model object.
Argument 'blocks' should be a BlockRecordSet. Argument 'blocks' should be a BlockRecordList.
""" """
if not isinstance(blocks, BlockRecordSet):
blocks = BlockRecordSet(blocks)
model, _ = self.get_or_create(hashed=blocks.to_hash(), defaults={'blocks_json': blocks.to_json()}) model, _ = self.get_or_create(hashed=blocks.to_hash(), defaults={'blocks_json': blocks.to_json()})
return model return model
def hash_from_blockrecords(self, blocks): def hash_from_blockrecords(self, blocks):
""" """
Return the hash for a given BlockRecordSet, serializing the records if Return the hash for a given list of blocks, saving the records if
possible, but returning the hash even if an IntegrityError occurs. possible, but returning the hash even if an IntegrityError occurs.
"""
if not isinstance(blocks, BlockRecordSet):
blocks = BlockRecordSet(blocks)
Argument 'blocks' should be a BlockRecordList.
"""
try: try:
with transaction.atomic(): with transaction.atomic():
model = self.create_from_blockrecords(blocks) model = self.create_from_blockrecords(blocks)
...@@ -176,7 +179,7 @@ class VisibleBlocks(models.Model): ...@@ -176,7 +179,7 @@ class VisibleBlocks(models.Model):
Returns the blocks_json data stored on this model as a list of Returns the blocks_json data stored on this model as a list of
BlockRecords in the order they were provided. BlockRecords in the order they were provided.
""" """
return BlockRecordSet.from_json(self.blocks_json) return BlockRecordList.from_json(self.blocks_json)
class PersistentSubsectionGradeQuerySet(models.QuerySet): class PersistentSubsectionGradeQuerySet(models.QuerySet):
...@@ -204,7 +207,7 @@ class PersistentSubsectionGradeQuerySet(models.QuerySet): ...@@ -204,7 +207,7 @@ class PersistentSubsectionGradeQuerySet(models.QuerySet):
if not kwargs.get('course_id', None): if not kwargs.get('course_id', None):
kwargs['course_id'] = kwargs['usage_key'].course_key kwargs['course_id'] = kwargs['usage_key'].course_key
visible_blocks_hash = VisibleBlocks.objects.hash_from_blockrecords(blocks=visible_blocks) visible_blocks_hash = VisibleBlocks.objects.hash_from_blockrecords(BlockRecordList.from_list(visible_blocks))
return super(PersistentSubsectionGradeQuerySet, self).create( return super(PersistentSubsectionGradeQuerySet, self).create(
visible_blocks_id=visible_blocks_hash, visible_blocks_id=visible_blocks_hash,
**kwargs **kwargs
...@@ -358,7 +361,7 @@ class PersistentSubsectionGrade(TimeStampedModel): ...@@ -358,7 +361,7 @@ class PersistentSubsectionGrade(TimeStampedModel):
Modify an existing PersistentSubsectionGrade object, saving the new Modify an existing PersistentSubsectionGrade object, saving the new
version. version.
""" """
visible_blocks_hash = VisibleBlocks.objects.hash_from_blockrecords(blocks=visible_blocks) visible_blocks_hash = VisibleBlocks.objects.hash_from_blockrecords(BlockRecordList.from_list(visible_blocks))
self.course_version = course_version or "" self.course_version = course_version or ""
self.subtree_edited_timestamp = subtree_edited_timestamp self.subtree_edited_timestamp = subtree_edited_timestamp
......
...@@ -14,27 +14,27 @@ from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator ...@@ -14,27 +14,27 @@ from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator
from lms.djangoapps.grades.models import ( from lms.djangoapps.grades.models import (
BlockRecord, BlockRecord,
BlockRecordSet, BlockRecordList,
PersistentSubsectionGrade, PersistentSubsectionGrade,
VisibleBlocks VisibleBlocks
) )
class BlockRecordSetTestCase(TestCase): class BlockRecordListTestCase(TestCase):
""" """
Verify the behavior of BlockRecordSets, particularly around edge cases Verify the behavior of BlockRecordList, particularly around edge cases
""" """
empty_json = '{"blocks":[],"course_key":null}' empty_json = '{"blocks":[],"course_key":null}'
def test_empty_block_record_set(self): def test_empty_block_record_set(self):
brs = BlockRecordSet() brs = BlockRecordList(())
self.assertFalse(brs) self.assertFalse(brs)
self.assertEqual( self.assertEqual(
brs.to_json(), brs.to_json(),
self.empty_json self.empty_json
) )
self.assertEqual( self.assertEqual(
BlockRecordSet.from_json(self.empty_json), BlockRecordList.from_json(self.empty_json),
brs brs
) )
...@@ -108,11 +108,17 @@ class VisibleBlocksTest(GradesModelTestCase): ...@@ -108,11 +108,17 @@ class VisibleBlocksTest(GradesModelTestCase):
""" """
Test the VisibleBlocks model. Test the VisibleBlocks model.
""" """
def _create_block_record_list(self, blocks):
"""
Creates and returns a BlockRecordList for the given blocks.
"""
return VisibleBlocks.objects.create_from_blockrecords(BlockRecordList.from_list(blocks))
def test_creation(self): def test_creation(self):
""" """
Happy path test to ensure basic create functionality works as expected. Happy path test to ensure basic create functionality works as expected.
""" """
vblocks = VisibleBlocks.objects.create_from_blockrecords([self.record_a]) vblocks = self._create_block_record_list([self.record_a])
list_of_block_dicts = [self.record_a._asdict()] list_of_block_dicts = [self.record_a._asdict()]
for block_dict in list_of_block_dicts: for block_dict in list_of_block_dicts:
block_dict['locator'] = unicode(block_dict['locator']) # BlockUsageLocator is not json-serializable block_dict['locator'] = unicode(block_dict['locator']) # BlockUsageLocator is not json-serializable
...@@ -128,30 +134,34 @@ class VisibleBlocksTest(GradesModelTestCase): ...@@ -128,30 +134,34 @@ class VisibleBlocksTest(GradesModelTestCase):
self.assertEqual(expected_json, vblocks.blocks_json) self.assertEqual(expected_json, vblocks.blocks_json)
self.assertEqual(expected_hash, vblocks.hashed) self.assertEqual(expected_hash, vblocks.hashed)
def test_ordering_does_not_matter(self): def test_ordering_matters(self):
""" """
When creating new vblocks, a different ordering of blocks produces the When creating new vblocks, different ordering of blocks produces
same record in the database. different records in the database.
""" """
stored_vblocks = VisibleBlocks.objects.create_from_blockrecords([self.record_a, self.record_b]) stored_vblocks = self._create_block_record_list([self.record_a, self.record_b])
repeat_vblocks = VisibleBlocks.objects.create_from_blockrecords([self.record_b, self.record_a]) repeat_vblocks = self._create_block_record_list([self.record_b, self.record_a])
new_vblocks = VisibleBlocks.objects.create_from_blockrecords([self.record_b]) same_order_vblocks = self._create_block_record_list([self.record_a, self.record_b])
new_vblocks = self._create_block_record_list([self.record_b])
self.assertNotEqual(stored_vblocks.pk, repeat_vblocks.pk)
self.assertNotEqual(stored_vblocks.hashed, repeat_vblocks.hashed)
self.assertEqual(stored_vblocks.pk, repeat_vblocks.pk) self.assertEquals(stored_vblocks.pk, same_order_vblocks.pk)
self.assertEqual(stored_vblocks.hashed, repeat_vblocks.hashed) self.assertEquals(stored_vblocks.hashed, same_order_vblocks.hashed)
self.assertNotEqual(stored_vblocks.pk, new_vblocks.pk) self.assertNotEqual(stored_vblocks.pk, new_vblocks.pk)
self.assertNotEqual(stored_vblocks.hashed, new_vblocks.hashed) self.assertNotEqual(stored_vblocks.hashed, new_vblocks.hashed)
def test_blocks_property(self): def test_blocks_property(self):
""" """
Ensures that, given an array of BlockRecord, creating visible_blocks and accessing Ensures that, given an array of BlockRecord, creating visible_blocks
visible_blocks.blocks yields a copy of the initial array. Also, trying to set the blocks property should raise and accessing visible_blocks.blocks yields a copy of the initial array.
an exception. Also, trying to set the blocks property should raise an exception.
""" """
expected_blocks = [self.record_a, self.record_b] expected_blocks = (self.record_a, self.record_b)
visible_blocks = VisibleBlocks.objects.create_from_blockrecords(expected_blocks) visible_blocks = self._create_block_record_list(expected_blocks)
self.assertEqual(BlockRecordSet(expected_blocks), visible_blocks.blocks) self.assertEqual(expected_blocks, visible_blocks.blocks)
with self.assertRaises(AttributeError): with self.assertRaises(AttributeError):
visible_blocks.blocks = expected_blocks visible_blocks.blocks = expected_blocks
......
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