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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
133 changes: 124 additions & 9 deletions xblock/core.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
"""
Base classes for all XBlock-like objects. Used by all XBlock Runtimes.
"""
from contextlib import contextmanager
import copy
import functools
import inspect
import json
import logging
import os
from types import MappingProxyType
from typing import Final, final
import warnings
from collections import OrderedDict, defaultdict

Expand All @@ -21,9 +24,9 @@
KeyValueMultiSaveError,
XBlockSaveError,
)
from xblock.fields import Field, List, Reference, ReferenceList, Scope, String
from xblock.fields import Field, List, Reference, ReferenceList, Scope, String, UserScope
from xblock.internal import class_lazy
from xblock.plugin import Plugin
from xblock.plugin import Plugin, PluginMissingError
from xblock.validation import Validation

# OrderedDict is used so that namespace attributes are put in predictable order
Expand Down Expand Up @@ -187,14 +190,33 @@
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.
field_updates = {"updated_fields": {"user": {}, "content": {}}}
try:
with self._track_field_writes(field_updates):
response = func(self, request_json, suffix)
except JsonHandlerError as err:
return err.get_response(updated_fields=field_updates["updated_fields"])

Check warning on line 200 in xblock/core.py

View check run for this annotation

Codecov / codecov/patch

xblock/core.py#L195-L200

Added lines #L195 - L200 were not covered by tests
else:
if response is None:
response = {}
elif not isinstance(response, dict):
raise TypeError("json_handler functions must return a dict")
return Response(

Check warning on line 206 in xblock/core.py

View check run for this annotation

Codecov / codecov/patch

xblock/core.py#L202-L206

Added lines #L202 - L206 were not covered by tests
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
Expand Down Expand Up @@ -930,6 +952,99 @@
return hasattr(view, "_supports") and functionality in view._supports # pylint: disable=protected-access


class XBlock2Mixin:
"""
Mixin with shared implementation for all v2 XBlocks, whether they are
keeping backwards compatibility with v1 or not.

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

def __init__(self, *args, **kwargs):
"""
Validation during init
"""
super().__init__(*args, **kwargs)
if self.has_children is not False:
raise ValueError('v2 XBlocks cannot declare has_children = True')

Check warning on line 971 in xblock/core.py

View check run for this annotation

Codecov / codecov/patch

xblock/core.py#L969-L971

Added lines #L969 - L971 were not covered by tests

@contextmanager
def _track_field_writes(self, field_updates):
if not isinstance(self, XBlock2Mixin):
raise TypeError("track_field_writes() is only compatible with XBlock2 instances")
if self._dirty_fields:
raise ValueError("Found dirty fields before handler even started - shouldn't happen")
print("Starting handler...")
try:
yield
for field in self._dirty_fields.keys():
scope_type = "user" if field.scope.user != UserScope.NONE else "content"
field_updates["updated_fields"][scope_type][field.name] = field.to_json(getattr(self, field.name))
print("success, dirty fields: ", self._dirty_fields)
print("success, dirty fields: ", field_updates["updated_fields"])
print(f"{self}")
self.force_save_fields([field.name for field in self._dirty_fields.keys()])
self.runtime.save_block(self)

Check warning on line 989 in xblock/core.py

View check run for this annotation

Codecov / codecov/patch

xblock/core.py#L975-L989

Added lines #L975 - L989 were not covered by tests
finally:
self._dirty_fields.clear()
print("Ending handler...")

Check warning on line 992 in xblock/core.py

View check run for this annotation

Codecov / codecov/patch

xblock/core.py#L991-L992

Added lines #L991 - L992 were not covered by tests


class XBlock2(XBlock2Mixin, XBlock):
"""
Base class for pure "v2" XBlocks, that don't need backwards compatibility with v1
"""

def __init__(
self,
runtime,
field_data=None,
scope_ids=UNSET,
for_parent=None,
**kwargs,
):
"""
Initialize this v2 XBlock, checking for deprecated usage first
"""
if self.has_children is not False:
raise ValueError('v2 XBlocks cannot declare has_children = True')

Check warning on line 1012 in xblock/core.py

View check run for this annotation

Codecov / codecov/patch

xblock/core.py#L1011-L1012

Added lines #L1011 - L1012 were not covered by tests

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

Check warning on line 1015 in xblock/core.py

View check run for this annotation

Codecov / codecov/patch

xblock/core.py#L1014-L1015

Added lines #L1014 - L1015 were not covered by tests

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

Check warning on line 1018 in xblock/core.py

View check run for this annotation

Codecov / codecov/patch

xblock/core.py#L1017-L1018

Added lines #L1017 - L1018 were not covered by tests

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

Check warning on line 1020 in xblock/core.py

View check run for this annotation

Codecov / codecov/patch

xblock/core.py#L1020

Added line #L1020 was not covered by tests

@final
def save(self):
raise AttributeError("Calling .save() on a v2 XBlock is forbidden")

Check warning on line 1024 in xblock/core.py

View check run for this annotation

Codecov / codecov/patch

xblock/core.py#L1024

Added line #L1024 was not covered by tests

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

Check warning on line 1029 in xblock/core.py

View check run for this annotation

Codecov / codecov/patch

xblock/core.py#L1028-L1029

Added lines #L1028 - L1029 were not covered by tests

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

Check warning on line 1035 in xblock/core.py

View check run for this annotation

Codecov / codecov/patch

xblock/core.py#L1033-L1035

Added lines #L1033 - L1035 were not covered by tests

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

Check warning on line 1040 in xblock/core.py

View check run for this annotation

Codecov / codecov/patch

xblock/core.py#L1039-L1040

Added lines #L1039 - L1040 were not covered by tests

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

Check warning on line 1045 in xblock/core.py

View check run for this annotation

Codecov / codecov/patch

xblock/core.py#L1044-L1045

Added lines #L1044 - L1045 were not covered by tests


class XBlockAside(Plugin, Blocklike):
"""
Base class for XBlock-like objects that are rendered alongside :class:`.XBlock` views.
Expand Down
7 changes: 4 additions & 3 deletions xblock/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand Down
Loading