class TestMissingStudentModule(TestCase): # lint-amnesty, pylint: disable=missing-class-docstring # Tell Django to clean out all databases, not just default databases = {alias for alias in connections} # lint-amnesty, pylint: disable=unnecessary-comprehension def setUp(self): super(TestMissingStudentModule, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments self.user = UserFactory.create(username='******') assert self.user.id == 1 # check our assumption hard-coded in the key functions above. # The descriptor has no fields, so FDC shouldn't send any queries with self.assertNumQueries(0): self.field_data_cache = FieldDataCache([mock_descriptor()], course_id, self.user) self.kvs = DjangoKeyValueStore(self.field_data_cache) def test_get_field_from_missing_student_module(self): "Test that getting a field from a missing StudentModule raises a KeyError" with self.assertNumQueries(0): self.assertRaises(KeyError, self.kvs.get, user_state_key('a_field')) def test_set_field_in_missing_student_module(self): "Test that setting a field in a missing StudentModule creates the student module" assert 0 == len(self.field_data_cache) assert 0 == StudentModule.objects.all().count() # 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). # 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') assert 1 == sum( (len(cache) for cache in self.field_data_cache.cache.values())) assert 1 == StudentModule.objects.all().count() student_module = StudentModule.objects.all()[0] assert {'a_field': 'a_value'} == json.loads(student_module.state) assert self.user == student_module.student assert location('usage_id').replace( run=None) == student_module.module_state_key assert course_id == student_module.course_id def test_delete_field_from_missing_student_module(self): "Test that deleting a field from a missing StudentModule raises a KeyError" with self.assertNumQueries(0): self.assertRaises(KeyError, self.kvs.delete, user_state_key('a_field')) def test_has_field_for_missing_student_module(self): "Test that `has` returns False for missing StudentModules" with self.assertNumQueries(0): assert not self.kvs.has(user_state_key('a_field'))
def setUp(self): super(TestMissingStudentModule, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments self.user = UserFactory.create(username='******') self.assertEqual(self.user.id, 1) # check our assumption hard-coded in the key functions above. # The descriptor has no fields, so FDC shouldn't send any queries with self.assertNumQueries(0): self.field_data_cache = FieldDataCache([mock_descriptor()], course_id, self.user) self.kvs = DjangoKeyValueStore(self.field_data_cache)
def setUp(self): super().setUp() self.user = UserFactory.create(username='******') assert self.user.id == 1 # check our assumption hard-coded in the key functions above. # The descriptor has no fields, so FDC shouldn't send any queries with self.assertNumQueries(0): self.field_data_cache = FieldDataCache([mock_descriptor()], course_id, self.user) self.kvs = DjangoKeyValueStore(self.field_data_cache)
def setUp(self): super(TestStudentModuleStorage, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments student_module = StudentModuleFactory(state=json.dumps({'a_field': 'a_value', 'b_field': 'b_value'})) self.user = student_module.student self.assertEqual(self.user.id, 1) # check our assumption hard-coded in the key functions above. # There should be only one query to load a single descriptor with a single user_state field with self.assertNumQueries(1): self.field_data_cache = FieldDataCache( [mock_descriptor([mock_field(Scope.user_state, 'a_field')])], course_id, self.user ) self.kvs = DjangoKeyValueStore(self.field_data_cache)
def setUp(self): field_storage = self.factory.create() if hasattr(field_storage, 'student'): self.user = field_storage.student else: self.user = UserFactory.create() self.mock_descriptor = mock_descriptor([ mock_field(self.scope, 'existing_field'), mock_field(self.scope, 'other_existing_field')]) # Each field is stored as a separate row in the table, # but we can query them in a single query with self.assertNumQueries(1): self.field_data_cache = FieldDataCache([self.mock_descriptor], course_id, self.user) self.kvs = DjangoKeyValueStore(self.field_data_cache)
def setUp(self): super(TestInvalidScopes, self).setUp() self.user = UserFactory.create(username='******') self.field_data_cache = FieldDataCache( [mock_descriptor([mock_field(Scope.user_state, 'a_field')])], course_id, self.user) self.kvs = DjangoKeyValueStore(self.field_data_cache)
def setUp(self): super().setUp() self.user = UserFactory.create(username='******') self.field_data_cache = FieldDataCache( [mock_descriptor([mock_field(Scope.user_state, 'a_field')])], COURSE_KEY, self.user, ) self.kvs = DjangoKeyValueStore(self.field_data_cache)
def setUp(self): super().setUp() student_module = StudentModuleFactory( state=json.dumps({ 'a_field': 'a_value', 'b_field': 'b_value' })) self.user = student_module.student assert self.user.id == 1 # check our assumption hard-coded in the key functions above. # There should be only one query to load a single descriptor with a single user_state field with self.assertNumQueries(1): self.field_data_cache = FieldDataCache( [mock_descriptor([mock_field(Scope.user_state, 'a_field')])], COURSE_KEY, self.user, ) self.kvs = DjangoKeyValueStore(self.field_data_cache)
def test_invalid_scopes(self): for scope in (Scope(user=True, block=BlockScope.DEFINITION), Scope(user=False, block=BlockScope.TYPE), Scope(user=False, block=BlockScope.ALL)): key = DjangoKeyValueStore.Key(scope, None, None, 'field') self.assertRaises(InvalidScopeError, self.kvs.get, key) self.assertRaises(InvalidScopeError, self.kvs.set, key, 'value') self.assertRaises(InvalidScopeError, self.kvs.delete, key) self.assertRaises(InvalidScopeError, self.kvs.has, key) self.assertRaises(InvalidScopeError, self.kvs.set_many, {key: 'value'})
def _get_module_instance_for_task(course_id, student, module_descriptor, xmodule_instance_args=None, grade_bucket_type=None, course=None): """ Fetches a StudentModule instance for a given `course_id`, `student` object, and `module_descriptor`. `xmodule_instance_args` is used to provide information for creating a track function and an XQueue callback. These are passed, along with `grade_bucket_type`, to get_module_for_descriptor_internal, which sidesteps the need for a Request object when instantiating an xmodule instance. """ # reconstitute the problem's corresponding XModule: field_data_cache = FieldDataCache.cache_for_descriptor_descendents( course_id, student, module_descriptor) student_data = KvsFieldData(DjangoKeyValueStore(field_data_cache)) # get request-related tracking information from args passthrough, and supplement with task-specific # information: request_info = xmodule_instance_args.get( 'request_info', {}) if xmodule_instance_args is not None else {} task_info = { "student": student.username, "task_id": _get_task_id_from_xmodule_args(xmodule_instance_args) } def make_track_function(): ''' Make a tracking function that logs what happened. For insertion into ModuleSystem, and used by CapaModule, which will provide the event_type (as string) and event (as dict) as arguments. The request_info and task_info (and page) are provided here. ''' return lambda event_type, event: task_track( request_info, task_info, event_type, event, page='x_module_task') xqueue_callback_url_prefix = xmodule_instance_args.get('xqueue_callback_url_prefix', '') \ if xmodule_instance_args is not None else '' return get_module_for_descriptor_internal( user=student, descriptor=module_descriptor, student_data=student_data, course_id=course_id, track_function=make_track_function(), xqueue_callback_url_prefix=xqueue_callback_url_prefix, grade_bucket_type=grade_bucket_type, # This module isn't being used for front-end rendering request_token=None, # pass in a loaded course for override enabling course=course)
def _init_field_data_for_block(self, block): """ Initialize the FieldData implementation for the specified XBlock """ if self.user is None: # No user is specified, so we want to throw an error if anything attempts to read/write user-specific fields student_data_store = None elif self.user.is_anonymous: # The user is anonymous. Future work will support saving their state # in a cache or the django session but for now just use a highly # ephemeral dict. student_data_store = KvsFieldData(kvs=DictKeyValueStore()) elif self.system.student_data_mode == XBlockRuntimeSystem.STUDENT_DATA_EPHEMERAL: # We're in an environment like Studio where we want to let the # author test blocks out but not permanently save their state. # This in-memory dict will typically only persist for one # request-response cycle, so we need to soon replace it with a store # that puts the state into a cache or the django session. student_data_store = KvsFieldData(kvs=DictKeyValueStore()) else: # Use database-backed field data (i.e. store user_state in StudentModule) context_key = block.scope_ids.usage_id.context_key if context_key not in self.django_field_data_caches: field_data_cache = FieldDataCache( [block], course_id=context_key, user=self.user, asides=None, read_only=False, ) self.django_field_data_caches[context_key] = field_data_cache else: field_data_cache = self.django_field_data_caches[context_key] field_data_cache.add_descriptors_to_cache([block]) student_data_store = KvsFieldData( kvs=DjangoKeyValueStore(field_data_cache)) return SplitFieldData({ Scope.content: self.system.authored_data_store, Scope.settings: self.system.authored_data_store, Scope.parent: self.system.authored_data_store, Scope.children: self.system.authored_data_store, Scope.user_state_summary: student_data_store, Scope.user_state: student_data_store, Scope.user_info: student_data_store, Scope.preferences: student_data_store, })
def _init_field_data_for_block(self, block): """ Initialize the FieldData implementation for the specified XBlock """ if self.user is None: # No user is specified, so we want to throw an error if anything attempts to read/write user-specific fields student_data_store = None elif self.user.is_anonymous: # This is an anonymous (non-registered) user: assert self.user_id.startswith("anon") kvs = EphemeralKeyValueStore() student_data_store = KvsFieldData(kvs) elif self.system.student_data_mode == XBlockRuntimeSystem.STUDENT_DATA_EPHEMERAL: # We're in an environment like Studio where we want to let the # author test blocks out but not permanently save their state. kvs = EphemeralKeyValueStore() student_data_store = KvsFieldData(kvs) else: # Use database-backed field data (i.e. store user_state in StudentModule) context_key = block.scope_ids.usage_id.context_key if context_key not in self.django_field_data_caches: field_data_cache = FieldDataCache( [block], course_id=context_key, user=self.user, asides=None, read_only=False, ) self.django_field_data_caches[context_key] = field_data_cache else: field_data_cache = self.django_field_data_caches[context_key] field_data_cache.add_descriptors_to_cache([block]) student_data_store = KvsFieldData( kvs=DjangoKeyValueStore(field_data_cache)) return SplitFieldData({ Scope.content: self.system.authored_data_store, Scope.settings: self.system.authored_data_store, Scope.parent: self.system.authored_data_store, Scope.children: self.system.children_data_store, Scope.user_state_summary: student_data_store, Scope.user_state: student_data_store, Scope.user_info: student_data_store, Scope.preferences: student_data_store, })
class StorageTestBase: """ A base class for that gets subclassed when testing each of the scopes. """ # Disable pylint warnings that arise because of the way the child classes call # this base class -- pylint's static analysis can't keep up with it. # pylint: disable=no-member, not-callable factory = None scope = None key_factory = None storage_class = None def setUp(self): field_storage = self.factory.create() if hasattr(field_storage, 'student'): self.user = field_storage.student else: self.user = UserFactory.create() self.mock_descriptor = mock_descriptor([ mock_field(self.scope, 'existing_field'), mock_field(self.scope, 'other_existing_field') ]) # Each field is stored as a separate row in the table, # but we can query them in a single query with self.assertNumQueries(1): self.field_data_cache = FieldDataCache( [self.mock_descriptor], COURSE_KEY, self.user, ) self.kvs = DjangoKeyValueStore(self.field_data_cache) def test_set_and_get_existing_field(self): with self.assertNumQueries(1): self.kvs.set(self.key_factory('existing_field'), 'test_value') with self.assertNumQueries(0): assert 'test_value' == self.kvs.get( self.key_factory('existing_field')) def test_get_existing_field(self): "Test that getting an existing field in an existing Storage Field works" with self.assertNumQueries(0): assert 'old_value' == self.kvs.get( self.key_factory('existing_field')) def test_get_missing_field(self): "Test that getting a missing field from an existing Storage Field raises a KeyError" with self.assertNumQueries(0): self.assertRaises(KeyError, self.kvs.get, self.key_factory('missing_field')) def test_set_existing_field(self): "Test that setting an existing field changes the value" with self.assertNumQueries(1): self.kvs.set(self.key_factory('existing_field'), 'new_value') assert 1 == self.storage_class.objects.all().count() assert '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(1): self.kvs.set(self.key_factory('missing_field'), 'new_value') assert 2 == self.storage_class.objects.all().count() assert 'old_value' == json.loads( self.storage_class.objects.get(field_name='existing_field').value) assert 'new_value' == json.loads( self.storage_class.objects.get(field_name='missing_field').value) def test_delete_existing_field(self): "Test that deleting an existing field removes it" with self.assertNumQueries(1): self.kvs.delete(self.key_factory('existing_field')) assert 0 == self.storage_class.objects.all().count() def test_delete_missing_field(self): "Test that deleting a missing field from an existing Storage Field raises a KeyError" with self.assertNumQueries(0): self.assertRaises(KeyError, self.kvs.delete, self.key_factory('missing_field')) assert 1 == self.storage_class.objects.all().count() def test_has_existing_field(self): "Test that `has` returns True for an existing Storage Field" with self.assertNumQueries(0): assert self.kvs.has(self.key_factory('existing_field')) def test_has_missing_field(self): "Test that `has` return False for an existing Storage Field" with self.assertNumQueries(0): assert not self.kvs.has(self.key_factory('missing_field')) def construct_kv_dict(self): """Construct a kv_dict that can be passed to set_many""" key1 = self.key_factory('existing_field') key2 = self.key_factory('other_existing_field') new_value = 'new value' newer_value = 'newer value' return {key1: new_value, key2: newer_value} def test_set_many(self): """Test that setting many regular fields at the same time works""" kv_dict = self.construct_kv_dict() # Each field is a separate row in the database, hence # a separate query with self.assertNumQueries(len(kv_dict)): self.kvs.set_many(kv_dict) for key in kv_dict: assert self.kvs.get(key) == kv_dict[key] def test_set_many_failure(self): """Test that setting many regular fields with a DB error """ kv_dict = self.construct_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]): with pytest.raises(KeyValueMultiSaveError) as exception_context: self.kvs.set_many(kv_dict) exception = exception_context.value assert exception.saved_field_names == [ 'existing_field', 'other_existing_field' ]
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 databases = {alias for alias in connections} # lint-amnesty, pylint: disable=unnecessary-comprehension def setUp(self): super().setUp() student_module = StudentModuleFactory( state=json.dumps({ 'a_field': 'a_value', 'b_field': 'b_value' })) self.user = student_module.student assert self.user.id == 1 # check our assumption hard-coded in the key functions above. # There should be only one query to load a single descriptor with a single user_state field with self.assertNumQueries(1): self.field_data_cache = FieldDataCache( [mock_descriptor([mock_field(Scope.user_state, 'a_field')])], COURSE_KEY, self.user, ) self.kvs = DjangoKeyValueStore(self.field_data_cache) def test_get_existing_field(self): "Test that getting an existing field in an existing StudentModule works" # This should only read from the cache, not the database with self.assertNumQueries(0): assert 'a_value' == self.kvs.get(user_state_key('a_field')) def test_get_missing_field(self): "Test that getting a missing field from an existing StudentModule raises a KeyError" # This should only read from the cache, not the database with self.assertNumQueries(0): self.assertRaises(KeyError, self.kvs.get, user_state_key('not_a_field')) 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. 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(4, using='default'): with self.assertNumQueries(1, using='student_module_history'): self.kvs.set(user_state_key('a_field'), 'new_value') assert 1 == StudentModule.objects.all().count() assert { 'b_field': 'b_value', 'a_field': 'new_value' } == json.loads(StudentModule.objects.all()[0].state) # lint-amnesty, pylint: disable=line-too-long 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. 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(4, using='default'): with self.assertNumQueries(1, using='student_module_history'): self.kvs.set(user_state_key('not_a_field'), 'new_value') assert 1 == StudentModule.objects.all().count() assert { 'b_field': 'b_value', 'a_field': 'a_value', 'not_a_field': 'new_value' } == json.loads(StudentModule.objects.all()[0].state) # lint-amnesty, pylint: disable=line-too-long 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. 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'): with self.assertNumQueries(1, using='student_module_history'): self.kvs.delete(user_state_key('a_field')) assert 1 == StudentModule.objects.all().count() self.assertRaises(KeyError, self.kvs.get, user_state_key('not_a_field')) def test_delete_missing_field(self): "Test that deleting a missing field from an existing StudentModule raises a KeyError" with self.assertNumQueries(0): self.assertRaises(KeyError, self.kvs.delete, user_state_key('not_a_field')) assert 1 == StudentModule.objects.all().count() assert { 'b_field': 'b_value', 'a_field': 'a_value' } == json.loads(StudentModule.objects.all()[0].state) def test_has_existing_field(self): "Test that `has` returns True for existing fields in StudentModules" with self.assertNumQueries(0): assert self.kvs.has(user_state_key('a_field')) def test_has_missing_field(self): "Test that `has` returns False for missing fields in StudentModule" with self.assertNumQueries(0): assert not self.kvs.has(user_state_key('not_a_field')) def construct_kv_dict(self): """Construct a kv_dict that can be passed to set_many""" key1 = user_state_key('field_a') key2 = user_state_key('field_b') new_value = 'new value' newer_value = 'newer value' return {key1: new_value, key2: newer_value} def test_set_many(self): "Test setting many fields that are scoped to Scope.user_state" kv_dict = self.construct_kv_dict() # 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(4, using="default"): with self.assertNumQueries(1, using="student_module_history"): self.kvs.set_many(kv_dict) for key in kv_dict: assert self.kvs.get(key) == kv_dict[key] def test_set_many_failure(self): "Test failures when setting many fields that are scoped to Scope.user_state" kv_dict = self.construct_kv_dict() # because we're patching the underlying save, we need to ensure the # fields are in the cache for key in kv_dict: self.kvs.set(key, 'test_value') with patch('django.db.models.Model.save', side_effect=DatabaseError): with pytest.raises(KeyValueMultiSaveError) as exception_context: self.kvs.set_many(kv_dict) assert exception_context.value.saved_field_names == []
def setUp(self): super(TestInvalidScopes, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments self.user = UserFactory.create(username='******') self.field_data_cache = FieldDataCache([mock_descriptor([mock_field(Scope.user_state, 'a_field')])], course_id, self.user) # lint-amnesty, pylint: disable=line-too-long self.kvs = DjangoKeyValueStore(self.field_data_cache)
class TestMissingStudentModule(TestCase): # Tell Django to clean out all databases, not just default multi_db = True def setUp(self): super(TestMissingStudentModule, self).setUp() self.user = UserFactory.create(username='******') self.assertEqual( self.user.id, 1) # check our assumption hard-coded in the key functions above. # The descriptor has no fields, so FDC shouldn't send any queries with self.assertNumQueries(0): self.field_data_cache = FieldDataCache([mock_descriptor()], course_id, self.user) self.kvs = DjangoKeyValueStore(self.field_data_cache) def test_get_field_from_missing_student_module(self): "Test that getting a field from a missing StudentModule raises a KeyError" with self.assertNumQueries(0): self.assertRaises(KeyError, self.kvs.get, user_state_key('a_field')) def test_set_field_in_missing_student_module(self): "Test that setting a field in a missing StudentModule creates the student module" 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_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). # 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') self.assertEquals( 1, sum(len(cache) for cache in self.field_data_cache.cache.values())) self.assertEquals(1, StudentModule.objects.all().count()) student_module = StudentModule.objects.all()[0] self.assertEquals({'a_field': 'a_value'}, json.loads(student_module.state)) self.assertEquals(self.user, student_module.student) self.assertEquals( location('usage_id').replace(run=None), student_module.module_state_key) self.assertEquals(course_id, student_module.course_id) def test_delete_field_from_missing_student_module(self): "Test that deleting a field from a missing StudentModule raises a KeyError" with self.assertNumQueries(0): self.assertRaises(KeyError, self.kvs.delete, user_state_key('a_field')) def test_has_field_for_missing_student_module(self): "Test that `has` returns False for missing StudentModules" with self.assertNumQueries(0): self.assertFalse(self.kvs.has(user_state_key('a_field')))