Implement a BaseStudentModuleHistory

This abstract class contains most of the fields (aside from the id and
foreign key to StudentModule that the subclasses need to manage).  It
also provides a get_history method that abstracts searching across
multiple backends.

Move router code to openedx/core
We need to use it from cms and lms.
Ensure aws_migrate can be used for migrating both the lms and cms.

Handle queries directed to student_module_history vs default and the
extra queries generated by Django 1.8 (SAVEPOINTS, etc).

Additionally, flag testing classes as multi_db so that Django will
flush the non-default database between unit tests.

Further decouple the foreignkey relation between csm and csmhe

When calling StudentModule().delete() Django will try to delete CSMHE
objects, but naively does so in the database, not by consulting the
database router.

Instead, we disable django cascading deletes and listen for post_delete
signals and clean up CSMHE by hand.

Add feature flags for CSMHE
One to turn it on/off so we can control the deploy.
The other will control whether or not we read from two database tables
or one when searching.

Update tests to explicitly use this get_history method rather than
looking directly into StudentModuleHistory or
StudentModuleHistoryExtended.

Inform lettuce to avoid the coursewarehistoryextended app

Otherwise it fails when it can't find features/ in that app.

Add Pg support, this is not tested automatically.
This commit is contained in:
Kevin Falcone
2016-01-28 10:32:28 -05:00
parent dd90c55602
commit 6f9a3911e5
25 changed files with 366 additions and 149 deletions

View File

@@ -103,6 +103,8 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase):
"""Tests for user_state storage via StudentModule"""
other_key_factory = partial(DjangoKeyValueStore.Key, Scope.user_state, 2, location('usage_id')) # user_id=2, not 1
existing_field_name = "a_field"
# Tell Django to clean out all databases, not just default
multi_db = True
def setUp(self):
super(TestStudentModuleStorage, self).setUp()
@@ -137,8 +139,9 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase):
# to discover if something other than the DjangoXBlockUserStateClient
# has written to the StudentModule (such as UserStateCache setting the score
# on the StudentModule).
with self.assertNumQueries(3):
self.kvs.set(user_state_key('a_field'), 'new_value')
with self.assertNumQueries(2, using='default'):
with self.assertNumQueries(1, using='student_module_history'):
self.kvs.set(user_state_key('a_field'), 'new_value')
self.assertEquals(1, StudentModule.objects.all().count())
self.assertEquals({'b_field': 'b_value', 'a_field': 'new_value'}, json.loads(StudentModule.objects.all()[0].state))
@@ -149,8 +152,9 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase):
# to discover if something other than the DjangoXBlockUserStateClient
# has written to the StudentModule (such as UserStateCache setting the score
# on the StudentModule).
with self.assertNumQueries(3):
self.kvs.set(user_state_key('not_a_field'), 'new_value')
with self.assertNumQueries(2, using='default'):
with self.assertNumQueries(1, using='student_module_history'):
self.kvs.set(user_state_key('not_a_field'), 'new_value')
self.assertEquals(1, StudentModule.objects.all().count())
self.assertEquals({'b_field': 'b_value', 'a_field': 'a_value', 'not_a_field': 'new_value'}, json.loads(StudentModule.objects.all()[0].state))
@@ -161,8 +165,9 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase):
# to discover if something other than the DjangoXBlockUserStateClient
# has written to the StudentModule (such as UserStateCache setting the score
# on the StudentModule).
with self.assertNumQueries(3):
self.kvs.delete(user_state_key('a_field'))
with self.assertNumQueries(2, using='default'):
with self.assertNumQueries(1, using='student_module_history'):
self.kvs.delete(user_state_key('a_field'))
self.assertEquals(1, StudentModule.objects.all().count())
self.assertRaises(KeyError, self.kvs.get, user_state_key('not_a_field'))
@@ -201,8 +206,9 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase):
# We also need to read the database to discover if something other than the
# DjangoXBlockUserStateClient has written to the StudentModule (such as
# UserStateCache setting the score on the StudentModule).
with self.assertNumQueries(3):
self.kvs.set_many(kv_dict)
with self.assertNumQueries(2, using="default"):
with self.assertNumQueries(1, using="student_module_history"):
self.kvs.set_many(kv_dict)
for key in kv_dict:
self.assertEquals(self.kvs.get(key), kv_dict[key])
@@ -223,6 +229,9 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase):
@attr('shard_1')
class TestMissingStudentModule(TestCase):
# Tell Django to clean out all databases, not just default
multi_db = True
def setUp(self):
super(TestMissingStudentModule, self).setUp()
@@ -244,12 +253,13 @@ class TestMissingStudentModule(TestCase):
self.assertEquals(0, len(self.field_data_cache))
self.assertEquals(0, StudentModule.objects.all().count())
# We are updating a problem, so we write to courseware_studentmodulehistory
# We are updating a problem, so we write to courseware_studentmodulehistoryextended
# as well as courseware_studentmodule. We also need to read the database
# to discover if something other than the DjangoXBlockUserStateClient
# has written to the StudentModule (such as UserStateCache setting the score
# on the StudentModule).
with self.assertNumQueries(2, using='default'):
# Django 1.8 also has a number of other BEGIN and SAVESTATE queries.
with self.assertNumQueries(4, using='default'):
with self.assertNumQueries(1, using='student_module_history'):
self.kvs.set(user_state_key('a_field'), 'a_value')