diff --git a/lms/djangoapps/xblock_user_state/__init__.py b/lms/djangoapps/xblock_user_state/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/xblock_user_state/interface.py b/lms/djangoapps/xblock_user_state/interface.py new file mode 100644 index 0000000000..5d001de4d1 --- /dev/null +++ b/lms/djangoapps/xblock_user_state/interface.py @@ -0,0 +1,254 @@ +""" +A baseclass for a generic client for accessing XBlock Scope.user_state field data. +""" + +from abc import abstractmethod + +from contracts import contract, new_contract, ContractsMeta +from opaque_keys.edx.keys import UsageKey +from xblock.fields import Scope, ScopeBase + +new_contract('UsageKey', UsageKey) + +class XBlockUserStateClient(object): + """ + First stab at an interface for accessing XBlock User State. This will have + use StudentModule as a backing store in the default case. + + Scope/Goals: + 1. Mediate access to all student-specific state stored by XBlocks. + a. This includes "preferences" and "user_info" (i.e. UserScope.ONE) + b. This includes XBlock Asides. + c. This may later include user_state_summary (i.e. UserScope.ALL). + d. This may include group state in the future. + e. This may include other key types + UserScope.ONE (e.g. Definition) + 2. Assume network service semantics. + At some point, this will probably be calling out to an external service. + Even if it doesn't, we want to be able to implement circuit breakers, so + that a failure in StudentModule doesn't bring down the whole site. + This also implies that the client is running as a user, and whatever is + backing it is smart enough to do authorization checks. + 3. This does not yet cover export-related functionality. + + Open Questions: + 1. Is it sufficient to just send the block_key in and extract course + + version info from it? + 2. Do we want to use the username as the identifier? Privacy implications? + Ease of debugging? + 3. Would a get_many_by_type() be useful? + """ + + __metaclass__ = ContractsMeta + + class ServiceUnavailable(Exception): + """ + This error is raised if the service backing this client is currently unavailable. + """ + pass + + class PermissionDenied(Exception): + """ + This error is raised if the caller is not allowed to access the requested data. + """ + pass + + class DoesNotExist(Exception): + """ + This error is raised if the caller has requested data that does not exist. + """ + pass + + @contract( + username="basestring", + block_key=UsageKey, + scope=ScopeBase, + fields="seq(basestring)|set(basestring)|None", + returns="dict(basestring: *)" + ) + def get(self, username, block_key, scope=Scope.user_state, fields=None): + """ + Retrieve the stored XBlock state for a single xblock usage. + + Arguments: + username: The name of the user whose state should be retrieved + block_key (UsageKey): The UsageKey identifying which xblock state to load. + scope (Scope): The scope to load data from + fields: A list of field values to retrieve. If None, retrieve all stored fields. + + Returns + dict: A dictionary mapping field names to values + """ + return next(self.get_many(username, [block_key], scope, fields=fields))[1] + + @contract( + username="basestring", + block_key=UsageKey, + state="dict(basestring: *)", + scope=ScopeBase, + returns=None, + ) + def set(self, username, block_key, state, scope=Scope.user_state): + """ + Set fields for a particular XBlock. + + Arguments: + username: The name of the user whose state should be retrieved + block_key (UsageKey): The UsageKey identifying which xblock state to load. + state (dict): A dictionary mapping field names to values + scope (Scope): The scope to store data to + """ + self.set_many(username, {block_key: state}, scope) + + @contract( + username="basestring", + block_key=UsageKey, + scope=ScopeBase, + fields="seq(basestring)|set(basestring)|None", + returns=None, + ) + def delete(self, username, block_key, scope=Scope.user_state, fields=None): + """ + Delete the stored XBlock state for a single xblock usage. + + Arguments: + username: The name of the user whose state should be deleted + block_key (UsageKey): The UsageKey identifying which xblock state to delete. + scope (Scope): The scope to delete data from + fields: A list of fields to delete. If None, delete all stored fields. + """ + return self.delete_many(username, [block_key], scope, fields=fields) + + @contract( + username="basestring", + block_key=UsageKey, + scope=ScopeBase, + fields="seq(basestring)|set(basestring)|None", + returns="dict(basestring: datetime)", + ) + 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 queried + 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, query all 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)", + scope=ScopeBase, + fields="seq(basestring)|set(basestring)|None", + ) + @abstractmethod + def get_many(self, username, block_keys, scope=Scope.user_state, fields=None): + """ + Retrieve the stored XBlock state for a single xblock usage. + + 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: + (UsageKey, field_state) tuples for each specified UsageKey in block_keys. + field_state is a dict mapping field names to values. + """ + raise NotImplementedError() + + @contract( + username="basestring", + block_keys_to_state="dict(UsageKey: dict(basestring: *))", + scope=ScopeBase, + returns=None, + ) + @abstractmethod + def set_many(self, username, block_keys_to_state, scope=Scope.user_state): + """ + Set fields for a particular XBlock. + + Arguments: + username: The name of the user whose state should be retrieved + block_keys_to_state (dict): A dict mapping UsageKeys to state dicts. + Each state dict maps field names to values. These state dicts + are overlaid over the stored state. To delete fields, use + :meth:`delete` or :meth:`delete_many`. + scope (Scope): The scope to load data from + """ + raise NotImplementedError() + + @contract( + username="basestring", + block_keys="seq(UsageKey)|set(UsageKey)", + scope=ScopeBase, + fields="seq(basestring)|set(basestring)|None", + returns=None, + ) + @abstractmethod + def delete_many(self, username, block_keys, scope=Scope.user_state, fields=None): + """ + Delete the stored XBlock state for a many xblock usages. + + Arguments: + username: The name of the user whose state should be deleted + block_key (UsageKey): The UsageKey identifying which xblock state to delete. + scope (Scope): The scope to delete data from + fields: A list of fields to delete. If None, delete all stored fields. + """ + raise NotImplementedError() + + @contract( + username="basestring", + block_keys="seq(UsageKey)|set(UsageKey)", + scope=ScopeBase, + fields="seq(basestring)|set(basestring)|None", + ) + @abstractmethod + 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 queried + 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. + """ + raise NotImplementedError() + + def get_history(self, username, block_key, scope=Scope.user_state): + """We don't guarantee that history for many blocks will be fast.""" + raise NotImplementedError() + + def iter_all_for_block(self, block_key, scope=Scope.user_state, batch_size=None): + """ + You get no ordering guarantees. Fetching will happen in batch_size + increments. If you're using this method, you should be running in an + async task. + """ + raise NotImplementedError() + + def iter_all_for_course(self, course_key, block_type=None, scope=Scope.user_state, batch_size=None): + """ + You get no ordering guarantees. Fetching will happen in batch_size + increments. If you're using this method, you should be running in an + async task. + """ + raise NotImplementedError()