Commit 2e06e592 by Calen Pennington

Use DjangoXBlockUserStateClient to implement UserStateCache

parent 257660ed
......@@ -134,7 +134,10 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase):
def test_set_existing_field(self):
"Test that setting an existing user_state field changes the value"
# We are updating a problem, so we write to courseware_studentmodulehistory
# as well as courseware_studentmodule
# 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(3):
self.kvs.set(user_state_key('a_field'), 'new_value')
self.assertEquals(1, StudentModule.objects.all().count())
......@@ -143,7 +146,10 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase):
def test_set_missing_field(self):
"Test that setting a new user_state field changes the value"
# We are updating a problem, so we write to courseware_studentmodulehistory
# as well as courseware_studentmodule
# 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(3):
self.kvs.set(user_state_key('not_a_field'), 'new_value')
self.assertEquals(1, StudentModule.objects.all().count())
......@@ -152,7 +158,10 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase):
def test_delete_existing_field(self):
"Test that deleting an existing field removes it from the StudentModule"
# We are updating a problem, so we write to courseware_studentmodulehistory
# as well as courseware_studentmodule
# 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(3):
self.kvs.delete(user_state_key('a_field'))
self.assertEquals(1, StudentModule.objects.all().count())
......@@ -190,6 +199,9 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase):
# Scope.user_state is stored in a single row in the database, so we only
# need to send a single update to that table.
# We also are updating a problem, so we write to courseware student module history
# 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)
......@@ -207,7 +219,7 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase):
with patch('django.db.models.Model.save', side_effect=DatabaseError):
with self.assertRaises(KeyValueMultiSaveError) as exception_context:
self.kvs.set_many(kv_dict)
self.assertEquals(len(exception_context.exception.saved_field_names), 0)
self.assertEquals(exception_context.exception.saved_field_names, [])
@attr('shard_1')
......@@ -234,8 +246,11 @@ class TestMissingStudentModule(TestCase):
self.assertEquals(0, StudentModule.objects.all().count())
# We are updating a problem, so we write to courseware_studentmodulehistory
# as well as courseware_studentmodule
with self.assertNumQueries(6):
# 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(3):
self.kvs.set(user_state_key('a_field'), 'a_value')
self.assertEquals(1, sum(len(cache) for cache in self.field_data_cache.cache.values()))
......@@ -289,7 +304,7 @@ class StorageTestBase(object):
self.kvs = DjangoKeyValueStore(self.field_data_cache)
def test_set_and_get_existing_field(self):
with self.assertNumQueries(2):
with self.assertNumQueries(1):
self.kvs.set(self.key_factory('existing_field'), 'test_value')
with self.assertNumQueries(0):
self.assertEquals('test_value', self.kvs.get(self.key_factory('existing_field')))
......@@ -306,14 +321,14 @@ class StorageTestBase(object):
def test_set_existing_field(self):
"Test that setting an existing field changes the value"
with self.assertNumQueries(2):
with self.assertNumQueries(1):
self.kvs.set(self.key_factory('existing_field'), 'new_value')
self.assertEquals(1, self.storage_class.objects.all().count())
self.assertEquals('new_value', json.loads(self.storage_class.objects.all()[0].value))
def test_set_missing_field(self):
"Test that setting a new field changes the value"
with self.assertNumQueries(4):
with self.assertNumQueries(1):
self.kvs.set(self.key_factory('missing_field'), 'new_value')
self.assertEquals(2, self.storage_class.objects.all().count())
self.assertEquals('old_value', json.loads(self.storage_class.objects.get(field_name='existing_field').value))
......@@ -355,7 +370,7 @@ class StorageTestBase(object):
# Each field is a separate row in the database, hence
# a separate query
with self.assertNumQueries(len(kv_dict) * 3):
with self.assertNumQueries(len(kv_dict)):
self.kvs.set_many(kv_dict)
for key in kv_dict:
self.assertEquals(self.kvs.get(key), kv_dict[key])
......@@ -363,8 +378,8 @@ class StorageTestBase(object):
def test_set_many_failure(self):
"""Test that setting many regular fields with a DB error """
kv_dict = self.construct_kv_dict()
with self.assertNumQueries(6):
for key in kv_dict:
for key in kv_dict:
with self.assertNumQueries(1):
self.kvs.set(key, 'test value')
with patch('django.db.models.Model.save', side_effect=[None, DatabaseError]):
......@@ -372,8 +387,7 @@ class StorageTestBase(object):
self.kvs.set_many(kv_dict)
exception = exception_context.exception
self.assertEquals(len(exception.saved_field_names), 1)
self.assertIn(exception.saved_field_names[0], ('existing_field', 'other_existing_field'))
self.assertEquals(exception.saved_field_names, ['existing_field', 'other_existing_field'])
class TestUserStateSummaryStorage(StorageTestBase, TestCase):
......
......@@ -40,7 +40,6 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
pass
def __init__(self, user):
self._student_module_cache = {}
self.user = user
def get(self, username, block_key, scope=Scope.user_state, fields=None):
......@@ -93,6 +92,28 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
assert self.user.username == username
return self.delete_many(username, [block_key], scope, fields=fields)
@contract(username="basestring", block_key=UsageKey, scope=ScopeBase, fields="seq(basestring)|set(basestring)|None")
def get_mod_date(self, username, block_key, scope=Scope.user_state, fields=None):
"""
Get the last modification date for fields from the specified blocks.
Arguments:
username: The name of the user whose state should be deleted
block_key (UsageKey): The UsageKey identifying which xblock modification dates to retrieve.
scope (Scope): The scope to retrieve from.
fields: A list of fields to query. If None, delete all stored fields.
Specific implementations are free to return the same modification date
for all fields, if they don't store changes individually per field.
Implementations may omit fields for which data has not been stored.
Returns: list a dict of {field_name: modified_date} for each selected field.
"""
results = self.get_mod_date_many(username, [block_key], scope, fields=fields)
return {
field: date for (_, field, date) in results
}
@contract(username="basestring", block_keys="seq(UsageKey)|set(UsageKey)")
def _get_field_objects(self, username, block_keys):
"""
Retrieve the :class:`~StudentModule`s for the supplied ``username`` and ``block_keys``.
......@@ -108,23 +129,15 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
)
for course_key, usage_keys in by_course:
not_cached = []
for usage_key in usage_keys:
if usage_key in self._student_module_cache:
yield self._student_module_cache[usage_key]
else:
not_cached.append(usage_key)
query = StudentModule.objects.chunked_filter(
'module_state_key__in',
not_cached,
usage_keys,
student__username=username,
course_id=course_key,
)
for student_module in query:
usage_key = student_module.module_state_key.map_into_course(student_module.course_id)
self._student_module_cache[usage_key] = student_module
yield (student_module, usage_key)
......@@ -170,14 +183,10 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
if scope != Scope.user_state:
raise ValueError("Only Scope.user_state is supported")
field_objects = self._get_field_objects(username, block_keys_to_state.keys())
for field_object in field_objects:
usage_key = field_object.module_state_key.map_into_course(field_object.course_id)
current_state = json.loads(field_object.state)
current_state.update(block_keys_to_state.pop(usage_key))
field_object.state = json.dumps(current_state)
field_object.save()
# We do a find_or_create for every block (rather than re-using field objects
# that were queried in get_many) so that if the score has
# been changed by some other piece of the code, we don't overwrite
# that score.
for usage_key, state in block_keys_to_state.items():
student_module, created = StudentModule.objects.get_or_create(
student=self.user,
......@@ -227,6 +236,39 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
# We just read this object, so we know that we can do an update
student_module.save(force_update=True)
@contract(
username="basestring",
block_keys="seq(UsageKey)|set(UsageKey)",
scope=ScopeBase,
fields="seq(basestring)|set(basestring)|None"
)
def get_mod_date_many(self, username, block_keys, scope=Scope.user_state, fields=None):
"""
Get the last modification date for fields from the specified blocks.
Arguments:
username: The name of the user whose state should be deleted
block_key (UsageKey): The UsageKey identifying which xblock modification dates to retrieve.
scope (Scope): The scope to retrieve from.
fields: A list of fields to query. If None, delete all stored fields.
Specific implementations are free to return the same modification date
for all fields, if they don't store changes individually per field.
Implementations may omit fields for which data has not been stored.
Yields: tuples of (block, field_name, modified_date) for each selected field.
"""
assert self.user.username == username
if scope != Scope.user_state:
raise ValueError("Only Scope.user_state is supported")
field_objects = self._get_field_objects(username, block_keys)
for field_object, usage_key in field_objects:
if field_object.state is None:
continue
for field in json.loads(field_object.state):
yield (usage_key, field, field_object.modified)
def get_history(self, username, block_key, scope=Scope.user_state):
"""We don't guarantee that history for many blocks will be fast."""
assert self.user.username == username
......
......@@ -147,7 +147,7 @@ class UserCourseStatus(views.APIView):
scope=Scope.user_state,
user_id=request.user.id,
block_scope_id=course.location,
field_name=None
field_name='position'
)
original_store_date = field_data_cache.last_modified(key)
if original_store_date is not None and modification_date < original_store_date:
......
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