def setUp(self): super(TestDjangoUserStateClient, self).setUp() self.client = DjangoXBlockUserStateClient() self.users = defaultdict(UserFactory.create)
def __init__(self, user, course_id): self._cache = defaultdict(dict) self.course_id = course_id self.user = user self._client = DjangoXBlockUserStateClient(self.user)
def _build_student_data( cls, user_id, course_key, usage_key_str_list, filter_types=None, ): """ Generate a list of problem responses for all problem under the ``problem_location`` root. Arguments: user_id (int): The user id for the user generating the report course_key (CourseKey): The ``CourseKey`` for the course whose report is being generated usage_key_str_list (List[str]): The generated report will include these blocks and their child blocks. filter_types (List[str]): The report generator will only include data for block types in this list. Returns: Tuple[List[Dict], List[str]]: Yields a list of dictionaries containing the student data which will be included in the final csv, and the features/keys to include in that CSV. """ usage_keys = [ UsageKey.from_string(usage_key_str).map_into_course(course_key) for usage_key_str in usage_key_str_list ] user = get_user_model().objects.get(pk=user_id) max_count = settings.FEATURES.get('MAX_PROBLEM_RESPONSES_COUNT') store = modulestore() user_state_client = DjangoXBlockUserStateClient() student_data_keys = set() batch_no = 1 with store.bulk_operations(course_key): for usage_key in usage_keys: if max_count is not None and max_count <= 0: break course_blocks = get_course_blocks(user, usage_key) base_path = cls._build_block_base_path(store.get_item(usage_key)) for title, path, block_key in cls._build_problem_list(course_blocks, usage_key): # Chapter and sequential blocks are filtered out since they include state # which isn't useful for this report. if block_key.block_type in ('sequential', 'chapter'): continue if filter_types is not None and block_key.block_type not in filter_types: continue block = store.get_item(block_key) generated_report_data = defaultdict(list) # Blocks can implement the generate_report_data method to provide their own # human-readable formatting for user state. if hasattr(block, 'generate_report_data'): try: user_state_iterator = user_state_client.iter_all_for_block(block_key) for username, state in block.generate_report_data(user_state_iterator, max_count): generated_report_data[username].append(state) except NotImplementedError: pass responses = [] for response in list_problem_responses(course_key, block_key, max_count): response['title'] = title # A human-readable location for the current block response['location'] = ' > '.join(base_path + path) # A machine-friendly location for the current block response['block_key'] = str(block_key) # A block that has a single state per user can contain multiple responses # within the same state. user_states = generated_report_data.get(response['username'], []) if user_states: # For each response in the block, copy over the basic data like the # title, location, block_key and state, and add in the responses for user_state in user_states: user_response = response.copy() user_response.update(user_state) student_data_keys = student_data_keys.union(user_state.keys()) responses.append(user_response) else: responses.append(response) # Keep the keys in a useful order, starting with username, title and location, # then the columns returned by the xblock report generator in sorted order and # finally end with the more machine friendly block_key and state. student_data_keys_list = ( ['username', 'title', 'location'] + sorted(student_data_keys) + ['block_key', 'state'] ) yield responses, student_data_keys_list, batch_no batch_no += 1 if max_count is not None: max_count -= len(responses) if max_count <= 0: break
class UserStateCache(object): """ Cache for Scope.user_state xblock field data. """ def __init__(self, user, course_id): self._cache = defaultdict(dict) self.course_id = course_id self.user = user self._client = DjangoXBlockUserStateClient(self.user) def cache_fields(self, fields, xblocks, aside_types): # pylint: disable=unused-argument """ Load all fields specified by ``fields`` for the supplied ``xblocks`` and ``aside_types`` into this cache. Arguments: fields (list of str): Field names to cache. xblocks (list of :class:`XBlock`): XBlocks to cache fields for. aside_types (list of str): Aside types to cache fields for. """ block_field_state = self._client.get_many( self.user.username, _all_usage_keys(xblocks, aside_types), ) for user_state in block_field_state: self._cache[user_state.block_key] = user_state.state @contract(kvs_key=DjangoKeyValueStore.Key) def set(self, kvs_key, value): """ Set the specified `kvs_key` to the field value `value`. Arguments: kvs_key (`DjangoKeyValueStore.Key`): The field value to delete value: The field value to store """ self.set_many({kvs_key: value}) @contract(kvs_key=DjangoKeyValueStore.Key, returns="datetime|None") def last_modified(self, kvs_key): """ Return when the supplied field was changed. Arguments: kvs_key (`DjangoKeyValueStore.Key`): The key representing the cached field Returns: datetime if there was a modified date, or None otherwise """ try: return self._client.get( self.user.username, kvs_key.block_scope_id, fields=[kvs_key.field_name], ).updated except self._client.DoesNotExist: return None @contract(kv_dict="dict(DjangoKeyValueStore_Key: *)") def set_many(self, kv_dict): """ Set the specified fields to the supplied values. Arguments: kv_dict (dict): A dictionary mapping :class:`~DjangoKeyValueStore.Key` objects to values to set. """ pending_updates = defaultdict(dict) for kvs_key, value in kv_dict.items(): cache_key = self._cache_key_for_kvs_key(kvs_key) pending_updates[cache_key][kvs_key.field_name] = value try: self._client.set_many( self.user.username, pending_updates ) except DatabaseError: log.exception(u"Saving user state failed for %s", self.user.username) raise KeyValueMultiSaveError([]) # lint-amnesty, pylint: disable=raise-missing-from finally: self._cache.update(pending_updates) @contract(kvs_key=DjangoKeyValueStore.Key) def get(self, kvs_key): """ Return the django model object specified by `kvs_key` from the cache. Arguments: kvs_key (`DjangoKeyValueStore.Key`): The field value to delete Returns: A django orm object from the cache """ cache_key = self._cache_key_for_kvs_key(kvs_key) if cache_key not in self._cache: raise KeyError(kvs_key.field_name) return self._cache[cache_key][kvs_key.field_name] @contract(kvs_key=DjangoKeyValueStore.Key) def delete(self, kvs_key): """ Delete the value specified by `kvs_key`. Arguments: kvs_key (`DjangoKeyValueStore.Key`): The field value to delete Raises: KeyError if key isn't found in the cache """ cache_key = self._cache_key_for_kvs_key(kvs_key) if cache_key not in self._cache: raise KeyError(kvs_key.field_name) field_state = self._cache[cache_key] if kvs_key.field_name not in field_state: raise KeyError(kvs_key.field_name) self._client.delete(self.user.username, cache_key, fields=[kvs_key.field_name]) del field_state[kvs_key.field_name] @contract(kvs_key=DjangoKeyValueStore.Key, returns=bool) def has(self, kvs_key): """ Return whether the specified `kvs_key` is set. Arguments: kvs_key (`DjangoKeyValueStore.Key`): The field value to delete Returns: bool """ cache_key = self._cache_key_for_kvs_key(kvs_key) return ( cache_key in self._cache and kvs_key.field_name in self._cache[cache_key] ) def __len__(self): return len(self._cache) def _cache_key_for_kvs_key(self, key): """ Return the key used in this DjangoOrmFieldCache for the specified KeyValueStore key. Arguments: key (:class:`~DjangoKeyValueStore.Key`): The key representing the cached field """ return key.block_scope_id
def setUp(self): super(TestDjangoUserStateClient, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments self.client = DjangoXBlockUserStateClient() self.users = defaultdict(UserFactory.create)