Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XBV2 Prototype #755

Closed
wants to merge 4 commits into from
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
WIP - combine v1 and v2 together
bradenmacdonald committed Jun 1, 2024
commit 316bb505f69abf86f8d6328d6373591935616439
114 changes: 68 additions & 46 deletions xblock/core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Base classes for all XBlock-like objects. Used by all XBlock Runtimes.
"""
from contextlib import contextmanager
import copy
import functools
import inspect
@@ -189,14 +190,28 @@ def wrapper(self, request, suffix=''):
request_json = json.loads(request.body.decode('utf-8'))
except ValueError:
return JsonHandlerError(400, "Invalid JSON").get_response()
try:
response = func(self, request_json, suffix)
except JsonHandlerError as err:
return err.get_response()
if isinstance(response, Response):
return response
if isinstance(self, XBlock2Mixin):
# For XBlock v2 blocks, a json_handler is one of the only times where field edits are allowed.
with self._track_field_writes() as field_updates:
try:
response = func(self, request_json, suffix)
except JsonHandlerError as err:
return err.get_response(updated_fields=field_updates["updated_fields"])
else:
return Response(
json.dumps({"data": response, "updated_fields": field_updates["updated_fields"]}),
content_type='application/json',
charset='utf8',
)
else:
return Response(json.dumps(response), content_type='application/json', charset='utf8')
try:
response = func(self, request_json, suffix)
except JsonHandlerError as err:
return err.get_response()
if isinstance(response, Response):
return response
else:
return Response(json.dumps(response), content_type='application/json', charset='utf8')
return wrapper

@classmethod
@@ -932,31 +947,23 @@ def has_support(self, view, functionality):
return hasattr(view, "_supports") and functionality in view._supports # pylint: disable=protected-access


class XBlock2(Plugin):
class XBlock2Mixin:
"""
Base class for all v2 XBlocks, whether they are wrappers around a v1 XBlock,
or pure v2-only XBlocks. This ensures that `issubclass(block, XBlock2)` is
always useful.
Mixin with shared implementation for all v2 XBlocks, whether they are
wrappers around a v1 XBlock, or pure v2-only XBlocks.

Note: check if an XBlock is "v2" using `issubclass(block, XBlock2Mixin)`,
not `issubclass(block, XBlock2)`
"""
has_children: Final = False

entry_point = 'xblock.v2'

@classmethod
def load_class(cls, identifier, default=None, select=None, fallback_to_v1=False):
"""
Load a v2 XBlock, with optional fallback to v1
"""
try:
return super().load_class(identifier, default, select)
except PluginMissingError:
if fallback_to_v1:
return XBlock.load_class(identifier, default, select)
if default:
return default
raise

def __init__(self, runtime, scope_ids):
def __init__(
self,
runtime,
field_data=None,
scope_ids=UNSET,
for_parent=None,
**kwargs):
"""
Arguments:

@@ -968,14 +975,14 @@ def __init__(self, runtime, scope_ids):
"""
if self.has_children is not False:
raise ValueError('v2 XBlocks cannot declare has_children = True')
if hasattr(self, "student_view"):
raise AttributeError("v2 XBlocks must not declare a student_view() method.")

super().__init__(runtime=runtime, scope_ids=scope_ids)

if field_data is not None:
raise ValueError('v2 XBlocks do not allow the deprecated field_data init parameter.')

if for_parent is not None:
warnings.warn("Ignoring for_parent kwarg passed to a v2 XBlock init method", stacklevel=2)

# Prevent changes to _dirty_fields by forcing it to be a read-only dict. V2 XBlocks are expected to use the
# self.set_field[s]() API instead of writing to fields directly.
self._dirty_fields = MappingProxyType({})
super().__init__(runtime, scope_ids=scope_ids, **kwargs)

@final
def save(self):
@@ -986,24 +993,39 @@ def parent(self):
warnings.warn("Accessing .parent of v2 XBlocks is forbidden", stacklevel=2)
return None

@parent.setter
def parent(self, value):
if value is not None:
raise ValueError("v2 XBlocks cannot have a parent.")
warnings.warn("Accessing .parent of v2 XBlocks is forbidden", stacklevel=2)

@property
def _parent_block_id(self):
warnings.warn("Accessing ._parent_block_id of v2 XBlocks is forbidden", stacklevel=2)
return None


class XBlock2Mixin(XBlock2):
"""
Mixin that allows subclassing a v1 XBlock to become a v2 XBlock, allowing
both versions of the XBlock to be used, each with a different entry point.
"""

@final
def student_view(self, _context):
raise AttributeError("v2 XBlocks do not support student_view()")
@_parent_block_id.setter
def _parent_block_id(self, value):
if value is not None:
raise ValueError("v2 XBlocks cannot have a parent.")

@contextmanager
def _track_field_writes(self):
if not isinstance(self, XBlock2Mixin):
raise TypeError("track_field_writes() is only compatible with XBlock2 instances")
if hasattr(self, "_collect_field_writes"):
raise RuntimeError("Nested _track_field_writes calls detected")
print("Starting handler...")
field_updates = {"updated_fields": {"user": {}, "content": {}}}
self._collect_field_writes = field_updates["updated_fields"]
try:
yield field_updates
finally:
delattr(self, "_collect_field_writes")
print("Ending handler...")


class PureXBlock2(XBlock2, XBlock):
class XBlock2(XBlock2Mixin, XBlock):
"""
Base class for pure "v2" XBlocks, that don't need backwards compatibility with v1
"""
7 changes: 4 additions & 3 deletions xblock/runtime.py
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@

from web_fragments.fragment import Fragment

from xblock.core import XBlock, XBlockAside, XML_NAMESPACES
from xblock.core import XBlock, XBlockAside, XBlock2Mixin, XML_NAMESPACES
from xblock.fields import Field, BlockScope, Scope, ScopeIds, UserScope
from xblock.field_data import FieldData
from xblock.exceptions import (
@@ -1063,8 +1063,9 @@ def handle(self, block, handler_name, request, suffix=''):
else:
raise NoSuchHandlerError(f"Couldn't find handler {handler_name!r} for {block!r}")

# Write out dirty fields
block.save()
# Write out dirty fields (v1 XBlocks); for v2 this is handled by @json_handler
if not isinstance(block, XBlock2Mixin):
block.save()
return results

# Services