def iter_all_for_block(block_key, scope=Scope.user_state): """ Return an iterator over the data stored in the block (e.g. a problem block). You get no ordering guarantees.If you're using this method, you should be running in an async task. Arguments: block_key: an XBlock's locator (e.g. :class:`~BlockUsageLocator`) scope (Scope): must be `Scope.user_state` Returns: an iterator over all data. Each invocation returns the next :class:`~XBlockUserState` object, which includes the block's contents. """ if scope != Scope.user_state: raise ValueError("Only Scope.user_state is supported") results = StudentModule.objects.order_by('id').filter( module_state_key=block_key) p = Paginator(results, settings.USER_STATE_BATCH_SIZE) for page_number in p.page_range: page = p.page(page_number) for sm in page.object_list: state = json.loads(sm.state) if state == {}: continue try: yield XBlockUserState(sm.student.username, sm.module_state_key, state, sm.modified, scope) except User.DoesNotExist: pass
def get_many(self, username, block_keys, scope=Scope.user_state, fields=None): """ Retrieve the stored XBlock state for the specified XBlock usages. Arguments: username: The name of the user whose state should be retrieved block_keys ([UsageKey]): A list of UsageKeys identifying which xblock states to load. scope (Scope): The scope to load data from fields: A list of field values to retrieve. If None, retrieve all stored fields. Yields: XBlockUserState tuples for each specified UsageKey in block_keys. field_state is a dict mapping field names to values. """ if scope != Scope.user_state: raise ValueError( "Only Scope.user_state is supported, not {}".format(scope)) block_count = state_length = 0 evt_time = time() self._ddog_histogram(evt_time, 'get_many.blks_requested', len(block_keys)) modules = self._get_student_modules(username, block_keys) for module, usage_key in modules: if module.state is None: self._ddog_increment(evt_time, 'get_many.empty_state') continue state = json.loads(module.state) state_length += len(module.state) self._ddog_histogram(evt_time, 'get_many.block_size', len(module.state)) # If the state is the empty dict, then it has been deleted, and so # conformant UserStateClients should treat it as if it doesn't exist. if state == {}: continue if fields is not None: state = { field: state[field] for field in fields if field in state } block_count += 1 yield XBlockUserState(username, usage_key, state, module.modified, scope) # The rest of this method exists only to submit DataDog events. # Remove it once we're no longer interested in the data. finish_time = time() self._ddog_histogram(evt_time, 'get_many.blks_out', block_count) self._ddog_histogram(evt_time, 'get_many.response_time', (finish_time - evt_time) * 1000)
def iter_all_for_course(self, course_key, block_type=None, scope=Scope.user_state): """ Return an iterator over all data stored in a course's blocks. You get no ordering guarantees. If you're using this method, you should be running in an async task. Arguments: course_key: a course locator scope (Scope): must be `Scope.user_state` Returns: an iterator over all data. Each invocation returns the next :class:`~XBlockUserState` object, which includes the block's contents. """ if scope != Scope.user_state: raise ValueError("Only Scope.user_state is supported") results = StudentModule.objects.order_by('id').filter(course_id=course_key) if block_type: results = results.filter(module_type=block_type) p = Paginator(results, settings.USER_STATE_BATCH_SIZE) for page_number in p.page_range: page = p.page(page_number) for sm in page.object_list: state = json.loads(sm.state) if state == {}: continue yield XBlockUserState(sm.student.username, sm.module_state_key, state, sm.modified, scope)
def get_many(self, username, block_keys, scope=Scope.user_state, fields=None): """ Retrieve the stored XBlock state for the specified XBlock usages. Arguments: username: The name of the user whose state should be retrieved block_keys ([UsageKey]): A list of UsageKeys identifying which xblock states to load. scope (Scope): The scope to load data from fields: A list of field values to retrieve. If None, retrieve all stored fields. Yields: XBlockUserState tuples for each specified UsageKey in block_keys. field_state is a dict mapping field names to values. """ if scope != Scope.user_state: raise ValueError(u"Only Scope.user_state is supported, not {}".format(scope)) total_block_count = 0 evt_time = time() # count how many times this function gets called self._nr_stat_increment('get_many', 'calls') # keep track of blocks requested self._nr_stat_accumulate('get_many', 'blocks_requested', len(block_keys)) modules = self._get_student_modules(username, block_keys) for module, usage_key in modules: if module.state is None: continue state = json.loads(module.state) state_length = len(module.state) # If the state is the empty dict, then it has been deleted, and so # conformant UserStateClients should treat it as if it doesn't exist. if state == {}: continue # collect statistics for metric reporting self._nr_block_stat_increment('get_many', usage_key.block_type, 'blocks_out') self._nr_block_stat_accumulate('get_many', usage_key.block_type, 'size', state_length) total_block_count += 1 # filter state on fields if fields is not None: state = { field: state[field] for field in fields if field in state } yield XBlockUserState(username, usage_key, state, module.modified, scope) # The rest of this method exists only to report metrics. finish_time = time() duration = (finish_time - evt_time) * 1000 # milliseconds self._nr_stat_accumulate('get_many', 'duration', duration)
def _add_state(self, username, block_key, scope, state): """ Add the specified state to the state history of this block. """ history_list = self._history.setdefault((username, block_key, scope), []) history_list.insert( 0, XBlockUserState(username, block_key, state, datetime.now(pytz.utc), scope))
def get_history(self, username, block_key, scope=Scope.user_state): """ Retrieve history of state changes for a given block for a given student. We don't guarantee that history for many blocks will be fast. If the specified block doesn't exist, raise :class:`~DoesNotExist`. Arguments: username: The name of the user whose history should be retrieved. block_key: The key identifying which xblock history to retrieve. scope (Scope): The scope to load data from. Yields: XBlockUserState entries for each modification to the specified XBlock, from latest to earliest. """ if scope != Scope.user_state: raise ValueError("Only Scope.user_state is supported") student_modules = list( student_module for student_module, usage_id in self._get_student_modules(username, [block_key]) ) if len(student_modules) == 0: raise self.DoesNotExist() history_entries = StudentModuleHistory.objects.prefetch_related('student_module').filter( student_module__in=student_modules ).order_by('-id') # If no history records exist, raise an error if not history_entries: raise self.DoesNotExist() for history_entry in history_entries: state = history_entry.state # If the state is serialized json, then load it if state is not None: state = json.loads(state) # If the state is empty, then for the purposes of `get_history`, it has been # deleted, and so we list that entry as `None`. if state == {}: state = None block_key = history_entry.student_module.module_state_key block_key = block_key.map_into_course( history_entry.student_module.course_id ) yield XBlockUserState(username, block_key, state, history_entry.created, scope)