Commit d0da3ceb by Xavier Antoviaque

light-children: Light children as a replacement for XBlock children - WIP

parent da71d55d
from .answer import AnswerBlock
from .dataexport import MentoringDataExportBlock
from .html import HTMLBlock
from .quizz import QuizzBlock, QuizzChoiceBlock, QuizzTipBlock
from .mentoring import MentoringBlock
from .message import MentoringMessageBlock
......
......@@ -27,10 +27,9 @@ import logging
from lazy import lazy
from xblock.core import XBlock
from xblock.fields import Boolean, Scope, String
from xblock.fragment import Fragment
from .light_children import LightChild, Boolean, Scope, String
from .models import Answer
from .utils import load_resource, render_template
......@@ -42,7 +41,7 @@ log = logging.getLogger(__name__)
# Classes ###########################################################
class AnswerBlock(XBlock):
class AnswerBlock(LightChild):
"""
A field where the student enters an answer
......
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 Harvard
#
# Authors:
# Xavier Antoviaque <xavier@antoviaque.org>
#
# This software's license gives you freedom; you can copy, convey,
# propagate, redistribute and/or modify this program under the terms of
# the GNU Affero General Public License (AGPL) as published by the Free
# Software Foundation (FSF), either version 3 of the License, or (at your
# option) any later version of the AGPL published by the FSF.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
# General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program in a file in the toplevel directory called
# "AGPLv3". If not, see <http://www.gnu.org/licenses/>.
#
# Imports ###########################################################
import logging
from xblock.fragment import Fragment
from .light_children import LightChild, Scope, String
# Globals ###########################################################
log = logging.getLogger(__name__)
# Classes ###########################################################
class HTMLBlock(LightChild):
"""
A simplistic replacement for the HTML XModule, as a light XBlock child
"""
content = String(help="HTML content", scope=Scope.content, default="")
@classmethod
def init_block_from_node(cls, block, node):
block.light_children = []
# TODO-LIGHT-CHILDREN: get real value from `node` (lxml)
block.content = '<div>Placeholder HTML content</div>'
return block
def mentoring_view(self, context=None):
return Fragment(self.content)
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 Harvard
#
# Authors:
# Xavier Antoviaque <xavier@antoviaque.org>
#
# This software's license gives you freedom; you can copy, convey,
# propagate, redistribute and/or modify this program under the terms of
# the GNU Affero General Public License (AGPL) as published by the Free
# Software Foundation (FSF), either version 3 of the License, or (at your
# option) any later version of the AGPL published by the FSF.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
# General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program in a file in the toplevel directory called
# "AGPLv3". If not, see <http://www.gnu.org/licenses/>.
#
# Imports ###########################################################
import logging
from xblock.fragment import Fragment
from xblock.plugin import Plugin
from .utils import XBlockWithChildrenFragmentsMixin
# Globals ###########################################################
log = logging.getLogger(__name__)
# Classes ###########################################################
class XBlockWithLightChildrenMixin(XBlockWithChildrenFragmentsMixin):
"""
Allows to use lightweight children on a given XBlock, which will
have a similar behavior but will not be instanciated as full-fledged
XBlocks, which aren't correctly supported as children
TODO: Replace this once the support for XBlock children has matured
by a mixin implementing the following abstractions, used to keep
code reusable in the XBlocks:
* get_children_objects()
* Functionality of XBlockWithChildrenFragmentsMixin
Other changes caused by LightChild use:
* overrides of `parse_xml()` have been replaced by overrides of
`init_block_from_node()` on LightChildren
* fields on LightChild don't have any persistence
"""
def __init__(self, *args, **kwargs):
self.load_children_from_xml_content()
def load_children_from_xml_content(self):
"""
Load light children from the `xml_content` attribute
"""
if not hasattr(self, 'xml_content') or not self.xml_content:
return
# TODO-LIGHT-CHILDREN: replace by proper lxml call
node = None # lxml.load(self.xml_content)
self.init_block_from_node(self, node)
@classmethod
def parse_xml(cls, node, runtime, keys, id_generator):
block = runtime.construct_xblock_from_class(cls, keys)
cls.init_block_from_node(block, node)
return block
@classmethod
def init_block_from_node(cls, block, node):
block.light_children = []
for child_id, xml_child in enumerate(node):
cls.add_node_as_child(block, xml_child, child_id)
for name, value in node.items():
setattr(block, name, value)
return block
@classmethod
def add_node_as_child(cls, block, xml_child, child_id):
# Instantiate child
child_class = cls.get_class_by_element(xml_child.tag)
child = child_class()
child.name = u'{}_{}'.format(block.name, child_id)
log.warn(child_class)
# Add any children the child may itself have
child_class.init_block_from_node(child, xml_child)
block.light_children.append(child)
@classmethod
def get_class_by_element(cls, xml_tag):
return LightChild.load_class(xml_tag)
def get_children_objects(self):
"""
Replacement for ```[self.runtime.get_block(child_id) for child_id in self.children]```
"""
return self.light_children
def render_child(self, child, view_name, context):
"""
Replacement for ```self.runtime.render_child()```
"""
return getattr(child, view_name)(context)
def get_children_fragment(self, context, view_name='student_view', instance_of=None,
not_instance_of=None):
fragment = Fragment()
named_child_frags = []
for child in self.get_children_objects():
if instance_of is not None and not isinstance(child, instance_of):
continue
if not_instance_of is not None and isinstance(child, not_instance_of):
continue
frag = self.render_child(child, view_name, context)
fragment.add_frag_resources(frag)
named_child_frags.append((child.name, frag))
return fragment, named_child_frags
class LightChild(Plugin, XBlockWithLightChildrenMixin):
"""
Base class for the light children
"""
entry_point = 'xblock.light_children'
class LightChildField(object):
"""
Fake field with no persistence - allows to keep XBlocks fields definitions on LightChild
"""
def __init__(self, *args, **kwargs):
pass
class String(LightChildField):
pass
class Boolean(LightChildField):
pass
class Scope(object):
content = None
user_state = None
......@@ -28,9 +28,9 @@ import logging
from xblock.core import XBlock
from xblock.fields import Boolean, Scope, String
from .light_children import XBlockWithLightChildrenMixin
from .message import MentoringMessageBlock
from .utils import get_scenarios_from_path, load_resource, render_template, \
XBlockWithChildrenFragmentsMixin
from .utils import get_scenarios_from_path, load_resource, render_template
# Globals ###########################################################
......@@ -40,7 +40,7 @@ log = logging.getLogger(__name__)
# Classes ###########################################################
class MentoringBlock(XBlock, XBlockWithChildrenFragmentsMixin):
class MentoringBlock(XBlockWithLightChildrenMixin, XBlock):
"""
An XBlock providing mentoring capabilities
......@@ -61,6 +61,7 @@ class MentoringBlock(XBlock, XBlockWithChildrenFragmentsMixin):
default='mentoring', scope=Scope.content)
enforce_dependency = Boolean(help="Should the next step be the current block to complete?",
default=True, scope=Scope.content)
xml_content = String(help="XML content", default='', scope=Scope.content)
has_children = True
def student_view(self, context):
......@@ -107,8 +108,7 @@ class MentoringBlock(XBlock, XBlockWithChildrenFragmentsMixin):
submit_results = []
completed = True
for child_id in self.children: # pylint: disable=E1101
child = self.runtime.get_block(child_id)
for child in self.get_children_objects():
if child.name and child.name in submissions:
submission = submissions[child.name]
child_result = child.submit(submission)
......@@ -136,10 +136,9 @@ class MentoringBlock(XBlock, XBlockWithChildrenFragmentsMixin):
}
def get_message_fragment(self, message_type):
for child_id in self.children: # pylint: disable=E1101
child = self.runtime.get_block(child_id)
for child in self.get_children_objects():
if isinstance(child, MentoringMessageBlock) and child.type == message_type:
return self.runtime.render_child(child, 'mentoring_view', {})
return self.render_child(child, 'mentoring_view', {})
def get_message_html(self, message_type):
fragment = self.get_message_fragment(message_type)
......
......@@ -25,10 +25,8 @@
import logging
from xblock.core import XBlock
from xblock.fields import Scope, String
from .utils import render_template, XBlockWithChildrenFragmentsMixin
from .light_children import LightChild, Scope, String
from .utils import render_template
# Globals ###########################################################
......@@ -38,14 +36,13 @@ log = logging.getLogger(__name__)
# Classes ###########################################################
class MentoringMessageBlock(XBlock, XBlockWithChildrenFragmentsMixin):
class MentoringMessageBlock(LightChild):
"""
A message which can be conditionally displayed at the mentoring block level,
for example upon completion of the block
"""
content = String(help="Message to display upon completion", scope=Scope.content, default="")
type = String(help="Type of message", scope=Scope.content, default="completed")
has_children = True
def mentoring_view(self, context=None):
fragment, named_children = self.get_children_fragment(context, view_name='mentoring_view')
......
......@@ -25,11 +25,10 @@
import logging
from xblock.core import XBlock
from xblock.fields import Scope, String
from xblock.fragment import Fragment
from .utils import load_resource, render_template, XBlockWithChildrenFragmentsMixin
from .light_children import LightChild, Scope, String
from .utils import load_resource, render_template
# Globals ###########################################################
......@@ -53,7 +52,7 @@ def commas_to_list(commas_str):
# Classes ###########################################################
class QuizzBlock(XBlock):
class QuizzBlock(LightChild):
"""
An XBlock used to ask multiple-choice questions
......@@ -66,21 +65,18 @@ class QuizzBlock(XBlock):
student_choice = String(help="Last input submitted by the student", default="", scope=Scope.user_state)
low = String(help="Label for low ratings", scope=Scope.content, default="Less")
high = String(help="Label for high ratings", scope=Scope.content, default="More")
has_children = True
@classmethod
def parse_xml(cls, node, runtime, keys, id_generator):
block = runtime.construct_xblock_from_class(cls, keys)
for child in node:
if child.tag == "question":
block.question = child.text
def init_block_from_node(cls, block, node):
block.light_children = []
for child_id, xml_child in enumerate(node):
if xml_child.tag == "question":
block.question = xml_child.text
else:
block.runtime.add_node_as_child(block, child, id_generator)
cls.add_node_as_child(block, xml_child, child_id)
for name, value in node.items():
if name in block.fields:
setattr(block, name, value)
setattr(block, name, value)
return block
......@@ -103,8 +99,7 @@ class QuizzBlock(XBlock):
@property
def custom_choices(self):
custom_choices = []
for child_id in self.children: # pylint: disable=E1101
child = self.runtime.get_block(child_id)
for child in self.get_children_objects():
if isinstance(child, QuizzChoiceBlock):
custom_choices.append(child)
return custom_choices
......@@ -156,14 +151,13 @@ class QuizzBlock(XBlock):
return tips
class QuizzTipBlock(XBlock, XBlockWithChildrenFragmentsMixin):
class QuizzTipBlock(LightChild):
"""
Each quizz
"""
content = String(help="Text of the tip to provide if needed", scope=Scope.content, default="")
display = String(help="List of choices to display the tip for", scope=Scope.content, default=None)
reject = String(help="List of choices to reject", scope=Scope.content, default=None)
has_children = True
def render(self, submission):
"""
......@@ -199,7 +193,7 @@ class QuizzTipBlock(XBlock, XBlockWithChildrenFragmentsMixin):
return reject or []
class QuizzChoiceBlock(XBlock):
class QuizzChoiceBlock(LightChild):
"""
Custom choice of an answer for a quizz
"""
......
......@@ -29,7 +29,8 @@ import logging
from xblock.core import XBlock
from xblock.fields import Scope, String
from .utils import load_resource, render_template, XBlockWithChildrenFragmentsMixin
from .light_children import LightChild, XBlockWithLightChildrenMixin, String as LightString
from .utils import load_resource, render_template
# Globals ###########################################################
......@@ -39,7 +40,8 @@ log = logging.getLogger(__name__)
# Classes ###########################################################
class MentoringTableBlock(XBlock, XBlockWithChildrenFragmentsMixin):
# TODO-LIGHT-CHILDREN: Transform this into always using as LightChildren
class MentoringTableBlock(XBlockWithLightChildrenMixin, XBlock):
"""
Table-type display of information from mentoring blocks
......@@ -85,11 +87,11 @@ class MentoringTableBlock(XBlock, XBlockWithChildrenFragmentsMixin):
return self.student_view(context)
class MentoringTableColumnBlock(XBlock, XBlockWithChildrenFragmentsMixin):
class MentoringTableColumnBlock(LightChild):
"""
Individual column of a mentoring table
"""
header = String(help="Header of the column", scope=Scope.content, default=None)
header = LightString(help="Header of the column", scope=Scope.content, default=None)
has_children = True
def mentoring_table_view(self, context):
......@@ -119,12 +121,11 @@ class MentoringTableColumnBlock(XBlock, XBlockWithChildrenFragmentsMixin):
return fragment
class MentoringTableColumnHeaderBlock(XBlock, XBlockWithChildrenFragmentsMixin):
class MentoringTableColumnHeaderBlock(LightChild):
"""
Header content for a given column
"""
content = String(help="Body of the header", scope=Scope.content, default='')
has_children = True
content = LightString(help="Body of the header", scope=Scope.content, default='')
def mentoring_table_header_view(self, context):
fragment = super(MentoringTableColumnHeaderBlock, self).children_view(context)
......
......@@ -45,6 +45,9 @@ BLOCKS = [
'mentoring = mentoring:MentoringBlock',
'mentoring-dataexport = mentoring:MentoringDataExportBlock',
'mentoring-table = mentoring:MentoringTableBlock',
]
BLOCKS_CHILDREN = [
'column = mentoring:MentoringTableColumnBlock',
'header = mentoring:MentoringTableColumnHeaderBlock',
'answer = mentoring:AnswerBlock',
......@@ -52,6 +55,7 @@ BLOCKS = [
'message = mentoring:MentoringMessageBlock',
'tip = mentoring:QuizzTipBlock',
'choice = mentoring:QuizzChoiceBlock',
'html = mentoring:HTMLBlock',
]
setup(
......@@ -64,6 +68,7 @@ setup(
],
entry_points={
'xblock.v1': BLOCKS,
'xblock.light_children': BLOCKS_CHILDREN,
},
package_data=package_data("mentoring", "static"),
)
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