Commit 4182f874 by Calen Pennington

Add the candidate XBlockUserStateClient interface

parent 4312c0e7
"""
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()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment