Skip to content

Commit

Permalink
Deprecation of edx-user-state-client repo (#33218)
Browse files Browse the repository at this point in the history
* chore: add code for edx-user-state-client in edx-platform
  • Loading branch information
salman2013 authored Sep 14, 2023
1 parent 5290697 commit 648a302
Show file tree
Hide file tree
Showing 8 changed files with 901 additions and 26 deletions.
713 changes: 708 additions & 5 deletions lms/djangoapps/courseware/tests/test_user_state_client.py

Large diffs are not rendered by default.

193 changes: 192 additions & 1 deletion lms/djangoapps/courseware/user_state_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
from operator import attrgetter
from time import time

from abc import abstractmethod
from collections import namedtuple

from django.conf import settings
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.core.paginator import Paginator
from django.db import transaction
from django.db.utils import IntegrityError
from edx_django_utils import monitoring as monitoring_utils
from edx_user_state_client.interface import XBlockUserState, XBlockUserStateClient
from xblock.fields import Scope

from lms.djangoapps.courseware.models import BaseStudentModuleHistory, StudentModule
Expand All @@ -29,6 +31,195 @@
log = logging.getLogger(__name__)


class XBlockUserState(namedtuple('_XBlockUserState', ['username', 'block_key', 'state', 'updated', 'scope'])):
"""
The current state of a single XBlock.
Arguments:
username: The username of the user that stored this state.
block_key: The key identifying the scoped state. Depending on the :class:`~xblock.fields.BlockScope` of
``scope``, this may take one of several types:
* ``USAGE``: :class:`~opaque_keys.edx.keys.UsageKey`
* ``DEFINITION``: :class:`~opaque_keys.edx.keys.DefinitionKey`
* ``TYPE``: :class:`str`
* ``ALL``: ``None``
state: A dict mapping field names to the values of those fields for this XBlock.
updated: A :class:`datetime.datetime`. We guarantee that the fields
that were returned in "state" have not been changed since
this time (in UTC).
scope: A :class:`xblock.fields.Scope` identifying which XBlock scope this state is coming from.
"""
__slots__ = ()

def __repr__(self):
return "{}{!r}".format( # pylint: disable=consider-using-f-string
self.__class__.__name__,
tuple(self)
)


class XBlockUserStateClient():
"""
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.
"""

class ServiceUnavailable(Exception):
"""
This error is raised if the service backing this client is currently unavailable.
"""

class PermissionDenied(Exception):
"""
This error is raised if the caller is not allowed to access the requested data.
"""

class DoesNotExist(Exception):
"""
This error is raised if the caller has requested data that does not exist.
"""

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: The key 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:
XBlockUserState: The current state of the block for the specified username and block_key.
Raises:
DoesNotExist if no entry is found.
"""
try:
return next(self.get_many(username, [block_key], scope, fields=fields))
except StopIteration as exception:
raise self.DoesNotExist() from exception

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: The key 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)

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: The key 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)

@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: A list of keys 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 key in block_keys.
field_state is a dict mapping field names to values.
"""
raise NotImplementedError()

@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 keys 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()

@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: The key 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()

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.
"""
raise NotImplementedError()

def iter_all_for_block(self, block_key, scope=Scope.user_state):
"""
You get no ordering guarantees. 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):
"""
You get no ordering guarantees. If you're using this method, you should be running in an
async task.
"""
raise NotImplementedError()


class DjangoXBlockUserStateClient(XBlockUserStateClient):
"""
An interface that uses the Django ORM StudentModule as a backend.
Expand Down
4 changes: 0 additions & 4 deletions requirements/edx/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,6 @@ edx-opaque-keys[django]==2.5.0
# edx-milestones
# edx-organizations
# edx-proctoring
# edx-user-state-client
# edx-when
# lti-consumer-xblock
# openedx-events
Expand Down Expand Up @@ -550,8 +549,6 @@ edx-toggles==5.1.0
# ora2
edx-token-utils==0.2.1
# via -r requirements/edx/kernel.in
edx-user-state-client==1.3.2
# via -r requirements/edx/kernel.in
edx-when==2.4.0
# via
# -r requirements/edx/kernel.in
Expand Down Expand Up @@ -1205,7 +1202,6 @@ xblock[django]==1.7.0
# done-xblock
# edx-completion
# edx-sga
# edx-user-state-client
# edx-when
# lti-consumer-xblock
# ora2
Expand Down
6 changes: 0 additions & 6 deletions requirements/edx/development.txt
Original file line number Diff line number Diff line change
Expand Up @@ -788,7 +788,6 @@ edx-opaque-keys[django]==2.5.0
# edx-milestones
# edx-organizations
# edx-proctoring
# edx-user-state-client
# edx-when
# lti-consumer-xblock
# openedx-events
Expand Down Expand Up @@ -850,10 +849,6 @@ edx-token-utils==0.2.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
edx-user-state-client==1.3.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
edx-when==2.4.0
# via
# -r requirements/edx/doc.txt
Expand Down Expand Up @@ -2197,7 +2192,6 @@ xblock[django]==1.7.0
# done-xblock
# edx-completion
# edx-sga
# edx-user-state-client
# edx-when
# lti-consumer-xblock
# ora2
Expand Down
4 changes: 0 additions & 4 deletions requirements/edx/doc.txt
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,6 @@ edx-opaque-keys[django]==2.5.0
# edx-milestones
# edx-organizations
# edx-proctoring
# edx-user-state-client
# edx-when
# lti-consumer-xblock
# openedx-events
Expand Down Expand Up @@ -629,8 +628,6 @@ edx-toggles==5.1.0
# ora2
edx-token-utils==0.2.1
# via -r requirements/edx/base.txt
edx-user-state-client==1.3.2
# via -r requirements/edx/base.txt
edx-when==2.4.0
# via
# -r requirements/edx/base.txt
Expand Down Expand Up @@ -1471,7 +1468,6 @@ xblock[django]==1.7.0
# done-xblock
# edx-completion
# edx-sga
# edx-user-state-client
# edx-when
# lti-consumer-xblock
# ora2
Expand Down
1 change: 0 additions & 1 deletion requirements/edx/kernel.in
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ edx-search
edx-submissions
edx-toggles # Feature toggles management
edx-token-utils # Validate exam access tokens
edx-user-state-client
edx-when
edxval
event-tracking
Expand Down
4 changes: 0 additions & 4 deletions requirements/edx/testing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,6 @@ edx-opaque-keys[django]==2.5.0
# edx-milestones
# edx-organizations
# edx-proctoring
# edx-user-state-client
# edx-when
# lti-consumer-xblock
# openedx-events
Expand Down Expand Up @@ -662,8 +661,6 @@ edx-toggles==5.1.0
# ora2
edx-token-utils==0.2.1
# via -r requirements/edx/base.txt
edx-user-state-client==1.3.2
# via -r requirements/edx/base.txt
edx-when==2.4.0
# via
# -r requirements/edx/base.txt
Expand Down Expand Up @@ -1618,7 +1615,6 @@ xblock[django]==1.7.0
# done-xblock
# edx-completion
# edx-sga
# edx-user-state-client
# edx-when
# lti-consumer-xblock
# ora2
Expand Down
2 changes: 1 addition & 1 deletion xmodule/tests/test_capa_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from codejail.safe_exec import SafeExecException
from django.test import override_settings
from django.utils.encoding import smart_str
from edx_user_state_client.interface import XBlockUserState
from lms.djangoapps.courseware.user_state_client import XBlockUserState
from lxml import etree
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
from pytz import UTC
Expand Down

0 comments on commit 648a302

Please sign in to comment.