256 lines
9.9 KiB
Python
256 lines
9.9 KiB
Python
"""
|
|
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()
|