Add the candidate XBlockUserStateClient interface
This commit is contained in:
0
lms/djangoapps/xblock_user_state/__init__.py
Normal file
0
lms/djangoapps/xblock_user_state/__init__.py
Normal file
254
lms/djangoapps/xblock_user_state/interface.py
Normal file
254
lms/djangoapps/xblock_user_state/interface.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user