Commit ba29389d by Xavier Antoviaque

Merge pull request #6 from open-craft/ui-editor

UI editor for use in Studio (OC-513) (WIP)
parents 00778d03 0a2cd52a
......@@ -9,7 +9,7 @@ install:
- "pip install -r $VIRTUAL_ENV/src/xblock-sdk/requirements.txt"
- "pip install -r $VIRTUAL_ENV/src/xblock-sdk/test-requirements.txt"
- "pip install -r requirements.txt"
- "pip uninstall -y xblock-mentoring && python setup.py sdist && pip install dist/xblock-mentoring-0.1.tar.gz"
- "pip uninstall -y xblock-mentoring && python setup.py sdist && pip install dist/xblock-mentoring-2.0.tar.gz"
script: python run_tests.py --with-coverage --cover-package=mentoring && pep8 mentoring --max-line-length=120 && pylint mentoring --disable=all --enable=function-redefined,undefined-variable,unused-variable
notifications:
email: false
......@@ -54,45 +54,8 @@ Usage
-----
When you add the `Mentoring` component to a course in the studio, the
block is field with default XML content, shown in the screenshot below.
![Edit View](https://raw.githubusercontent.com/edx-solutions/xblock-mentoring/1fb5e3ece6f34b6cf33c956a4fba1d7cbb7349a2/doc/img/edit-view.png)
The wrapping `<mentoring>` supports the following attributes:
* `weight` - The number of points for this block (float; defaults to `1`).
* `max_attempts` - The maximum number of of times the student can
submit an answer for this block. Set to zero for no limit. (integer;
defaults to `0`)
* `url_name` - A unique identifier for this block. Used to refer to
this block from other blocks and to be able to declare dependencies
(string; a default unique value is generated when block is created).
* `followed_by` - The `url_name` of the next block the student should
go to after completing this block. (string; defaults to `None`).
* `enforce_dependency` - Whether to enforce dependencies on this block
as specified with `followed_by`. When set to `true`, the student
will only be allowed to attempt this block after finishing the
block that specifies the current block in its `followed_by`
attribute (boolean; defaults to `false`).
* `mode` - The mentoring mode to use for this block. Two mentoring
modes are currently supported: `standard` and `assessement`. For
more information on the modes see the sectino below (string;
defaults to `standard`)
The wrapping `<mentoring>` element can contain the following child
elements:
* `<title>` - Renders the title of the block.
* `<html>` - May contain arbitrary HTML to be displayed in the block.
* `<shared-header>` - A specialized HTML block, displayed together with the title as a shared header for every step in assessment mode.
* `<answer>` - Represents a free-form answer, rendered as a textarea
element.
* `<mcq>` - Multiple choice question, rendered as radio buttons.
* `<mrq>` - Multiple response question, rendered as checkboxes.
* `<mentoring-table>` - Displays answers to free-form questions in a
HTML table.
* `<message>` - Declares feedback text that is displayed when the
student submits an answer (ignored in `assessment` mode).
built-it editing tools guide you through the process of configuring the
block and adding individual questions.
### Mentoring modes
......@@ -126,41 +89,9 @@ Score review and the "Try Again" button:
### Free-form questions
Free-form questions are represented by an `<answer>` element. The
answer element supports the following attributes:
Free-form questions are represented by a "Long Answer" component.
* `name` - Sets the name of the question. The name can be used to
refer to the question when displaying the answer in another block.
The name is not visible to the student. Should be unique within a
course.
* `weight` - The weight is used when computing total grade/score of
the mentoring block. The larger the weight, the more influence this
question will have on the grade. Value of zero means this question
has no influence on the grade (float, defaults to `1`).
* `min_characters` - The minimum length of the answer. If the answer
contains less than the specified number of characters, the answer is
considered invalid and the student will not be allowed to submit the
answer (integer; defaults to `0`).
* `read_only` - Whether the answer should be rendered read-only. This
only makes sense when displaying an answer to a previously answered
question. The answer is rendered as a HTML quote instead of a
textarea element (boolean; defaults to `false`).
It can also have a `<question>` element containing a paragraph of non-formatted plain text.
#### Example
Here is a simple example of a free-form question:
```xml
<mentoring url_name="goal_definition" followed_by="getting_feedback" weight="20">
<answer name="goal" weight="10">
<question>What is your goal?</question>
</answer>
</mentoring>
```
Screenshot before answering the question:
Example screenshot before answering the question:
![Answer Initial](https://raw.githubusercontent.com/edx-solutions/xblock-mentoring/1fb5e3ece6f34b6cf33c956a4fba1d7cbb7349a2/doc/img/answer-1.png)
......@@ -168,18 +99,9 @@ Screenshot after answering the question:
![Answer Complete](https://raw.githubusercontent.com/edx-solutions/xblock-mentoring/1fb5e3ece6f34b6cf33c956a4fba1d7cbb7349a2/doc/img/answer-2.png)
The second example shows how to display the answer that the student
submitted in the previous step. The only difference is that the
`read_only` attribute is set to `true`.
```xml
<mentoring url_name="getting_feedback">
<html>
<p>The goal you entered was:</p>
</html>
<answer name="goal" read_only="true" />
</mentoring>
```
You can add "Long Answer Recap" components to mentoring blocks later on
in the course to provide a read-only view of any answer that the student
entered earlier.
The read-only answer is rendered as a quote in the LMS:
......@@ -439,40 +361,9 @@ After submitting a wrong answer two times:
### Custom tip popup window size
You can specify `width` and `height` attributes to the `tip` child to
You can specify `width` and `height` attributes of any Tip component to
customize the popup window size. The value of those attribute should
be valid CSS.
```xml
<mentoring url_name="mcq_1" enforce_dependency="false">
<mrq name="mrq_1_1" type="choices">
<question>What do you like in this MRQ?</question>
<choice value="elegance">Its elegance</choice>
...
<tip require="elegance" width="50px" height="20px">
This is something everyone has to like about this MRQ.
</tip>
</mrq>
</mentoring>
```
### Custom Nav title
The Nav title (the black tooltip showed on hover on the Units Nav bar)
is a list of the `display_name` attributes of all the blocks present
in that Unit.
So two Mentoring blocks like the following will result in a tooltip
like the one below:
```xml
<mentoring url_name="mentoring-0a06b184" weight="20" display_name="First Mentoring block">
<mentoring url_name="mentoring-1a04badd" weight="20" display_name="Second Mentoring block">
```
![Nav Title](https://cloud.githubusercontent.com/assets/1225294/2820216/b0228fd8-cef7-11e3-98e1-5fdbf49b706a.png)
The default title is "Mentoring Block".
be valid CSS (e.g. `50px`).
Workbench installation and settings
-----------------------------------
......@@ -543,6 +434,17 @@ $ cat > templates/xml/my_mentoring_scenario.xml
Restart the workbench to take the new scenarios into account.
Upgrading from Version 1
------------------------
To upgrade a course from the earlier version of this XBlock, run the following
command on a system with edx-platform and xblock-mentoring installed:
```bash
$ SERVICE_VARIANT=cms DJANGO_SETTINGS_MODULE="cms.envs.devstack" python -m mentoring.v1.upgrade "Org/Course/Run"
```
Where "Org/Course/Run" is replaced with the ID of the course to upgrade.
License
-------
......
from .answer import AnswerBlock
from .answer import AnswerBlock, AnswerRecapBlock
from .choice import ChoiceBlock
from .html import HTMLBlock
from .mcq import MCQBlock
from .mcq import MCQBlock, RatingBlock
from .mrq import MRQBlock
from .message import MentoringMessageBlock
from .table import MentoringTableBlock, MentoringTableColumnBlock, MentoringTableColumnHeaderBlock
from .table import MentoringTableBlock, MentoringTableColumn
from .tip import TipBlock
from .title import TitleBlock
from .header import SharedHeaderBlock
......@@ -29,9 +29,10 @@ from lazy import lazy
from mentoring.models import Answer
from xblock.core import XBlock
from xblock.fields import Scope, Boolean, Float, Integer, String
from xblock.fields import Scope, Float, Integer, String
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin
from .step import StepMixin
......@@ -43,7 +44,51 @@ loader = ResourceLoader(__name__)
# Classes ###########################################################
class AnswerBlock(XBlock, StepMixin):
class AnswerMixin(object):
"""
Mixin to give an XBlock the ability to read/write data to the Answers DB table.
"""
name = String(
display_name="Answer ID",
help="The ID of the long answer. Should be unique unless you want the answer to be used in multiple places.",
default="",
scope=Scope.content,
)
def _get_course_id(self):
""" Get a course ID if available """
return getattr(self.runtime, 'course_id', 'all')
def _get_student_id(self):
""" Get student anonymous ID or normal ID """
try:
return self.runtime.anonymous_student_id
except AttributeError:
return self.scope_ids.user_id
def get_model_object(self, name=None):
"""
Fetches the Answer model object for the answer named `name`
"""
# By default, get the model object for the current answer's name
if not name:
name = self.name
# Consistency check - we should have a name by now
if not name:
raise ValueError('AnswerBlock.name field need to be set to a non-null/empty value')
student_id = self._get_student_id()
course_id = self._get_course_id()
answer_data, _ = Answer.objects.get_or_create(
student_id=student_id,
course_id=course_id,
name=name,
)
return answer_data
class AnswerBlock(AnswerMixin, StepMixin, StudioEditableXBlockMixin, XBlock):
"""
A field where the student enters an answer
......@@ -55,11 +100,6 @@ class AnswerBlock(XBlock, StepMixin):
default="",
scope=Scope.content
)
read_only = Boolean(
help="Display as a read-only field",
default=False,
scope=Scope.content
)
default_from = String(
help="If specified, get the default value from this answer.",
default=None,
......@@ -82,32 +122,11 @@ class AnswerBlock(XBlock, StepMixin):
enforce_type=True
)
@classmethod
def parse_xml(cls, node, runtime, keys, id_generator):
block = runtime.construct_xblock_from_class(cls, keys)
editable_fields = ('question', 'name', 'min_characters', 'weight', 'default_from')
# Load XBlock properties from the XML attributes:
for name, value in node.items():
setattr(block, name, value)
for xml_child in node:
if xml_child.tag == 'question':
block.question = xml_child.text
else:
block.runtime.add_node_as_child(block, xml_child, id_generator)
return block
def _get_course_id(self):
""" Get a course ID if available """
return getattr(self.runtime, 'course_id', 'all')
def _get_student_id(self):
""" Get student anonymous ID or normal ID """
try:
return self.runtime.anonymous_student_id
except AttributeError:
return self.scope_ids.user_id
@property
def display_name(self):
return u"Question {}".format(self.step_number) if not self.lonely_step else u"Question"
@lazy
def student_input(self):
......@@ -129,14 +148,9 @@ class AnswerBlock(XBlock, StepMixin):
return student_input
def mentoring_view(self, context=None):
if not self.read_only:
html = loader.render_template('templates/html/answer_editable.html', {
'self': self,
})
else:
html = loader.render_template('templates/html/answer_read_only.html', {
'self': self,
})
context = context or {}
context['self'] = self
html = loader.render_template('templates/html/answer_editable.html', context)
fragment = Fragment(html)
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/answer.css'))
......@@ -144,19 +158,10 @@ class AnswerBlock(XBlock, StepMixin):
fragment.initialize_js('AnswerBlock')
return fragment
def mentoring_table_view(self, context=None):
html = loader.render_template('templates/html/answer_table.html', {
'self': self,
})
fragment = Fragment(html)
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/answer_table.css'))
return fragment
def submit(self, submission):
if not self.read_only:
self.student_input = submission[0]['value'].strip()
self.save()
log.info(u'Answer submitted for`{}`: "{}"'.format(self.name, self.student_input))
self.student_input = submission[0]['value'].strip()
self.save()
log.info(u'Answer submitted for`{}`: "{}"'.format(self.name, self.student_input))
return {
'student_input': self.student_input,
'status': self.status,
......@@ -170,7 +175,7 @@ class AnswerBlock(XBlock, StepMixin):
if self.min_characters > 0:
answer_length_ok = len(self.student_input.strip()) >= self.min_characters
return 'correct' if (self.read_only or answer_length_ok) else 'incorrect'
return 'correct' if answer_length_ok else 'incorrect'
@property
def completed(self):
......@@ -189,27 +194,42 @@ class AnswerBlock(XBlock, StepMixin):
# Only attempt to locate a model object for this block when the answer has a name
if self.name:
answer_data = self.get_model_object()
if answer_data.student_input != self.student_input and not self.read_only:
if answer_data.student_input != self.student_input:
answer_data.student_input = self.student_input
answer_data.save()
def get_model_object(self, name=None):
"""
Fetches the Answer model object for the answer named `name`
"""
# By default, get the model object for the current answer's name
if not name:
name = self.name
# Consistency check - we should have a name by now
if not name:
raise ValueError('AnswerBlock.name field need to be set to a non-null/empty value')
student_id = self._get_student_id()
course_id = self._get_course_id()
class AnswerRecapBlock(AnswerMixin, StudioEditableXBlockMixin, XBlock):
"""
A block that displays an answer previously entered by the student (read-only).
"""
display_name = String(
display_name="Title",
help="Title of this answer recap section",
scope=Scope.content,
default="",
)
description = String(
help="Description of this answer (optional). Can include HTML.",
scope=Scope.content,
default="",
display_name="Description",
)
editable_fields = ('name', 'display_name', 'description')
answer_data, _ = Answer.objects.get_or_create(
student_id=student_id,
course_id=course_id,
name=name,
)
return answer_data
@property
def student_input(self):
if self.name:
return self.get_model_object().student_input
return ''
def fallback_view(self, view_name, context=None):
context = context or {}
context['title'] = self.display_name
context['description'] = self.description
context['student_input'] = self.student_input
html = loader.render_template('templates/html/answer_read_only.html', context)
fragment = Fragment(html)
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/answer.css'))
return fragment
......@@ -23,17 +23,73 @@
# Imports ###########################################################
from .common import BlockWithContent
from lxml import etree
from xblock.core import XBlock
from xblock.fields import Scope, String
from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from xblockutils.studio_editable import StudioEditableXBlockMixin
# Classes ###########################################################
class ChoiceBlock(BlockWithContent):
class ChoiceBlock(StudioEditableXBlockMixin, XBlock):
"""
Custom choice of an answer for a MCQ/MRQ
"""
TEMPLATE = 'templates/html/choice.html'
value = String(
display_name="Value",
help="Value of the choice when selected. Should be unique.",
scope=Scope.content,
default="",
)
content = String(
display_name="Choice Text",
help="Human-readable version of the choice value",
scope=Scope.content,
default="",
)
editable_fields = ('value', 'content')
@property
def display_name(self):
try:
status = self.get_parent().describe_choice_correctness(self.value)
except Exception:
status = u"Out of Context" # Parent block should implement describe_choice_correctness()
return u"Choice ({}) ({})".format(self.value, status)
def fallback_view(self, view_name, context):
return Fragment(u'<span class="choice-text">{}</span>'.format(self.content))
def validate_field_data(self, validation, data):
"""
Validate this block's field data.
"""
super(ChoiceBlock, self).validate_field_data(validation, data)
def add_error(msg):
validation.add(ValidationMessage(ValidationMessage.ERROR, msg))
if not data.value.strip():
add_error(u"No value set yet.")
if not data.content.strip():
add_error(u"No choice text set yet.")
@classmethod
def parse_xml(cls, node, runtime, keys, id_generator):
"""
Construct this XBlock from the given XML node.
"""
block = runtime.construct_xblock_from_class(cls, keys)
for field_name in cls.editable_fields:
if field_name in node.attrib:
setattr(block, field_name, node.attrib[field_name])
# HTML content:
block.content = unicode(node.text or u"")
for child in node:
block.content += etree.tostring(child, encoding='unicode')
value = String(help="Value of the choice when selected", scope=Scope.content, default="")
content = String(help="Human-readable version of the choice value", scope=Scope.content, default="")
return block
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014-2015 Harvard, edX & OpenCraft
#
# 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 ###########################################################
from xblock.core import XBlock
from xblock.fields import Scope, String
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
# Classes ###########################################################
class BlockWithContent(XBlock):
"""
A block that can contain simple text content OR <html> blocks
with rich HTML content.
"""
TEMPLATE = None # Override in subclass
content = String(help="Content", scope=Scope.content, default="")
has_children = True
def fallback_view(self, view_name, context=None):
"""
Returns a fragment containing the HTML
"""
fragment = Fragment()
child_content = u""
for child_id in self.children:
child = self.runtime.get_block(child_id)
child_fragment = child.render('mentoring_view', {})
fragment.add_frag_resources(child_fragment)
child_content += child_fragment.content
fragment.add_content(ResourceLoader(__name__).render_template(self.TEMPLATE, {
'self': self,
'content': self.content,
'child_content': child_content,
}))
return fragment # TODO: fragment_text_rewriting
def get_html(self):
""" Render as HTML - not as a Fragment """
return self.fallback_view(None, None).content
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 Harvard
#
# Authors:
# Xavier Antoviaque <xavier@antoviaque.org>
# David Gabor Bodor <david.gabor.bodor@gmail.com>
#
# 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/>.
#
from .html import HTMLBlock
class SharedHeaderBlock(HTMLBlock):
"""
A shared header block shown under the title.
"""
FIXED_CSS_CLASS = "shared-header"
pass
# -*- 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 ###########################################################
from lxml import etree
from xblock.core import XBlock
from xblock.fields import Scope, String
from xblock.fragment import Fragment
# Classes ###########################################################
class HTMLBlock(XBlock):
"""
Render content as HTML
"""
FIXED_CSS_CLASS = "html_child"
content = String(help="HTML content", scope=Scope.content, default="")
css_class = String(help="CSS Class[es] applied to wrapper div element", scope=Scope.content, default="")
@classmethod
def parse_xml(cls, node, runtime, keys, id_generator):
"""
Construct this XBlock from the given XML node.
"""
block = runtime.construct_xblock_from_class(cls, keys)
if node.get('class'): # Older API used "class" property, not "css_class"
node.set('css_class', node.get('css_class', node.get('class')))
del node.attrib['class']
block.css_class = node.get('css_class')
block.content = unicode(node.text or u"")
for child in node:
block.content += etree.tostring(child, encoding='unicode')
return block
def fallback_view(self, view_name, context=None):
""" Default view handler """
css_class = ' '.join(cls for cls in (self.css_class, self.FIXED_CSS_CLASS) if cls)
html = u'<div class="{classes}">{content}</div>'.format(classes=css_class, content=unicode(self.content))
return Fragment(html)
......@@ -25,7 +25,8 @@
import logging
from xblock.fields import Scope, String
from xblock.fields import Scope, String, List
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
from .questionnaire import QuestionnaireAbstractBlock
......@@ -34,6 +35,7 @@ from .questionnaire import QuestionnaireAbstractBlock
# Globals ###########################################################
log = logging.getLogger(__name__)
loader = ResourceLoader(__name__)
# Classes ###########################################################
......@@ -42,22 +44,34 @@ class MCQBlock(QuestionnaireAbstractBlock):
"""
An XBlock used to ask multiple-choice questions
"""
type = String(help="Type of MCQ", scope=Scope.content, default="choices")
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")
valid_types = ('rating', 'choices')
correct_choices = List(
display_name="Correct Choice[s]",
help="Enter the value[s] that students may select for this question to be considered correct. ",
scope=Scope.content,
list_editor="comma-separated",
)
editable_fields = QuestionnaireAbstractBlock.editable_fields + ('correct_choices', )
def describe_choice_correctness(self, choice_value):
if choice_value in self.correct_choices:
if len(self.correct_choices) == 1:
return u"Correct"
return u"Acceptable"
else:
if len(self.correct_choices) == 1:
return u"Wrong"
return u"Not Acceptable"
def submit(self, submission):
log.debug(u'Received MCQ submission: "%s"', submission)
correct = True
correct = submission in self.correct_choices
tips_html = []
for tip in self.get_tips():
correct = correct and self.is_tip_correct(tip, submission)
if submission in tip.display_with_defaults:
tips_html.append(tip.get_html())
if submission in tip.values:
tips_html.append(tip.render('mentoring_view').content)
formatted_tips = ResourceLoader(__name__).render_template('templates/html/tip_choice_group.html', {
'self': self,
......@@ -76,11 +90,51 @@ class MCQBlock(QuestionnaireAbstractBlock):
log.debug(u'MCQ submission result: %s', result)
return result
def is_tip_correct(self, tip, submission):
if not submission:
return False
def author_edit_view(self, context):
"""
The options for the 1-5 values of the Likert scale aren't child blocks but we want to
show them in the author edit view, for clarity.
"""
fragment = Fragment(u"<p>{}</p>".format(self.question))
self.render_children(context, fragment, can_reorder=True, can_add=False)
fragment.add_content(loader.render_template('templates/html/questionnaire_add_buttons.html', {}))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/questionnaire-edit.css'))
return fragment
if submission in tip.reject_with_defaults:
return False
return True
class RatingBlock(MCQBlock):
"""
An XBlock used to rate something on a five-point scale, e.g. Likert Scale
"""
low = String(help="Label for low ratings", scope=Scope.content, default="Less")
high = String(help="Label for high ratings", scope=Scope.content, default="More")
FIXED_VALUES = ["1", "2", "3", "4", "5"]
correct_choices = List(
display_name="Accepted Choice[s]",
help="Enter the rating value[s] that students may select for this question to be considered correct. ",
scope=Scope.content,
list_editor="comma-separated",
default=FIXED_VALUES,
)
editable_fields = MCQBlock.editable_fields + ('low', 'high')
@property
def all_choice_values(self):
return self.FIXED_VALUES + [c.value for c in self.custom_choices]
def author_edit_view(self, context):
"""
The options for the 1-5 values of the Likert scale aren't child blocks but we want to
show them in the author edit view, for clarity.
"""
fragment = Fragment()
fragment.add_content(loader.render_template('templates/html/ratingblock_edit_preview.html', {
'question': self.question,
'low': self.low,
'high': self.high,
'accepted_statuses': [None] + [self.describe_choice_correctness(c) for c in "12345"],
}))
self.render_children(context, fragment, can_reorder=True, can_add=False)
fragment.add_content(loader.render_template('templates/html/questionnaire_add_buttons.html', {}))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/questionnaire-edit.css'))
return fragment
......@@ -22,18 +22,74 @@
#
# Imports ###########################################################
from lxml import etree
from .common import BlockWithContent
from xblock.core import XBlock
from xblock.fields import Scope, String
from xblock.fragment import Fragment
from xblockutils.studio_editable import StudioEditableXBlockMixin
# Classes ###########################################################
class MentoringMessageBlock(BlockWithContent):
class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin):
"""
A message which can be conditionally displayed at the mentoring block level,
for example upon completion of the block
"""
TEMPLATE = 'templates/html/message.html'
content = String(help="Message to display upon completion", scope=Scope.content, default="")
type = String(help="Type of message", scope=Scope.content, default="completed")
content = String(
display_name="Message",
help="Message to display upon completion",
scope=Scope.content,
default="",
multiline_editor="html",
resettable_editor=False,
)
type = String(
help="Type of message",
scope=Scope.content,
default="completed",
values=(
{"display_name": "Completed", "value": "completed"},
{"display_name": "Incompleted", "value": "incomplete"},
{"display_name": "Reached max. # of attemps", "value": "max_attempts_reached"},
),
)
editable_fields = ("content", )
def fallback_view(self, view_name, context):
html = u'<div class="message {msg_type}">{content}</div>'.format(msg_type=self.type, content=self.content)
return Fragment(html)
@property
def display_name(self):
if self.type == 'max_attempts_reached':
max_attempts = self.get_parent().max_attempts
return u"Message when student reaches max. # of attempts ({current_limit})".format(
current_limit=u"unlimited" if max_attempts == 0 else max_attempts
)
if self.type == 'completed':
return u"Message shown when complete"
if self.type == 'incomplete':
return u"Message shown when incomplete"
return u"INVALID MESSAGE"
@classmethod
def get_template(cls, template_id):
"""
Used to interact with Studio's create_xblock method to instantiate pre-defined templates.
"""
return {'metadata': {'type': template_id, 'content': "Message goes here."}, 'data': {}}
@classmethod
def parse_xml(cls, node, runtime, keys, id_generator):
"""
Construct this XBlock from the given XML node.
"""
block = runtime.construct_xblock_from_class(cls, keys)
block.content = unicode(node.text or u"")
block.type = node.attrib['type']
for child in node:
block.content += etree.tostring(child, encoding='unicode')
return block
......@@ -26,6 +26,7 @@
import logging
from xblock.fields import List, Scope, Boolean
from xblock.validation import ValidationMessage
from .questionnaire import QuestionnaireAbstractBlock
from xblockutils.resources import ResourceLoader
......@@ -42,7 +43,35 @@ class MRQBlock(QuestionnaireAbstractBlock):
An XBlock used to ask multiple-response questions
"""
student_choices = List(help="Last submissions by the student", default=[], scope=Scope.user_state)
hide_results = Boolean(help="Hide results", scope=Scope.content, default=False)
required_choices = List(
display_name="Required Choices",
help=(
"Enter the value[s] that students must select for this MRQ to be considered correct. "
"Separate multiple required choices with a comma."
),
scope=Scope.content,
list_editor="comma-separated",
default=[],
)
ignored_choices = List(
display_name="Ignored Choices",
help=(
"Enter the value[s] that are neither correct nor incorrect. "
"Any values not listed as required or ignored will be considered wrong."
),
scope=Scope.content,
list_editor="comma-separated",
default=[],
)
hide_results = Boolean(display_name="Hide results", scope=Scope.content, default=False)
editable_fields = ('question', 'required_choices', 'ignored_choices', 'message', 'weight', 'hide_results', )
def describe_choice_correctness(self, choice_value):
if choice_value in self.required_choices:
return u"Required"
elif choice_value in self.ignored_choices:
return u"Ignored"
return u"Not Acceptable"
def submit(self, submissions):
log.debug(u'Received MRQ submissions: "%s"', submissions)
......@@ -54,13 +83,14 @@ class MRQBlock(QuestionnaireAbstractBlock):
choice_completed = True
choice_tips_html = []
choice_selected = choice.value in submissions
for tip in self.get_tips():
if choice.value in tip.display_with_defaults:
choice_tips_html.append(tip.get_html())
if ((not choice_selected and choice.value in tip.require_with_defaults) or
(choice_selected and choice.value in tip.reject_with_defaults)):
if choice.value in self.required_choices:
if not choice_selected:
choice_completed = False
elif choice_selected and choice.value not in self.ignored_choices:
choice_completed = False
for tip in self.get_tips():
if choice.value in tip.values:
choice_tips_html.append(tip.render('mentoring_view').content)
if choice_completed:
score += 1
......@@ -96,3 +126,27 @@ class MRQBlock(QuestionnaireAbstractBlock):
log.debug(u'MRQ submissions result: %s', result)
return result
def validate_field_data(self, validation, data):
"""
Validate this block's field data.
"""
super(MRQBlock, self).validate_field_data(validation, data)
def add_error(msg):
validation.add(ValidationMessage(ValidationMessage.ERROR, msg))
all_values = set(self.all_choice_values)
required = set(data.required_choices)
ignored = set(data.ignored_choices)
if len(required) < len(data.required_choices):
add_error(u"Duplicate required choices set")
if len(ignored) < len(data.ignored_choices):
add_error(u"Duplicate ignored choices set")
for val in required.intersection(ignored):
add_error(u"A choice is listed as both required and ignored: {}".format(val))
for val in (required - all_values):
add_error(u"A choice value listed as required does not exist: {}".format(val))
for val in (ignored - all_values):
add_error(u"A choice value listed as ignored does not exist: {}".format(val))
......@@ -25,3 +25,7 @@
margin-right: 13px;
vertical-align: -25px;
}
.answer-table {
margin-bottom: 20px;
}
/* Custom appearance for our "Add" buttons */
.xblock .add-xblock-component .new-component .new-component-type .add-xblock-component-button {
width: 200px;
height: 30px;
line-height: 30px;
}
.wrapper-xblock.level-page .xblock-render {
padding: 10px;
}
......@@ -137,6 +137,10 @@ function MCQBlock(runtime, element) {
};
}
function RatingBlock(runtime, element) {
return MCQBlock(runtime, element);
}
function MRQBlock(runtime, element, mentoring) {
return {
mode: null,
......
......@@ -25,18 +25,34 @@
from lxml import etree
from xblock.core import XBlock
from xblock.fields import Scope, String, Float
from xblock.fields import Scope, String, Float, List
from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from xblockutils.helpers import child_isinstance
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin, StudioContainerXBlockMixin
from .choice import ChoiceBlock
from .step import StepMixin
from .tip import TipBlock
# Globals ###########################################################
loader = ResourceLoader(__name__)
# Classes ###########################################################
class QuestionnaireAbstractBlock(XBlock, StepMixin):
class property_with_default(property):
"""
Decorator for creating a dynamic display_name property that looks like an XBlock field. This
is needed for Studio container page blocks as studio will try to read
BlockClass.display_name.default
"""
default = u"Question"
class QuestionnaireAbstractBlock(StudioEditableXBlockMixin, StudioContainerXBlockMixin, StepMixin, XBlock):
"""
An abstract class used for MCQ/MRQ blocks
......@@ -44,51 +60,68 @@ class QuestionnaireAbstractBlock(XBlock, StepMixin):
values entered by the student, and supports multiple types of multiple-choice
set, with preset choices and author-defined values.
"""
type = String(help="Type of questionnaire", scope=Scope.content, default="choices")
question = String(help="Question to ask the student", scope=Scope.content, default="")
message = String(help="General feedback provided when submiting", scope=Scope.content, default="")
weight = Float(help="Defines the maximum total grade of the light child block.",
default=1, scope=Scope.content, enforce_type=True)
valid_types = ('choices')
question = String(
display_name="Question",
help="Question to ask the student",
scope=Scope.content,
default=""
)
message = String(
display_name="Message",
help="General feedback provided when submiting",
scope=Scope.content,
default=""
)
weight = Float(
display_name="Weight",
help="Defines the maximum total grade of this question.",
default=1,
scope=Scope.content,
enforce_type=True
)
editable_fields = ('question', 'message', 'weight')
has_children = True
@classmethod
def parse_xml(cls, node, runtime, keys, id_generator):
"""
Custom XML parser that can handle list type fields properly,
as well as the old way of defining 'question' and 'message' field values via tags.
"""
block = runtime.construct_xblock_from_class(cls, keys)
# Load XBlock properties from the XML attributes:
for name, value in node.items():
setattr(block, name, value)
field = block.fields[name]
if isinstance(field, List) and not value.startswith('['):
# This list attribute is just a string of comma separated strings:
setattr(block, name, [unicode(val).strip() for val in value.split(',')])
elif isinstance(field, String):
setattr(block, name, value)
else:
setattr(block, name, field.from_json(value))
for xml_child in node:
if xml_child.tag == 'question':
block.question = xml_child.text
elif xml_child.tag == 'message' and xml_child.get('type') == 'on-submit':
block.message = (xml_child.text or '').strip()
elif xml_child.tag is not etree.Comment:
if xml_child.tag is not etree.Comment:
block.runtime.add_node_as_child(block, xml_child, id_generator)
return block
@property_with_default
def display_name(self):
return u"Question {}".format(self.step_number) if not self.lonely_step else u"Question"
def student_view(self, context=None):
name = getattr(self, "unmixed_class", self.__class__).__name__
if str(self.type) not in self.valid_types:
raise ValueError(u'Invalid value for {}.type: `{}`'.format(name, self.type))
template_path = 'templates/html/{}.html'.format(name.lower())
template_path = 'templates/html/{}_{}.html'.format(name.lower(), self.type)
loader = ResourceLoader(__name__)
context = context or {}
context['self'] = self
context['custom_choices'] = self.custom_choices
html = loader.render_template(template_path, {
'self': self,
'custom_choices': self.custom_choices
})
fragment = Fragment(html)
fragment.add_css(loader.render_template('public/css/questionnaire.css', {
'self': self
}))
fragment = Fragment(loader.render_template(template_path, context))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/questionnaire.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/questionnaire.js'))
fragment.initialize_js(name)
return fragment
......@@ -100,20 +133,22 @@ class QuestionnaireAbstractBlock(XBlock, StepMixin):
def custom_choices(self):
custom_choices = []
for child_id in self.children:
child = self.runtime.get_block(child_id)
if isinstance(child, ChoiceBlock):
custom_choices.append(child)
if child_isinstance(self, child_id, ChoiceBlock):
custom_choices.append(self.runtime.get_block(child_id))
return custom_choices
@property
def all_choice_values(self):
return [c.value for c in self.custom_choices]
def get_tips(self):
"""
Returns the tips contained in this block
"""
tips = []
for child_id in self.children:
child = self.runtime.get_block(child_id)
if isinstance(child, TipBlock):
tips.append(child)
if child_isinstance(self, child_id, TipBlock):
tips.append(self.runtime.get_block(child_id))
return tips
def get_submission_display(self, submission):
......@@ -124,3 +159,35 @@ class QuestionnaireAbstractBlock(XBlock, StepMixin):
if choice.value == submission:
return choice.content
return submission
def author_edit_view(self, context):
"""
Add some HTML to the author view that allows authors to add choices and tips.
"""
fragment = super(QuestionnaireAbstractBlock, self).author_edit_view(context)
fragment.add_content(loader.render_template('templates/html/questionnaire_add_buttons.html', {}))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/questionnaire-edit.css'))
return fragment
def validate(self):
"""
Validates the state of this XBlock.
"""
validation = super(QuestionnaireAbstractBlock, self).validate()
def add_error(msg):
validation.add(ValidationMessage(ValidationMessage.ERROR, msg))
# Validate the choice values:
all_choice_values = self.all_choice_values
all_choice_values_set = set(all_choice_values)
if len(all_choice_values) != len(all_choice_values_set):
add_error(u"Some choice values are not unique.")
# Validate the tips:
values_with_tips = set()
for tip in self.get_tips():
values = set(tip.values)
for val in (values & values_with_tips):
add_error(u"Multiple tips for value '{}'".format(val))
values_with_tips.update(values)
return validation
......@@ -17,7 +17,20 @@
# along with this program in a file in the toplevel directory called
# "AGPLv3". If not, see <http://www.gnu.org/licenses/>.
#
from .utils import child_isinstance
from lazy import lazy
from xblockutils.helpers import child_isinstance
def _normalize_id(key):
"""
Helper method to normalize a key to avoid issues where some keys have version/branch and others don't.
e.g. self.scope_ids.usage_id != self.runtime.get_block(self.scope_ids.usage_id).scope_ids.usage_id
"""
if hasattr(key, "for_branch"):
key = key.for_branch(None)
if hasattr(key, "for_version"):
key = key.for_version(None)
return key
class StepParentMixin(object):
......@@ -30,19 +43,31 @@ class StepParentMixin(object):
"""
Get the usage_ids of all of this XBlock's children that are "Steps"
"""
return [child_id for child_id in self.children if child_isinstance(self, child_id, StepMixin)]
return [_normalize_id(child_id) for child_id in self.children if child_isinstance(self, child_id, StepMixin)]
class StepMixin(object):
"""
An XBlock mixin for a child block that is a "Step"
"""
@property
has_author_view = True
@lazy
def step_number(self):
return list(self.get_parent().steps).index(self.scope_ids.usage_id) + 1
return list(self.get_parent().steps).index(_normalize_id(self.scope_ids.usage_id)) + 1
@property
@lazy
def lonely_step(self):
if self.scope_ids.usage_id not in self.get_parent().steps:
if _normalize_id(self.scope_ids.usage_id) not in self.get_parent().steps:
raise ValueError("Step's parent should contain Step", self, self.get_parent().steps)
return len(self.get_parent().steps) == 1
def author_view(self, context):
context = context or {}
context['hide_header'] = True
return self.mentoring_view(context)
def author_preview_view(self, context):
context = context or {}
context['hide_header'] = True
return self.student_view(context)
......@@ -25,13 +25,14 @@
import errno
from .utils import child_isinstance
from xblock.core import XBlock
from xblock.exceptions import NoSuchViewError
from xblock.fields import Scope, String
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin, StudioContainerXBlockMixin
# Globals ###########################################################
......@@ -40,47 +41,61 @@ loader = ResourceLoader(__name__)
# Classes ###########################################################
class MentoringTableBlock(XBlock):
class MentoringTableBlock(StudioEditableXBlockMixin, StudioContainerXBlockMixin, XBlock):
"""
Table-type display of information from mentoring blocks
Used to present summary of information entered by the students in mentoring blocks.
Supports different types of formatting through the `type` parameter.
"""
type = String(help="Variant of the table to display", scope=Scope.content, default='')
display_name = String(
display_name="Display name",
help="Title of the table",
default="Answers Table",
scope=Scope.settings
)
type = String(
display_name="Special Mode",
help="Variant of the table that will display a specific background image.",
scope=Scope.content,
default='',
values=[
{"display_name": "Normal", "value": ""},
{"display_name": "Immunity Map Assumptions", "value": "immunity-map-assumptions"},
{"display_name": "Immunity Map", "value": "immunity-map"},
],
)
editable_fields = ("type", )
has_children = True
def student_view(self, context):
context = context or {}
fragment = Fragment()
columns_frags = []
header_frags = []
header_values = []
content_values = []
for child_id in self.children:
child = self.runtime.get_block(child_id)
column_fragment = child.render('mentoring_table_view', context)
fragment.add_frag_resources(column_fragment)
columns_frags.append((child.name, column_fragment))
header_fragment = child.render('mentoring_table_header_view', context)
fragment.add_frag_resources(header_fragment)
header_frags.append((child.name, header_fragment))
bg_image_url = self.runtime.local_resource_url(self, 'public/img/{}-bg.png'.format(self.type))
# Load an optional description for the background image, for accessibility
try:
bg_image_description = loader.load_unicode('static/text/table-{}.txt'.format(self.type))
except IOError as e:
if e.errno == errno.ENOENT:
bg_image_description = ''
else:
raise
fragment.add_content(loader.render_template('templates/html/mentoring-table.html', {
'self': self,
'columns_frags': columns_frags,
'header_frags': header_frags,
'bg_image_url': bg_image_url,
'bg_image_description': bg_image_description,
}))
# Child should be an instance of MentoringTableColumn
header_values.append(child.header)
child_frag = child.render('mentoring_view', context)
content_values.append(child_frag.content)
fragment.add_frag_resources(child_frag)
context['header_values'] = header_values if any(header_values) else None
context['content_values'] = content_values
if self.type:
# Load an optional background image:
context['bg_image_url'] = self.runtime.local_resource_url(self, 'public/img/{}-bg.png'.format(self.type))
# Load an optional description for the background image, for accessibility
try:
context['bg_image_description'] = loader.load_unicode('static/text/table-{}.txt'.format(self.type))
except IOError as e:
if e.errno == errno.ENOENT:
pass
else:
raise
fragment.add_content(loader.render_template('templates/html/mentoring-table.html', context))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/mentoring-table.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/vendor/jquery-shorten.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring-table.js'))
......@@ -92,58 +107,57 @@ class MentoringTableBlock(XBlock):
# Allow to render within mentoring blocks, or outside
return self.student_view(context)
def author_edit_view(self, context):
"""
Add some HTML to the author view that allows authors to add choices and tips.
"""
fragment = super(MentoringTableBlock, self).author_edit_view(context)
fragment.add_content(loader.render_template('templates/html/mentoring-table-add-button.html', {}))
# Share styles with the questionnaire edit CSS:
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/questionnaire-edit.css'))
return fragment
class MentoringTableColumnBlock(XBlock):
class MentoringTableColumn(StudioEditableXBlockMixin, StudioContainerXBlockMixin, XBlock):
"""
Individual column of a mentoring table
A column in a mentoring table. Has a header and can contain HTML and AnswerRecapBlocks.
"""
header = String(help="Header of the column", scope=Scope.content, default=None)
display_name = String(display_name="Display Name", default="Column")
header = String(
display_name="Header",
help="Header of this column",
default="",
scope=Scope.content,
multiline_editor="html",
)
editable_fields = ("header", )
has_children = True
def _render_table_view(self, view_name, id_filter, template, context):
def fallback_view(self, view_name, context):
context = context or {}
fragment = Fragment()
named_children = []
for child_id in self.children:
if id_filter(child_id):
child = self.runtime.get_block(child_id)
child = self.runtime.get_block(child_id)
if child.scope_ids.block_type == "html":
# HTML block current doesn't support "mentoring_view" and if "student_view" is used, it gets wrapped
# with HTML we don't want. So just grab its HTML directly.
child_frag = Fragment(child.data)
else:
child_frag = child.render(view_name, context)
fragment.add_frag_resources(child_frag)
named_children.append((child.name, child_frag))
fragment.add_content(loader.render_template('templates/html/{}'.format(template), {
'self': self,
'named_children': named_children,
}))
fragment.add_content(child_frag.content)
fragment.add_frag_resources(child_frag)
return fragment
def mentoring_table_view(self, context):
"""
The content of the column
"""
return self._render_table_view(
view_name='mentoring_table_view',
id_filter=lambda child_id: not child_isinstance(self, child_id, MentoringTableColumnHeaderBlock),
template='mentoring-table-column.html',
context=context
)
def mentoring_table_header_view(self, context):
def author_preview_view(self, context):
return self.author_edit_view(context)
def author_edit_view(self, context):
"""
The content of the column's header
Add some HTML to the author view that allows authors to add choices and tips.
"""
return self._render_table_view(
view_name='mentoring_table_header_view',
id_filter=lambda child_id: child_isinstance(self, child_id, MentoringTableColumnHeaderBlock),
template='mentoring-table-header.html',
context=context
)
class MentoringTableColumnHeaderBlock(XBlock):
"""
Header content for a given column
"""
content = String(help="Body of the header", scope=Scope.content, default='')
def mentoring_table_header_view(self, context):
return Fragment(unicode(self.content))
fragment = super(MentoringTableColumn, self).author_edit_view(context)
fragment.content = u"<div style=\"font-weight: bold;\">{}</div>".format(self.header) + fragment.content
fragment.add_content(loader.render_template('templates/html/mentoring-column-add-button.html', {}))
# Share styles with the questionnaire edit CSS:
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/questionnaire-edit.css'))
return fragment
<div class="xblock-answer" data-completed="{{ self.completed }}">
<h3 class="question-title">QUESTION {% if not self.lonely_step %}{{ self.step_number }}{% endif %}</h3>
{% if not hide_header %}<h3 class="question-title">{{ self.display_name }}</h3>{% endif %}
<p>{{ self.question }}</p>
<textarea
class="answer editable" cols="50" rows="10" name="input"
......
<div class="xblock-answer" data-completed="{{ self.completed }}">
<h3 class="question-title">QUESTION {% if not self.lonely_step %}{{ self.step_number }}{% endif %}</h3>
<p>{{ self.question }}</p>
{% load i18n %}
<div class="xblock-answer" data-completed="{{ student_input|yesno:"true,false" }}">
{% if not hide_header %}<h3 class="question-title">{{ title }}</h3>{% endif %}
{% if description %}<p>{{ description|safe }}</p>{% endif %}
<blockquote class="answer read_only">
{{ self.student_input|linebreaksbr }}
{% if student_input %}
{{ student_input|linebreaksbr }}
{% else %}
<em>{% trans "No answer yet." %}</em>
{% endif %}
</blockquote>
</div>
<div class="answer-table">
{{ self.student_input|linebreaksbr }}
</div>
<span class="choice-text">
{{ self.content }}
{{ child_content|safe }}
</span>
<fieldset class="choices questionnaire">
<legend class="question">
<h3 class="question-title">QUESTION {% if not self.lonely_step %}{{ self.step_number }}{% endif %}</h3>
{% if not hide_header %}<h3 class="question-title">{{ self.display_name }}</h3>{% endif %}
<p>{{ self.question }}</p>
</legend>
<div class="choices-list">
......@@ -9,7 +9,7 @@
<div class="choice-result fa icon-2x"></div>
<label class="choice-label">
<input class="choice-selector" type="radio" name="{{ self.name }}" value="{{ choice.value }}"{% if self.student_choice == choice.value %} checked{% endif %} />
{{ choice.get_html|safe }}
<span class="choice-text">{{ choice.content|safe }}</span>
</label>
<div class="choice-tips"></div>
</div>
......
{% load i18n %}
<div class="add-xblock-component new-component-item adding">
<div class="new-component">
<ul class="new-component-type">
<li><a href="#" class="single-template add-xblock-component-button" data-category="html">{% trans "Add HTML" %}</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-answer-recap">{% trans "Add Answer Recap" %}</a></li>
</ul>
</div>
</div>
{% load i18n %}
<div class="add-xblock-component new-component-item adding">
<div class="new-component">
<ul class="new-component-type">
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-column">{% trans "Add Answer Recap Column" %}</a></li>
</ul>
</div>
</div>
<td>
<div class="mentoring-column">
{% for name, c in named_children %}
{{c.body_html|safe}}
{% endfor %}
</div>
</td>
<th>
{% for name, c in named_children %}
{{c.body_html|safe}}
{% endfor %}
</th>
<div class="mentoring-table {{ self.type }}" style="background-image: url({{ bg_image_url }})">
<div class="cont-text-sr">{{ bg_image_description }}</div>
<table>
{% if header_frags %}
{% if header_values %}
<thead>
{% for name, c in header_frags %}
{{c.body_html|safe}}
{% for header in header_values %}
<th>{{ header|safe }}</th>
{% endfor %}
</thead>
{% endif %}
<tbody>
<tr>
{% for name, c in columns_frags %}
{{c.body_html|safe}}
{% for content in content_values %}
<td>
<div class="mentoring-column">
{{content|safe}}
</div>
</td>
{% endfor %}
</tr>
</tbody>
......
<div class="message {{ self.type }}">
{% if self.content %}
<p>{{ self.content }}</p>
{% endif %}
{{ child_content|safe }}
</div>
<fieldset class="choices questionnaire" data-hide_results="{{self.hide_results}}">
<legend class="question">
<h3 class="question-title">QUESTION {% if not self.lonely_step %}{{ self.step_number }}{% endif %}</h3>
{% if not hide_header %}<h3 class="question-title">{{ self.display_name }}</h3>{% endif %}
<p>{{ self.question }}</p>
</legend>
<div class="choices-list">
......@@ -11,7 +11,7 @@
<input class="choice-selector" type="checkbox" name="{{ self.name }}"
value="{{ choice.value }}"
{% if choice.value in self.student_choices %} checked{% endif %} />
{{ choice.get_html|safe }}
<span class="choice-text">{{ choice.content|safe }}</span>
</label>
<div class="choice-tips"></div>
</div>
......
{% load i18n %}
<div class="add-xblock-component new-component-item adding">
<div class="new-component">
<ul class="new-component-type">
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-choice">Add Custom Choice</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-tip">Add Tip</a></li>
</ul>
</div>
</div>
<fieldset class="rating questionnaire">
<legend class="question">
<h3 class="question-title">QUESTION {% if not self.lonely_step %}{{ self.step_number }}{% endif %}</h3>
{% if not hide_header %}<h3 class="question-title">{{ self.display_name }}</h3>{% endif %}
<p>{{ self.question }}</p>
</legend>
<div class="choices-list">
......@@ -35,7 +35,7 @@
<div class="choice">
<div class="choice-result fa icon-2x"></div>
<label><input type="radio" name="{{ self.name }}" value="{{ choice.value }}"{% if self.student_choice == '{{ choice.value }}' %} checked{% endif %} />
{{ choice.get_html|safe }}
<span class="choice-text">{{ choice.content|safe }}</span>
</label>
<div class="choice-tips"></div>
</div>
......
<p>{{ question }}</p>
<h2>Built-in choices:</h2>
<ul>
<li>Choice (1): <strong>1 - {{ low }}</strong> ({{accepted_statuses.1}})</li>
<li>Choice (2): <strong>2</strong> ({{accepted_statuses.2}})</li>
<li>Choice (3): <strong>3</strong> ({{accepted_statuses.3}})</li>
<li>Choice (4): <strong>4</strong> ({{accepted_statuses.4}})</li>
<li>Choice (5): <strong>5 - {{ high }}</strong> ({{accepted_statuses.5}})</li>
</ul>
<h2>Additional custom choices and tips:</h2>
......@@ -3,8 +3,5 @@
{% if width %}data-width="{{width}}"{% endif %}
{% if height %}data-height="{{height}}"{% endif %}
>
{% if self.content %}
<p>{{ self.content }}</p>
{% endif %}
{{ child_content|safe }}
<p>{{ content|safe }}</p>
</div>
......@@ -23,8 +23,14 @@
# Imports ###########################################################
from .common import BlockWithContent
from xblock.fields import Scope, String
from lxml import etree
from xblock.core import XBlock
from xblock.fields import Scope, String, List
from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin
# Functions #########################################################
......@@ -41,28 +47,70 @@ def commas_to_set(commas_str):
# Classes ###########################################################
class TipBlock(BlockWithContent):
class TipBlock(StudioEditableXBlockMixin, XBlock):
"""
Each choice can define a tip depending on selection
"""
TEMPLATE = 'templates/html/tip.html'
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)
require = String(help="List of choices to require", scope=Scope.content, default=None)
values = List(
display_name="For Choices",
help="List of choice value[s] to display the tip for",
scope=Scope.content,
default=[],
)
width = String(help="Width of the tip popup", scope=Scope.content, default='')
height = String(help="Height of the tip popup", scope=Scope.content, default='')
editable_fields = ('values', 'content', 'width', 'height')
@property
def display_with_defaults(self):
display = commas_to_set(self.display)
return display | self.reject_with_defaults | self.require_with_defaults
def display_name(self):
return u"Tip for {}".format(u", ".join([unicode(v) for v in self.values]))
@property
def reject_with_defaults(self):
return commas_to_set(self.reject)
def fallback_view(self, view_name, context):
html = ResourceLoader(__name__).render_template("templates/html/tip.html", {
'content': self.content,
'width': self.width,
'height': self.height,
})
return Fragment(html)
@property
def require_with_defaults(self):
return commas_to_set(self.require)
def clean_studio_edits(self, data):
"""
Clean up the edits during studio_view save
"""
if "values" in data:
data["values"] = list([unicode(v) for v in set(data["values"])])
def validate_field_data(self, validation, data):
"""
Validate this block's field data.
"""
super(TipBlock, self).validate_field_data(validation, data)
def add_error(msg):
validation.add(ValidationMessage(ValidationMessage.ERROR, msg))
try:
valid_values = set(self.get_parent().all_choice_values)
except Exception:
pass
else:
for val in set(data.values) - valid_values:
add_error(u"A choice value listed for this tip does not exist: {}".format(val))
@classmethod
def parse_xml(cls, node, runtime, keys, id_generator):
"""
Construct this XBlock from the given XML node.
"""
block = runtime.construct_xblock_from_class(cls, keys)
block.values = [unicode(val).strip() for val in node.get('values', '').split(',')]
block.width = node.get('width', '')
block.height = node.get('height', '')
block.content = unicode(node.text or u"")
for child in node:
block.content += etree.tostring(child, encoding='unicode')
return block
# -*- 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 ###########################################################
from xblock.core import XBlock
from xblock.fields import Scope, String
# Classes ###########################################################
class TitleBlock(XBlock):
"""
A simple html representation of a title, with the mentoring weight.
"""
content = String(help="Text to display", scope=Scope.content, default="")
"""
Helper methods
Should eventually be moved to xblock-utils.
"""
def child_isinstance(block, child_id, block_class_or_mixin):
"""
Is "block"'s child identified by usage_id "child_id" an instance of
"block_class_or_mixin"?
This is a bit complicated since it avoids the need to actually
instantiate the child block.
"""
def_id = block.runtime.id_reader.get_definition_id(child_id)
type_name = block.runtime.id_reader.get_block_type(def_id)
child_class = block.runtime.load_block_type(type_name)
return issubclass(child_class, block_class_or_mixin)
......@@ -27,31 +27,30 @@ import logging
from collections import namedtuple
from lxml import etree
from StringIO import StringIO
from xblock.core import XBlock
from xblock.exceptions import NoSuchViewError
from xblock.fields import Boolean, Scope, String, Integer, Float, List
from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from components import TitleBlock, SharedHeaderBlock, MentoringMessageBlock
from components import MentoringMessageBlock
from components.step import StepParentMixin, StepMixin
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin, StudioContainerXBlockMixin
# Globals ###########################################################
log = logging.getLogger(__name__)
loader = ResourceLoader(__name__)
default_xml_content = loader.render_template('templates/xml/mentoring_default.xml', {})
# Classes ###########################################################
Score = namedtuple("Score", ["raw", "percentage", "correct", "incorrect", "partially_correct"])
class MentoringBlock(XBlock, StepParentMixin):
@XBlock.needs("i18n")
class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioContainerXBlockMixin):
"""
An XBlock providing mentoring capabilities
......@@ -60,14 +59,10 @@ class MentoringBlock(XBlock, StepParentMixin):
student is a) provided mentoring advices and asked to alter his answer, or b) is given the
ok to continue.
"""
@staticmethod
def is_default_xml_content(value):
return value == default_xml_content
# Content
MENTORING_MODES = ('standard', 'assessment')
mode = String(
display_name="Mode",
help="Mode of the mentoring. 'standard' or 'assessment'",
default='standard',
scope=Scope.content,
......@@ -84,14 +79,6 @@ class MentoringBlock(XBlock, StepParentMixin):
scope=Scope.content,
enforce_type=True
)
url_name = String(
help="Name of the current step, used for URL building",
default='mentoring-default',
scope=Scope.content
# TODO in future: set this field's default to xblock.fields.UNIQUE_ID
# and remove self.url_name_with_default. Waiting until UNIQUE_ID support
# is available in edx-platform's pinned version of xblock. (See XBlock PR 249)
)
enforce_dependency = Boolean(
help="Should the next step be the current block to complete?",
default=False,
......@@ -104,7 +91,13 @@ class MentoringBlock(XBlock, StepParentMixin):
scope=Scope.content,
enforce_type=True
)
xml_content = String(help="XML content", default=default_xml_content, scope=Scope.content)
xml_content = String(
help="Not used for version 2. This field is here only to preserve the data needed to upgrade from v1 to v2.",
display_name="XML content",
default='',
scope=Scope.content,
multiline_editor=True
)
# Settings
weight = Float(
......@@ -114,8 +107,8 @@ class MentoringBlock(XBlock, StepParentMixin):
enforce_type=True
)
display_name = String(
help="Display name of the component",
default="Mentoring XBlock",
help="Title to display",
default="Mentoring Questions",
scope=Scope.settings
)
......@@ -155,11 +148,17 @@ class MentoringBlock(XBlock, StepParentMixin):
scope=Scope.preferences
)
editable_fields = (
'mode', 'followed_by', 'max_attempts', 'enforce_dependency',
'display_submit', 'weight', 'display_name',
)
icon_class = 'problem'
has_score = True
has_children = True
FLOATING_BLOCKS = (TitleBlock, MentoringMessageBlock, SharedHeaderBlock)
def _(self, text):
""" translate text """
return self.runtime.service(self, "i18n").ugettext(text)
@property
def is_assessment(self):
......@@ -171,7 +170,7 @@ class MentoringBlock(XBlock, StepParentMixin):
weights = (float(self.runtime.get_block(step_id).weight) for step_id in self.steps)
total_child_weight = sum(weights)
if total_child_weight == 0:
return (0, 0, 0, 0)
return Score(0, 0, 0, 0, 0)
score = sum(r[1]['score'] * r[1]['weight'] for r in self.student_results) / total_child_weight
correct = sum(1 for r in self.student_results if r[1]['status'] == 'correct')
incorrect = sum(1 for r in self.student_results if r[1]['status'] == 'incorrect')
......@@ -184,27 +183,31 @@ class MentoringBlock(XBlock, StepParentMixin):
self.migrate_fields()
fragment = Fragment()
title = u""
header = u""
child_content = u""
for child_id in self.children:
child = self.runtime.get_block(child_id)
if isinstance(child, TitleBlock):
title = child.content
elif isinstance(child, SharedHeaderBlock):
header = child.render('mentoring_view', context).content
elif isinstance(child, MentoringMessageBlock):
if isinstance(child, MentoringMessageBlock):
pass # TODO
else:
child_fragment = child.render('mentoring_view', context)
try:
child_fragment = child.render('mentoring_view', context)
except NoSuchViewError:
if child.scope_ids.block_type == 'html':
if getattr(self.runtime, 'is_author_mode', False):
# html block doesn't support mentoring_view, and if we use student_view Studio will wrap
# it in HTML that we don't want in the preview. So just render its HTML directly:
child_fragment = Fragment(child.data)
else:
child_fragment = child.render('student_view', context)
else:
raise # This type of child is not supported.
fragment.add_frag_resources(child_fragment)
child_content += child_fragment.content
fragment.add_content(loader.render_template('templates/html/mentoring.html', {
'self': self,
'title': title,
'header': header,
'title': self.display_name,
'child_content': child_content,
'missing_dependency_url': self.has_missing_dependency and self.next_step_url,
}))
......@@ -239,36 +242,16 @@ class MentoringBlock(XBlock, StepParentMixin):
def additional_publish_event_data(self):
return {
'user_id': self.scope_ids.user_id,
'component_id': self.url_name_with_default,
'component_id': self.url_name,
}
@property
def title(self):
"""
Returns the title child.
"""
for child in self.get_children_objects():
if isinstance(child, TitleBlock):
return child
return None
@property
def header(self):
"""
Return the header child.
"""
for child in self.get_children_objects():
if isinstance(child, SharedHeaderBlock):
return child
return None
@property
def has_missing_dependency(self):
"""
Returns True if the student needs to complete another step before being able to complete
the current one, and False otherwise
"""
return self.enforce_dependency and (not self.completed) and (self.next_step != self.url_name_with_default)
return self.enforce_dependency and (not self.completed) and (self.next_step != self.url_name)
@property
def next_step_url(self):
......@@ -277,6 +260,17 @@ class MentoringBlock(XBlock, StepParentMixin):
"""
return '/jump_to_id/{}'.format(self.next_step)
@property
def url_name(self):
"""
Get the url_name for this block. In Studio/LMS it is provided by a mixin, so we just
defer to super(). In the workbench or any other platform, we use the usage_id.
"""
try:
return super(MentoringBlock, self).url_name
except AttributeError:
return unicode(self.scope_ids.usage_id)
@XBlock.json_handler
def view(self, data, suffix=''):
"""
......@@ -332,7 +326,7 @@ class MentoringBlock(XBlock, StepParentMixin):
if self.has_missing_dependency:
completed = False
message = 'You need to complete all previous steps before being able to complete the current one.'
elif completed and self.next_step == self.url_name_with_default:
elif completed and self.next_step == self.url_name:
self.next_step = self.followed_by
# Once it was completed, lock score
......@@ -375,7 +369,7 @@ class MentoringBlock(XBlock, StepParentMixin):
completed = False
current_child = None
children = [self.runtime.get_block(child_id) for child_id in self.children]
children = [child for child in children if not isinstance(child, self.FLOATING_BLOCKS)]
children = [child for child in children if not isinstance(child, MentoringMessageBlock)]
steps = [child for child in children if isinstance(child, StepMixin)] # Faster than the self.steps property
for child in children:
......@@ -466,72 +460,84 @@ class MentoringBlock(XBlock, StepParentMixin):
html += child.render('mentoring_view', {}).content # TODO: frament_text_rewriting ?
return html
def studio_view(self, context):
def clean_studio_edits(self, data):
"""
Editing view in Studio
Given POST data dictionary 'data', clean the data before validating it.
e.g. fix capitalization, remove trailing spaces, etc.
"""
fragment = Fragment()
fragment.add_content(loader.render_template('templates/html/mentoring_edit.html', {
'self': self,
'xml_content': self.xml_content,
}))
fragment.add_javascript_url(
self.runtime.local_resource_url(self, 'public/js/mentoring_edit.js'))
fragment.add_css_url(
self.runtime.local_resource_url(self, 'public/css/mentoring_edit.css'))
fragment.initialize_js('MentoringEditBlock')
if data.get('mode') == 'assessment' and 'max_attempts' not in data:
# assessment has a default of 2 max_attempts
data['max_attempts'] = 2
def validate(self):
"""
Validates the state of this XBlock except for individual field values.
"""
validation = super(MentoringBlock, self).validate()
a_child_has_issues = False
message_types_present = set()
for child_id in self.children:
child = self.runtime.get_block(child_id)
# Check if the child has any errors:
if not child.validate().empty:
a_child_has_issues = True
# Ensure there is only one "message" block of each type:
if isinstance(child, MentoringMessageBlock):
msg_type = child.type
if msg_type in message_types_present:
validation.add(ValidationMessage(
ValidationMessage.ERROR,
self._(u"There should only be one '{}' message component.".format(msg_type))
))
message_types_present.add(msg_type)
if a_child_has_issues:
validation.add(ValidationMessage(
ValidationMessage.ERROR,
self._(u"A component inside this mentoring block has issues.")
))
return validation
def author_preview_view(self, context):
"""
Child blocks can override this to add a custom preview shown to authors in Studio when
not editing this block's children.
"""
fragment = self.student_view(context)
fragment.add_content(loader.render_template('templates/html/mentoring_url_name.html', {
"url_name": self.url_name
}))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/mentoring_edit.css'))
return fragment
@XBlock.json_handler
def studio_submit(self, submissions, suffix=''):
log.info(u'Received studio submissions: {}'.format(submissions))
success = True
xml_content = submissions['xml_content']
try:
content = etree.parse(StringIO(xml_content))
except etree.XMLSyntaxError as e:
response = {
'result': 'error',
'message': e.message
}
success = False
else:
root = content.getroot()
if 'mode' in root.attrib:
if root.attrib['mode'] not in self.MENTORING_MODES:
response = {
'result': 'error',
'message': "Invalid mentoring mode: should be 'standard' or 'assessment'"
}
success = False
elif root.attrib['mode'] == 'assessment' and 'max_attempts' not in root.attrib:
# assessment has a default of 2 max_attempts
root.attrib['max_attempts'] = '2'
if success:
response = {
'result': 'success',
}
self.xml_content = etree.tostring(content, pretty_print=True)
log.debug(u'Response from Studio: {}'.format(response))
return response
def author_edit_view(self, context):
"""
Add some HTML to the author view that allows authors to add child blocks.
"""
fragment = super(MentoringBlock, self).author_edit_view(context)
fragment.add_content(loader.render_template('templates/html/mentoring_add_buttons.html', {}))
fragment.add_content(loader.render_template('templates/html/mentoring_url_name.html', {
"url_name": self.url_name
}))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/mentoring_edit.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring_edit.js'))
fragment.initialize_js('MentoringEditComponents')
return fragment
@property
def url_name_with_default(self):
@classmethod
def parse_xml(cls, node, runtime, keys, id_generator):
"""
Ensure the `url_name` is set to a unique, non-empty value.
In future once hte pinned version of XBlock is updated,
we can remove this and change the field to use the
xblock.fields.UNIQUE_ID flag instead.
To avoid collisions with e.g. the existing <html> XBlock in edx-platform,
we prefix all of the mentoring block tags with "mentoring-". However,
using that prefix in the XML is optional. This method adds that prefix
in when parsing XML in a mentoring context.
"""
if self.url_name == 'mentoring-default':
return self.scope_ids.usage_id
else:
return self.url_name
PREFIX_TAGS = ("answer", "answer-recap", "quizz", "mcq", "mrq", "rating", "message", "tip", "choice", "column")
for element in node.iter():
# We have prefixed all our XBlock entry points with "mentoring-". But using the "mentoring-"
# prefix in the XML is optional:
if element.tag in PREFIX_TAGS:
element.tag = "mentoring-{}".format(element.tag)
return super(MentoringBlock, cls).parse_xml(node, runtime, keys, id_generator)
@staticmethod
def workbench_scenarios():
......
.mentoring-edit .module-actions .error-message {
color: red;
/* Display of url_name below content */
.url-name-footer {
font-style: italic;
}
.url-name-footer .url-name {
margin: 0 10px;
font-family: monospace;
}
/* Custom appearance for our "Add" buttons */
.xblock[data-block-type=mentoring] .add-xblock-component .new-component .new-component-type .add-xblock-component-button {
width: 200px;
height: 30px;
line-height: 30px;
}
.xblock[data-block-type=mentoring] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled,
.xblock[data-block-type=mentoring] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled:hover {
background-color: #ccc;
border-color: #888;
cursor: default;
}
function MentoringEditBlock(runtime, element) {
var xmlEditorTextarea = $('.block-xml-editor', element),
xmlEditor = CodeMirror.fromTextArea(xmlEditorTextarea[0], { mode: 'xml' });
$('.save-button', element).bind('click', function() {
var handlerUrl = runtime.handlerUrl(element, 'studio_submit'),
data = {
'xml_content': xmlEditor.getValue(),
};
$('.error-message', element).html();
$.post(handlerUrl, JSON.stringify(data)).done(function(response) {
if (response.result === 'success') {
window.location.reload(false);
} else {
$('.error-message', element).html('Error: '+response.message);
}
function MentoringEditComponents(runtime, element) {
"use strict";
// Disable "add" buttons when a message of that type already exists:
var $buttons = $('.add-xblock-component-button[data-category=mentoring-message]', element);
var updateButtons = function() {
$buttons.each(function() {
var msg_type = $(this).data('boilerplate');
$(this).toggleClass('disabled', $('.xblock .message.'+msg_type).length > 0);
});
};
updateButtons();
$buttons.click(function(ev) {
if ($(this).is('.disabled')) {
ev.preventDefault();
ev.stopPropagation();
} else {
$(this).addClass('disabled');
}
});
$('.cancel-button', element).bind('click', function() {
runtime.notify('cancel', {});
});
runtime.listenTo('deleted-child', updateButtons);
}
......@@ -4,10 +4,9 @@
attempting this step.
</div>
{% if title or header %}
{% if title %}
<div class="title">
{% if title %} <h2>{{ title }}</h2> {% endif %}
{% if header %} {{ header|safe }} {% endif %}
</div>
{% endif %}
......
{% load i18n %}
<div class="add-xblock-component new-component-item adding">
<div class="new-component">
<h5>Add New Component</h5>
<ul class="new-component-type">
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-answer">Long Answer</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-mcq">Multiple Choice Question</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-rating">Rating Question</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-mrq">Multiple Response Question</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="html">HTML</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-answer-recap">Long Answer Recap</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-table">Answer Recap Table</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-message" data-boilerplate="completed">Message (Complete)</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-message" data-boilerplate="incomplete">Message (Incomplete)</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-message" data-boilerplate="max_attempts_reached">Message (Max # Attempts)</a></li>
</ul>
</div>
</div>
{% load i18n %}
<!-- TODO: Replace by default edit view once available in Studio -->
<div class="mentoring-edit editor-with-buttons">
<div class="wrapper-comp-settings is-active" id="settings-tab">
<textarea class="block-xml-editor">{{ xml_content }}</textarea>
</div>
<div class="xblock-actions">
<span class="error-message"></span>
<ul>
<li class="action-item">
<a href="#" class="button action-primary save-button">{% trans "Save" %}</a>
</li>
<li class="action-item">
<a href="#" class="button cancel-button">{% trans "Cancel" %}</a>
</li>
</ul>
</div>
</div>
{% load i18n %}
<div class="xblock-header-secondary url-name-footer">
<span class="url-name-label">{% trans "url_name for linking to this mentoring question set:" %}</span>
<span class="url-name">{{ url_name }}</span>
</div>
<mentoring display_name="Nav tooltip title" weight="1" mode="assessment" max_attempts="10">
<title>Default Title</title>
<shared-header>
<mentoring display_name="Mentoring Assessment Example" weight="1" mode="assessment" max_attempts="10">
<html_demo>
<p>This paragraph is shared between <strong>all</strong> questions.</p>
</shared-header>
<html>
<p>Please answer the questions below.</p>
</html>
</html_demo>
<answer name="goal">
<question>What is your goal?</question>
<answer name="goal" question="What is your goal?">
</answer>
<mcq name="mcq_1_1" type="choices">
<question>Do you like this MCQ?</question>
<mcq name="mcq_1_1" question="Do you like this MCQ?" correct_choices="yes">
<choice value="yes">Yes</choice>
<choice value="maybenot">Maybe not</choice>
<choice value="understand">I don't understand</choice>
<tip display="yes">Great!</tip>
<tip reject="maybenot">Ah, damn.</tip>
<tip reject="understand"><html><div id="test-custom-html">Really?</div></html></tip>
<tip values="yes">Great!</tip>
<tip values="maybenot">Ah, damn.</tip>
<tip values="understand"><div id="test-custom-html">Really?</div></tip>
</mcq>
<mcq name="mcq_1_2" type="rating" low="Not good at all" high="Extremely good">
<question>How much do you rate this MCQ?</question>
<rating name="mcq_1_2" low="Not good at all" high="Extremely good" question="How much do you rate this MCQ?" correct_choices="4,5">
<choice value="notwant">I don't want to rate it</choice>
<tip display="4,5">I love good grades.</tip>
<tip reject="1,2,3">Will do better next time...</tip>
<tip reject="notwant">Your loss!</tip>
</mcq>
<tip values="4,5">I love good grades.</tip>
<tip values="1,2,3">Will do better next time...</tip>
<tip values="notwant">Your loss!</tip>
</rating>
<mrq name="mrq_1_1" type="choices">
<question>What do you like in this MRQ?</question>
<mrq name="mrq_1_1" question="What do you like in this MRQ?" required_choices="gracefulness,elegance,beauty">
<choice value="elegance">Its elegance</choice>
<choice value="beauty">Its beauty</choice>
<choice value="gracefulness">Its gracefulness</choice>
<choice value="bugs">Its bugs</choice>
<tip require="gracefulness">This MRQ is indeed very graceful</tip>
<tip require="elegance,beauty">This is something everyone has to like about this MRQ</tip>
<tip reject="bugs">Nah, there isn't any!</tip>
<tip values="gracefulness">This MRQ is indeed very graceful</tip>
<tip values="elegance,beauty">This is something everyone has to like about this MRQ</tip>
<tip values="bugs">Nah, there isn't any!</tip>
</mrq>
</mentoring>
<vertical_demo>
<html>
<html_demo>
<p>Below are two mentoring blocks and a grade export block that
should be able to export their data as CSV.</p>
</html>
</html_demo>
<mentoring display_name="Mentoring Block 1" mode="standard">
<title>Mentoring Block 1</title>
<html>
<html_demo>
<p>Please answer the question below.</p>
</html>
</html_demo>
<answer name="goal">
<question>What is your goal?</question>
<answer name="goal" question="What is your goal?">
</answer>
</mentoring>
<mentoring display_name="Mentoring Block 1" mode="assessment">
<title>Mentoring Block 2 (Assessment)</title>
<html>
<mentoring display_name="Mentoring Block 2 (Assessment)" mode="assessment">
<html_demo>
<p>Please answer the question below.</p>
</html>
</html_demo>
<answer name="inspired">
<question>Who has inspired you the most?</question>
<answer name="inspired" question="Who has inspired you the most?">
</answer>
<answer name="meaning">
<question>What is the meaning of life?</question>
<answer name="meaning" question="What is the meaning of life?">
</answer>
</mentoring>
......
<mentoring display_name="Nav tooltip title" weight="1" mode="standard">
<title>Default Title</title>
<html>
<mentoring display_name="Default Title" weight="1" mode="standard">
<html_demo>
<p>Please answer the questions below.</p>
</html>
</html_demo>
<answer name="goal">
<question>What is your goal?</question>
<answer name="goal" question="What is your goal?">
</answer>
<mcq name="mcq_1_1" type="choices">
<question>Do you like this MCQ?</question>
<mcq name="mcq_1_1" question="Do you like this MCQ?" correct_choices="yes">
<choice value="yes">Yes</choice>
<choice value="maybenot">Maybe not</choice>
<choice value="understand">I don't understand</choice>
<tip display="yes">Great!</tip>
<tip reject="maybenot">Ah, damn.</tip>
<tip reject="understand"><html><div id="test-custom-html">Really?</div></html></tip>
<tip values="yes">Great!</tip>
<tip values="maybenot">Ah, damn.</tip>
<tip values="understand"><div id="test-custom-html">Really?</div></tip>
</mcq>
<mcq name="mcq_1_2" type="rating" low="Not good at all" high="Extremely good">
<question>How much do you rate this MCQ?</question>
<rating name="mcq_1_2" low="Not good at all" high="Extremely good" question="How much do you rate this MCQ?" correct_choices="4,5">
<choice value="notwant">I don't want to rate it</choice>
<tip values="4,5">I love good grades.</tip>
<tip values="1,2,3">Will do better next time...</tip>
<tip values="notwant">Your loss!</tip>
</rating>
<tip display="4,5">I love good grades.</tip>
<tip reject="1,2,3">Will do better next time...</tip>
<tip reject="notwant">Your loss!</tip>
</mcq>
<mrq name="mrq_1_1" type="choices">
<question>What do you like in this MRQ?</question>
<mrq name="mrq_1_1" question="What do you like in this MRQ?" required_choices="gracefulness,elegance,beauty" message="Thank you for answering!">
<choice value="elegance">Its elegance</choice>
<choice value="beauty">Its beauty</choice>
<choice value="gracefulness">Its gracefulness</choice>
<choice value="bugs">Its bugs</choice>
<tip require="gracefulness">This MRQ is indeed very graceful</tip>
<tip require="elegance,beauty">This is something everyone has to like about this MRQ</tip>
<tip reject="bugs">Nah, there isn't any!</tip>
<message type="on-submit">Thank you for answering!</message>
<tip values="gracefulness">This MRQ is indeed very graceful</tip>
<tip values="elegance,beauty">This is something everyone has to like about this MRQ</tip>
<tip values="bugs">Nah, there aren't any!</tip>
</mrq>
<message type="completed">
<html><p>Congratulations!</p></html>
<p>Congratulations!</p>
</message>
<message type="incomplete">
<html><p>Still some work to do...</p></html>
<p>Still some work to do...</p>
</message>
</mentoring>
<mentoring display_name="Nav tooltip title" weight="1" mode="standard">
<title>Default Title</title>
<html>
<mentoring display_name="Default Title" weight="1" mode="standard">
<html_demo>
<p>Please answer the questions below.</p>
</html>
</html_demo>
<mrq name="mrq_1_1" type="choices">
<question>What do you like in this MRQ?</question>
<mrq name="mrq_1_1" question="What do you like in this MRQ?" message="Thank you for answering!" required_choices="gracefulness,elegance,beauty">
<choice value="elegance">Its elegance</choice>
<choice value="beauty">Its beauty</choice>
<choice value="gracefulness">Its gracefulness</choice>
<choice value="bugs">Its bugs</choice>
<tip require="gracefulness">This MRQ is indeed very graceful</tip>
<tip require="elegance,beauty">This is something everyone has to like about this MRQ</tip>
<tip reject="bugs">Nah, there isn't any!</tip>
<message type="on-submit">Thank you for answering!</message>
<tip values="gracefulness">This MRQ is indeed very graceful</tip>
<tip values="elegance,beauty">This is something everyone has to like about this MRQ</tip>
<tip values="bugs">Nah, there aren't any!</tip>
</mrq>
<message type="completed">
<html><p>Congratulations!</p></html>
<p>Congratulations!</p>
</message>
<message type="incomplete">
<html><p>Still some work to do...</p></html>
<p>Still some work to do...</p>
</message>
</mentoring>
......@@ -21,8 +21,15 @@
# "AGPLv3". If not, see <http://www.gnu.org/licenses/>.
#
from xblock.fields import String
from xblockutils.base_test import SeleniumBaseTest
# Studio adds a url_name property to each XBlock but Workbench doesn't.
# Since we rely on it, we need to mock url_name support so it can be set via XML and
# accessed like a normal field.
from mentoring import MentoringBlock
MentoringBlock.url_name = String()
class MentoringBaseTest(SeleniumBaseTest):
module_name = __name__
......
......@@ -39,7 +39,7 @@ class AnswerBlockTest(MentoringBaseTest):
answer1_bis = mentoring.find_element_by_css_selector('textarea')
answer1_readonly = mentoring.find_element_by_css_selector('blockquote.answer.read_only')
self.assertEqual(answer1_bis.get_attribute('value'), '')
self.assertEqual(answer1_readonly.text, '')
self.assertEqual(answer1_readonly.text, 'No answer yet.')
# Another answer with the same name
mentoring = self.go_to_page('Answer Edit 1')
......@@ -89,7 +89,7 @@ class AnswerBlockTest(MentoringBaseTest):
# Check initial state
mentoring = self.go_to_page('Answer Blank Read Only')
answer = mentoring.find_element_by_css_selector('blockquote.answer.read_only')
self.assertEqual(answer.text, '')
self.assertEqual(answer.text, 'No answer yet.')
# Submit should allow to complete
submit = mentoring.find_element_by_css_selector('.submit input.input-main')
......
......@@ -91,9 +91,9 @@ class MentoringAssessmentTest(MentoringBaseTest):
@staticmethod
def question_text(number):
if number:
return "QUESTION %s" % number
return "Question %s" % number
else:
return "QUESTION"
return "Question"
def freeform_answer(self, number, mentoring, controls, text_input, result, saved_value="", last=False):
question = self.expect_question_visible(number, mentoring)
......
......@@ -68,8 +68,8 @@ class MCQBlockTest(MentoringBaseTest):
mcq1_legend = mcq1.find_element_by_css_selector('legend')
mcq2_legend = mcq2.find_element_by_css_selector('legend')
self.assertEqual(mcq1_legend.text, 'QUESTION 1\nDo you like this MCQ?')
self.assertEqual(mcq2_legend.text, 'QUESTION 2\nHow much do you rate this MCQ?')
self.assertEqual(mcq1_legend.text, 'Question 1\nDo you like this MCQ?')
self.assertEqual(mcq2_legend.text, 'Question 2\nHow do you rate this MCQ?')
mcq1_choices = mcq1.find_elements_by_css_selector('.choices .choice label')
mcq2_choices = mcq2.find_elements_by_css_selector('.rating .choice label')
......@@ -144,7 +144,7 @@ class MCQBlockTest(MentoringBaseTest):
self.assertFalse(submit.is_enabled())
mcq_legend = mcq.find_element_by_css_selector('legend')
self.assertEqual(mcq_legend.text, 'QUESTION\nWhat do you like in this MRQ?')
self.assertEqual(mcq_legend.text, 'Question\nWhat do you like in this MRQ?')
mcq_choices = mcq.find_elements_by_css_selector('.choices .choice label')
......@@ -195,7 +195,7 @@ class MCQBlockTest(MentoringBaseTest):
# this could be a list comprehension, but a bit complicated one - hence explicit loop
for choice_wrapper in questionnaire.find_elements_by_css_selector(".choice"):
choice_label = choice_wrapper.find_element_by_css_selector("label .choice-text")
result.append(choice_label.find_element_by_css_selector("div.html_child").get_attribute('innerHTML'))
result.append(choice_label.get_attribute('innerHTML'))
return result
......
......@@ -40,8 +40,8 @@ class MentoringTableBlockTest(MentoringBaseTest):
rows = table.find_elements_by_css_selector('td')
self.assertEqual(len(rows), 2)
self.assertEqual(rows[0].text, '')
self.assertEqual(rows[1].text, '')
self.assertEqual(rows[0].text, 'No answer yet.')
self.assertEqual(rows[1].text, 'No answer yet.')
# Fill the answers - they should appear in the table
mentoring = self.go_to_page('Table 1')
......
<vertical_demo>
<mentoring url_name="answer_blank_read_only" enforce_dependency="false">
<answer name="answer_blank" read_only="true" />
<answer-recap name="answer_blank" />
</mentoring>
</vertical_demo>
<vertical_demo>
<mentoring url_name="answer_edit_1" enforce_dependency="false">
<html>
<html_demo>
<p>This should be displayed in the answer_edit scenario</p>
</html>
</html_demo>
<answer name="answer_1" />
</mentoring>
......
<vertical_demo>
<mentoring url_name="answer_edit_2" enforce_dependency="false">
<answer name="answer_1" read_only="true" />
<answer-recap name="answer_1" />
<answer name="answer_1" />
</mentoring>
</vertical_demo>
<mentoring url_name="mentoring-assessment-1" display_name="Nav tooltip title" weight="1" mode="assessment" max_attempts="2">
<title>A Simple Assessment</title>
<shared-header>
<mentoring url_name="mentoring-assessment-1" display_name="A Simple Assessment" weight="1" mode="assessment" max_attempts="2">
<html_demo>
<p>This paragraph is shared between <strong>all</strong> questions.</p>
</shared-header>
<html>
<p>Please answer the questions below.</p>
</html>
</html_demo>
<answer name="goal">
<question>What is your goal?</question>
</answer>
<answer name="goal" question="What is your goal?" />
<mcq name="mcq_1_1" type="choices">
<question>Do you like this MCQ?</question>
<mcq name="mcq_1_1" question="Do you like this MCQ?" correct_choices="yes">
<choice value="yes">Yes</choice>
<choice value="maybenot">Maybe not</choice>
<choice value="understand">I don't understand</choice>
<tip display="yes">Great!</tip>
<tip reject="maybenot">Ah, damn.</tip>
<tip reject="understand"><html><div id="test-custom-html">Really?</div></html></tip>
<tip values="yes">Great!</tip>
<tip values="maybenot">Ah, damn.</tip>
<tip values="understand"><div id="test-custom-html">Really?</div></tip>
</mcq>
<mcq name="mcq_1_2" type="rating" low="Not good at all" high="Extremely good">
<question>How much do you rate this MCQ?</question>
<rating name="mcq_1_2" low="Not good at all" high="Extremely good" question="How much do you rate this MCQ?" correct_choices="4,5">
<choice value="notwant">I don't want to rate it</choice>
<tip display="4,5">I love good grades.</tip>
<tip reject="1,2,3">Will do better next time...</tip>
<tip reject="notwant">Your loss!</tip>
</mcq>
<tip values="4,5">I love good grades.</tip>
<tip values="1,2,3">Will do better next time...</tip>
<tip values="notwant">Your loss!</tip>
</rating>
<mrq name="mrq_1_1" type="choices">
<question>What do you like in this MRQ?</question>
<mrq name="mrq_1_1" question="What do you like in this MRQ?" required_choices="gracefulness,elegance,beauty">
<choice value="elegance">Its elegance</choice>
<choice value="beauty">Its beauty</choice>
<choice value="gracefulness">Its gracefulness</choice>
<choice value="bugs">Its bugs</choice>
<tip require="gracefulness">This MRQ is indeed very graceful</tip>
<tip require="elegance,beauty">This is something everyone has to like about this MRQ</tip>
<tip reject="bugs">Nah, there isn't any!</tip>
<tip values="gracefulness">This MRQ is indeed very graceful</tip>
<tip values="elegance,beauty">This is something everyone has to like about this MRQ</tip>
<tip values="bugs">Nah, there isn't any!</tip>
</mrq>
</mentoring>
<mentoring url_name="mentoring-assessment-2" display_name="Nav tooltip title" weight="1" mode="assessment" max_attempts="2">
<title>A Simple Assessment</title>
<shared-header>
<mentoring url_name="mentoring-assessment-2" display_name="A Simple Assessment" weight="1" mode="assessment" max_attempts="2">
<html_demo>
<p>This paragraph is shared between <strong>all</strong> questions.</p>
</shared-header>
<html>
<p>Please answer the questions below.</p>
</html>
</html_demo>
<mcq name="mcq_1_1" type="choices">
<question>Do you like this MCQ?</question>
<mcq name="mcq_1_1" question="Do you like this MCQ?" correct_choices="yes">
<choice value="yes">Yes</choice>
<choice value="maybenot">Maybe not</choice>
<choice value="understand">I don't understand</choice>
<tip display="yes">Great!</tip>
<tip reject="maybenot">Ah, damn.</tip>
<tip reject="understand"><html><div id="test-custom-html">Really?</div></html></tip>
<tip values="yes">Great!</tip>
<tip values="maybenot">Ah, damn.</tip>
<tip values="understand"><div id="test-custom-html">Really?</div></tip>
</mcq>
</mentoring>
<vertical_demo>
<html>
<html_demo>
<p>Below are two mentoring blocks and a grade export block that
should be able to export their data as CSV.</p>
</html>
</html_demo>
<mentoring display_name="Mentoring Block 1" mode="standard">
<title>Mentoring Block 1</title>
<html>
<html_demo>
<p>Please answer the question below.</p>
</html>
</html_demo>
<answer name="goal">
<question>What is your goal?</question>
</answer>
<answer name="goal" question="What is your goal?" />
</mentoring>
<mentoring display_name="Mentoring Block 1" mode="assessment">
<title>Mentoring Block 2 (Assessment)</title>
<html>
<mentoring display_name="Mentoring Block 2 (Assessment)" mode="assessment">
<html_demo>
<p>Please answer the question below.</p>
</html>
</html_demo>
<answer name="inspired">
<question>Who has inspired you the most?</question>
</answer>
<answer name="meaning">
<question>What is the meaning of life?</question>
</answer>
<answer name="inspired" question="Who has inspired you the most?"/>
<answer name="meaning" question="What is the meaning of life?" />
</mentoring>
<mentoring-dataexport display_name="Data Export Test" />
......
<vertical_demo>
<mentoring url_name="mcq_1" enforce_dependency="false">
<mcq name="mcq_1_1" type="choices">
<question>Do you like this MCQ?</question>
<mcq name="mcq_1_1" question="Do you like this MCQ?" correct_choices="yes">
<choice value="yes">Yes</choice>
<choice value="maybenot">Maybe not</choice>
<choice value="understand">I don't understand</choice>
<tip display="yes">Great!</tip>
<tip reject="maybenot">Ah, damn.</tip>
<tip reject="understand"><html><div id="test-custom-html">Really?</div></html></tip>
<tip values="yes">Great!</tip>
<tip values="maybenot">Ah, damn.</tip>
<tip values="understand"><div id="test-custom-html">Really?</div></tip>
</mcq>
<mcq name="mcq_1_2" type="rating" low="Not good at all" high="Extremely good">
<question>How much do you rate this MCQ?</question>
<rating name="mcq_1_2" low="Not good at all" high="Extremely good" question="How do you rate this MCQ?" correct_choices="4,5">
<choice value="notwant">I don't want to rate it</choice>
<tip display="4,5">I love good grades.</tip>
<tip reject="1,2,3">Will do better next time...</tip>
<tip reject="notwant">Your loss!</tip>
</mcq>
<tip values="4,5">I love good grades.</tip>
<tip values="1,2,3">Will do better next time...</tip>
<tip values="notwant">Your loss!</tip>
</rating>
<message type="completed">
All is good now...
<html><p>Congratulations!</p></html>
<p>Congratulations!</p>
</message>
</mentoring>
</vertical_demo>
<vertical_demo>
<mentoring url_name="mcq_with_comments" display_name="MRQ Exercise 7" weight="1" enforce_dependency="false">
<title>MRQ With Resizable popups</title>
<mrq name="mrq_1_1_7" type="choices">
<question>What do you like in this MRQ?</question>
<mentoring url_name="mcq_with_comments" display_name="MRQ With Resizable popups" weight="1" enforce_dependency="false">
<mrq name="mrq_1_1_7" question="What do you like in this MRQ?" required_choices="elegance,gracefulness,beauty">
<choice value="elegance">Its elegance</choice>
<choice value="beauty">Its beauty</choice>
<choice value="gracefulness">Its gracefulness</choice>
<choice value="bugs">Its bugs</choice>
<tip require="gracefulness" width ="200" height = "200">This MRQ is indeed very graceful</tip>
<tip require="elegance" width ="600" height = "800">This is something everyone has to like about this MRQ</tip>
<tip require="beauty" width ="400" height = "600">This is something everyone has to like about beauty</tip>
<tip reject="bugs" width = "100" height = "200">Nah, there isn\'t any!</tip>
<tip values="gracefulness" width ="200" height = "200">This MRQ is indeed very graceful</tip>
<tip values="elegance" width ="600" height = "800">This is something everyone has to like about this MRQ</tip>
<tip values="beauty" width ="400" height = "600">This is something everyone has to like about beauty</tip>
<tip values="bugs" width = "100" height = "200">Nah, there isn\'t any!</tip>
<!--<message type="on-submit">Thank you for answering!</message> -->
<!--<message type="on-submit">This is deliberately commented out to test parsing of XML comments</message> -->
</mrq>
<message type="completed">
<html><p>Congratulations!</p></html>
<p>Congratulations!</p>
</message>
<message type="incomplete">
<html><p>Still some work to do...</p></html>
<p>Still some work to do...</p>
</message>
</mentoring>
</vertical_demo>
<vertical_demo>
<mentoring url_name="mcq_with_comments" display_name="MCQ Exercise 7" weight="1" enforce_dependency="false">
<title>MRQ With Resizable popups</title>
<mcq name="mrq_1_1_7" type="choices">
<question>What do you like in this MRQ?</question>
<choice value="elegance"><html><b>Its elegance</b></html></choice>
<choice value="beauty"><html><i>Its beauty</i></html></choice>
<choice value="gracefulness"><html><strong>Its gracefulness</strong></html></choice>
<choice value="bugs"><html><span style="font-color:red">Its bugs</span></html></choice>
<mentoring url_name="mcq_with_comments" display_name="MCQ With Resizable popups" weight="1" enforce_dependency="false">
<mcq name="mrq_1_1_7" question="What do you like in this MCQ?" correct_choices="gracefulness,elegance,beauty">
<choice value="elegance"><b>Its elegance</b></choice>
<choice value="beauty"><i>Its beauty</i></choice>
<choice value="gracefulness"><strong>Its gracefulness</strong></choice>
<choice value="bugs"><span style="font-color:red">Its bugs</span></choice>
<tip require="gracefulness" width ="200" height = "200">This MRQ is indeed very graceful</tip>
<tip require="elegance" width ="600" height = "800">This is something everyone has to like about this MRQ</tip>
<tip require="beauty" width ="400" height = "600">This is something everyone has to like about beauty</tip>
<tip reject="bugs" width = "100" height = "200">Nah, there isn\'t any!</tip>
<tip values="gracefulness" width ="200" height = "200">This MCQ is indeed very graceful</tip>
<tip values="elegance" width ="600" height = "800">This is something everyone has to like about this MCQ</tip>
<tip values="beauty" width ="400" height = "600">This is something everyone has to like about beauty</tip>
<tip values="bugs" width = "100" height = "200">Nah, there isn\'t any!</tip>
</mcq>
<message type="completed">
<html><p>Congratulations!</p></html>
<p>Congratulations!</p>
</message>
<message type="incomplete">
<html><p>Still some work to do...</p></html>
<p>Still some work to do...</p>
</message>
</mentoring>
</vertical_demo>
<vertical_demo>
<mentoring url_name="mcq_with_comments" display_name="MRQ Exercise 7" weight="1" enforce_dependency="false">
<title>MRQ With Resizable popups</title>
<mrq name="mrq_1_1_7" type="choices">
<question>What do you like in this MRQ?</question>
<choice value="elegance"><html><b>Its elegance</b></html></choice>
<choice value="beauty"><html><i>Its beauty</i></html></choice>
<choice value="gracefulness"><html><strong>Its gracefulness</strong></html></choice>
<choice value="bugs"><html><span style="font-color:red">Its bugs</span></html></choice>
<mentoring url_name="mcq_with_comments" display_name="MRQ With Resizable popups" weight="1" enforce_dependency="false">
<mrq name="mrq_1_1_7" question="What do you like in this MRQ?" required_choices="elegance,beauty,gracefulness">
<choice value="elegance"><b>Its elegance</b></choice>
<choice value="beauty"><i>Its beauty</i></choice>
<choice value="gracefulness"><strong>Its gracefulness</strong></choice>
<choice value="bugs"><span style="font-color:red">Its bugs</span></choice>
<tip require="gracefulness" width ="200" height = "200">This MRQ is indeed very graceful</tip>
<tip require="elegance" width ="600" height = "800">This is something everyone has to like about this MRQ</tip>
<tip require="beauty" width ="400" height = "600">This is something everyone has to like about beauty</tip>
<tip reject="bugs" width = "100" height = "200">Nah, there isn\'t any!</tip>
<tip values="gracefulness" width ="200" height = "200">This MRQ is indeed very graceful</tip>
<tip values="elegance" width ="600" height = "800">This is something everyone has to like about this MRQ</tip>
<tip values="beauty" width ="400" height = "600">This is something everyone has to like about beauty</tip>
<tip values="bugs" width = "100" height = "200">Nah, there aren\'t any!</tip>
</mrq>
<message type="completed">
<html><p>Congratulations!</p></html>
<p>Congratulations!</p>
</message>
<message type="incomplete">
<html><p>Still some work to do...</p></html>
<p>Still some work to do...</p>
</message>
</mentoring>
</vertical_demo>
<vertical_demo>
<mentoring url_name="answer_blank_read_only" enforce_dependency="false" display_submit="false">
<mentoring url_name="answer_no_display_submit" enforce_dependency="false" display_submit="false">
<answer name="answer_blank"/>
</mentoring>
</vertical_demo>
<vertical_demo>
<mentoring url_name="table_1" enforce_dependency="false">
<mentoring enforce_dependency="false">
<answer name="table_1_answer_1" />
<answer name="table_1_answer_2" />
</mentoring>
......
<vertical_demo>
<mentoring display_submit="false" enforce_dependency="false">
<mentoring-table type="table_test" url_name="table_2">
<column>
<header>Header Test 1</header>
<answer name="table_1_answer_1" />
<mentoring-table>
<column header="Header Test 1">
<answer-recap name="table_1_answer_1"/>
</column>
<column>
<header>Header Test 2</header>
<answer name="table_1_answer_2" />
<column header="Header Test 2">
<answer-recap name="table_1_answer_2"/>
</column>
</mentoring-table>
</mentoring>
......
"""
The mentoring XBlock was previously designed to be edited using XML.
This file contains a hack necessary for us to parse the old XML data and create blocks in Studio
from the parsed XML, as part of the upgrade process.
It works by parsing the XML and creating XBlocks in a temporary runtime
environment, so that the blocks' fields can be read and copied into Studio.
"""
from xblock.fields import Scope
from xblock.runtime import Runtime, DictKeyValueStore, KvsFieldData, MemoryIdManager, ScopeIds
class TransientRuntime(Runtime):
"""
An XBlock runtime designed to have no persistence and no ability to render views/handlers.
"""
def __init__(self):
id_manager = MemoryIdManager()
field_data = KvsFieldData(DictKeyValueStore())
super(TransientRuntime, self).__init__(
id_reader=id_manager,
id_generator=id_manager,
field_data=field_data,
)
def create_block_from_node(self, node):
"""
Parse an XML node representing an XBlock (and children), and return the XBlock.
"""
block_type = node.tag
def_id = self.id_generator.create_definition(block_type)
usage_id = self.id_generator.create_usage(def_id)
keys = ScopeIds(None, block_type, def_id, usage_id)
block_class = self.mixologist.mix(self.load_block_type(block_type))
block = block_class.parse_xml(node, self, keys, self.id_generator)
block.save()
return block
def handler_url(self, *args, **kwargs):
raise NotImplementedError("TransientRuntime does not support handler_url.")
def local_resource_url(self, *args, **kwargs):
raise NotImplementedError("TransientRuntime does not support local_resource_url.")
def publish(self, *args, **kwargs):
raise NotImplementedError("TransientRuntime does not support publish.")
def resource_url(self, *args, **kwargs):
raise NotImplementedError("TransientRuntime does not support resource_url.")
def render_template(self, *args, **kwargs):
raise NotImplementedError("TransientRuntime cannot render templates.")
def studio_update_from_node(block, node):
"""
Given an XBlock that is using the edX Studio runtime, replace all of block's fields and
children with the fields and children defined by the XML node 'node'.
"""
user_id = block.runtime.user_id
temp_runtime = TransientRuntime()
source_block = temp_runtime.create_block_from_node(node)
def update_from_temp_block(real_block, temp_block):
"""
Recursively copy all fields and children from temp_block to real_block.
"""
# Fields:
for field_name, field in temp_block.fields.iteritems():
if field.scope in (Scope.content, Scope.settings) and field.is_set_on(temp_block):
setattr(real_block, field_name, getattr(temp_block, field_name))
# Children:
if real_block.has_children:
real_block.children = []
for child_id in temp_block.children:
child = temp_block.runtime.get_block(child_id)
new_child = real_block.runtime.modulestore.create_item(
user_id, real_block.location.course_key, child.scope_ids.block_type
)
update_from_temp_block(new_child, child)
real_block.children.append(new_child.location)
real_block.save()
real_block.runtime.modulestore.update_item(real_block, user_id)
with block.runtime.modulestore.bulk_operations(block.location.course_key):
for child_id in block.children:
block.runtime.modulestore.delete_item(child_id, user_id)
update_from_temp_block(block, source_block)
"""
Test that we can upgrade from mentoring v1 to mentoring v2.
"""
import ddt
from lxml import etree
from mentoring.v1.xml_changes import convert_xml_v1_to_v2
import os.path
from StringIO import StringIO
import unittest
from sample_xblocks.basic.content import HtmlBlock
from xblock.core import XBlock
from xblock.fields import ScopeIds
from xblock.runtime import DictKeyValueStore, KvsFieldData
from xblock.test.tools import TestRuntime
xml_path = os.path.join(os.path.dirname(__file__), "xml")
@ddt.ddt
class TestUpgrade(unittest.TestCase):
"""
Test upgrade from mentoring v1 (which uses xml_content even in Studio) to v2.
We can't test the steps that depend on Studio, so we just test the XML conversion.
"""
def setUp(self):
self.runtime = TestRuntime(field_data=KvsFieldData(DictKeyValueStore()))
@ddt.data(
"v1_upgrade_a",
"v1_upgrade_b",
"v1_upgrade_c",
)
@XBlock.register_temp_plugin(HtmlBlock, "html")
def test_xml_upgrade(self, file_name):
"""
Convert a v1 mentoring block to v2 and then compare the resulting block to a
pre-converted one.
"""
with open("{}/{}_old.xml".format(xml_path, file_name)) as xmlfile:
temp_node = etree.parse(xmlfile).getroot()
old_block = self.create_block_from_node(temp_node)
parser = etree.XMLParser(remove_blank_text=True)
xml_root = etree.parse(StringIO(old_block.xml_content), parser=parser).getroot()
convert_xml_v1_to_v2(xml_root)
converted_block = self.create_block_from_node(xml_root)
with open("{}/{}_new.xml".format(xml_path, file_name)) as xmlfile:
temp_node = etree.parse(xmlfile).getroot()
new_block = self.create_block_from_node(temp_node)
try:
self.assertBlocksAreEquivalent(converted_block, new_block)
except AssertionError:
xml_result = etree.tostring(xml_root, pretty_print=True, encoding="UTF-8")
print("Converted XML:\n{}".format(xml_result))
raise
def create_block_from_node(self, node):
"""
Parse an XML node representing an XBlock (and children), and return the XBlock.
"""
block_type = node.tag
def_id = self.runtime.id_generator.create_definition(block_type)
usage_id = self.runtime.id_generator.create_usage(def_id)
keys = ScopeIds(None, block_type, def_id, usage_id)
block_class = self.runtime.mixologist.mix(self.runtime.load_block_type(block_type))
block = block_class.parse_xml(node, self.runtime, keys, self.runtime.id_generator)
block.save()
return block
def assertBlocksAreEquivalent(self, block1, block2):
"""
Compare two blocks for equivalence.
Borrowed from xblock.test.tools.blocks_are_equivalent but modified to use assertions.
"""
# The two blocks have to be the same class.
self.assertEqual(block1.__class__, block2.__class__)
# They have to have the same fields.
self.assertEqual(set(block1.fields), set(block2.fields))
# The data fields have to have the same values.
for field_name in block1.fields:
if field_name in ('parent', 'children'):
continue
if field_name == "content":
# Inner HTML/XML content may have varying whitespace which we don't care about:
self.assertEqual(
self.clean_html(getattr(block1, field_name)),
self.clean_html(getattr(block2, field_name))
)
else:
self.assertEqual(getattr(block1, field_name), getattr(block2, field_name))
# The children need to be equal.
self.assertEqual(block1.has_children, block2.has_children)
if block1.has_children:
self.assertEqual(len(block1.children), len(block2.children))
for child_id1, child_id2 in zip(block1.children, block2.children):
# Load up the actual children to see if they are equal.
child1 = block1.runtime.get_block(child_id1)
child2 = block2.runtime.get_block(child_id2)
self.assertBlocksAreEquivalent(child1, child2)
def clean_html(self, html_str):
"""
Standardize the given HTML string for a consistent comparison.
Assumes the HTML is valid XML.
"""
# We wrap it in <x></x> so that the given HTML string doesn't need a single root element.
parser = etree.XMLParser(remove_blank_text=True)
parsed = etree.parse(StringIO(u"<x>{}</x>".format(html_str)), parser=parser).getroot()
return etree.tostring(parsed, pretty_print=False, encoding="UTF-8")[3:-3]
<mentoring url_name="some_url_name" weight="1" mode="standard" display_name="Default Title">
<html>
<p>This paragraph is shared between <strong>all</strong> questions.</p>
</html>
<html>
<p>Please answer the questions below.</p>
</html>
<answer name="goal" question="What is your goal?"/>
<mcq name="mcq_1_1" question="Do you like this MCQ?" correct_choices="yes">
<choice value="yes">Yes</choice>
<choice value="maybenot">Maybe not</choice>
<choice value="understand">I don't understand</choice>
<tip values="yes">Great!</tip>
<tip values="maybenot">Ah, damn.</tip>
<tip values="understand">
<div id="test-custom-html">Really?</div>
</tip>
</mcq>
<rating name="mcq_1_2" low="Not good at all" high="Extremely good" question="How much do you rate this MCQ?" correct_choices="4,5">
<choice value="notwant">I don't want to rate it</choice>
<tip values="4,5">I love good grades.</tip>
<tip values="1,2,3">Will do better next time...</tip>
<tip values="notwant">Your loss!</tip>
</rating>
<mrq name="mrq_1_1" question="What do you like in this MRQ?" message="Thank you for answering!" required_choices="gracefulness,elegance,beauty">
<choice value="elegance">Its elegance</choice>
<choice value="beauty">Its beauty</choice>
<choice value="gracefulness">Its gracefulness</choice>
<choice value="bugs">Its bugs</choice>
<tip values="gracefulness">This MRQ is indeed very graceful</tip>
<tip values="elegance,beauty">This is something everyone has to like about this MRQ</tip>
<tip values="bugs">Nah, there isn't any!</tip>
</mrq>
<message type="completed">
<p>Congratulations!</p>
</message>
<message type="incomplete">
<p>Still some work to do...</p>
</message>
</mentoring>
<?xml version='1.0' encoding='utf-8'?>
<!--
This contains the old version of mentoring_default.xml (using the v1 schema)
Changes from the original:
- a <shared-header> was added to test that migration.
- the display_name was removed to avoid the warning about overwriting a display_name
since v2 only supports display_name rather than separate "title" and "display_name"
-->
<mentoring xmlns:option="http://code.edx.org/xblock/option">
<option:xml_content>
<![CDATA[
<mentoring url_name="some_url_name" weight="1" mode="standard">
<title>Default Title</title>
<shared-header>
<p>This paragraph is shared between <strong>all</strong> questions.</p>
</shared-header>
<html>
<p>Please answer the questions below.</p>
</html>
<answer name="goal">
<question>What is your goal?</question>
</answer>
<mcq name="mcq_1_1" type="choices">
<question>Do you like this MCQ?</question>
<choice value="yes">Yes</choice>
<choice value="maybenot">Maybe not</choice>
<choice value="understand">I don't understand</choice>
<tip display="yes">Great!</tip>
<tip reject="maybenot">Ah, damn.</tip>
<tip reject="understand"><html><div id="test-custom-html">Really?</div></html></tip>
</mcq>
<mcq name="mcq_1_2" type="rating" low="Not good at all" high="Extremely good">
<question>How much do you rate this MCQ?</question>
<choice value="notwant">I don't want to rate it</choice>
<tip display="4,5">I love good grades.</tip>
<tip reject="1,2,3">Will do better next time...</tip>
<tip reject="notwant">Your loss!</tip>
</mcq>
<mrq name="mrq_1_1" type="choices">
<question>What do you like in this MRQ?</question>
<choice value="elegance">Its elegance</choice>
<choice value="beauty">Its beauty</choice>
<choice value="gracefulness">Its gracefulness</choice>
<choice value="bugs">Its bugs</choice>
<tip require="gracefulness">This MRQ is indeed very graceful</tip>
<tip require="elegance,beauty">This is something everyone has to like about this MRQ</tip>
<tip reject="bugs">Nah, there isn't any!</tip>
<message type="on-submit">Thank you for answering!</message>
</mrq>
<message type="completed">
<html><p>Congratulations!</p></html>
</message>
<message type="incomplete">
<html><p>Still some work to do...</p></html>
</message>
</mentoring>
]]>
</option:xml_content>
</mentoring>
<mentoring enforce_dependency="false" followed_by="past_attempts">
<html>
<h3>Checking your improvement frog</h3>
<p>Now, let's make sure your frog meets the criteria for a strong column 1. Here is your frog:</p>
</html>
<answer-recap name="improvement-frog"/>
<mcq name="frog-happy" question="Is this frog happy for you?" correct_choices="yes,maybenot,understand">
<choice value="yes">Yes</choice>
<choice value="maybenot">Maybe not</choice>
<choice value="understand">I don't understand</choice>
<tip values="yes">Great. Your frog should be happy for you.</tip>
<tip values="maybenot">In the end, all the feedback you have gotten from others should not lead you to choose a frog that does not also feel happy and important to you.</tip>
<tip values="understand">
<p>If a frog is <span class="italic">happy for you</span>, that means it is a frog that you genuinely feel in your own heart to be something that you want to improve. What is in your heart?</p>
</tip>
</mcq>
<mcq name="frog-implicate" question="Does this frog implicate you?" correct_choices="yes,maybenot,understand">
<choice value="yes">Yes</choice>
<choice value="maybenot">Maybe not</choice>
<choice value="understand">I don't understand</choice>
<tip values="yes">Great. Your frog should implicate you.</tip>
<tip values="maybenot">
<p>Since the Trial of Uruk-Shan focuses on your own growth and change, it is important to be clear about the ways <span class="bold">you</span> are hoping to change and improve.</p>
</tip>
<tip values="understand">Your frog implicates you if it is clear that you must get better at something. Your frog should focus on something you can control.</tip>
</mcq>
<rating name="frog-important" low="Not at all important to me" high="Very important to me" question="How important is it to you?" correct_choices="4,5,1,2,3,understand">
<choice value="understand">I don't understand</choice>
<tip values="4,5">Great!</tip>
<tip values="1,2,3">The Trial of Uruk-Shan helps you uncover some of the core beliefs and assumptions you have held that are preventing you from making change.</tip>
<tip values="understand">A frog is important if it is one that could make a big difference in helping you reach your frogs in your work life or your personal life (or both).</tip>
</rating>
<message type="completed">
Great! You have indicated that you have chosen a frog that is happy for you, implicates you, has room for improvement, and is important to you. You are now ready to move onto the next step.
</message>
</mentoring>
<?xml version='1.0' encoding='utf-8'?>
<!--
This contains a typical problem taken from a live course (content changed)
-->
<mentoring xmlns:option="http://code.edx.org/xblock/option">
<option:xml_content>
<![CDATA[
<mentoring enforce_dependency="false" followed_by="past_attempts">
<html>
<h3>Checking your improvement frog</h3>
<p>Now, let's make sure your frog meets the criteria for a strong column 1. Here is your frog:</p>
</html>
<answer name="improvement-frog" read_only="true"/>
<quizz name="frog-happy" type="choices">
<question>Is this frog happy for you?</question>
<choice value="yes">Yes</choice>
<choice value="maybenot">Maybe not</choice>
<choice value="understand">I don't understand</choice>
<tip display="yes">Great. Your frog should be happy for you.</tip>
<tip display="maybenot">In the end, all the feedback you have gotten from others should not lead you to choose a frog that does not also feel happy and important to you.</tip>
<tip display="understand">
<html>
<p>If a frog is <span class="italic">happy for you</span>, that means it is a frog that you genuinely feel in your own heart to be something that you want to improve. What is in your heart?</p>
</html>
</tip>
</quizz>
<quizz name="frog-implicate" type="choices">
<question>Does this frog implicate you?</question>
<choice value="yes">Yes</choice>
<choice value="maybenot">Maybe not</choice>
<choice value="understand">I don't understand</choice>
<tip display="yes">Great. Your frog should implicate you.</tip>
<tip display="maybenot">
<html>
<p>Since the Trial of Uruk-Shan focuses on your own growth and change, it is important to be clear about the ways <span class="bold">you</span> are hoping to change and improve.</p>
</html>
</tip>
<tip display="understand">Your frog implicates you if it is clear that you must get better at something. Your frog should focus on something you can control.</tip>
</quizz>
<quizz name="frog-important" type="rating" low="Not at all important to me" high="Very important to me">
<question>How important is it to you?</question>
<choice value="understand">I don't understand</choice>
<tip display="4,5">Great!</tip>
<tip display="1,2,3">The Trial of Uruk-Shan helps you uncover some of the core beliefs and assumptions you have held that are preventing you from making change.</tip>
<tip display="understand">A frog is important if it is one that could make a big difference in helping you reach your frogs in your work life or your personal life (or both).</tip>
</quizz>
<message type="completed">
Great! You have indicated that you have chosen a frog that is happy for you, implicates you, has room for improvement, and is important to you. You are now ready to move onto the next step.
</message>
</mentoring>
]]>
</option:xml_content>
</mentoring>
<mentoring display_submit="false" enforce_dependency="false">
<mentoring-table type="table_test" url_name="table_2">
<column header="Header Test 1">
<answer-recap name="table_1_answer_1" />
</column>
<column header="Header &lt;strong&gt;Test 2&lt;/strong&gt;">
<answer-recap name="table_1_answer_2" />
<html><p>Inline HTML</p></html>
<answer-recap name="table_1_answer_2" />
</column>
</mentoring-table>
</mentoring>
<?xml version='1.0' encoding='utf-8'?>
<!--
This contains a table to test migration of tables from v1 schema to v2.
-->
<mentoring xmlns:option="http://code.edx.org/xblock/option">
<option:xml_content>
<![CDATA[
<mentoring display_submit="false" enforce_dependency="false">
<mentoring-table type="table_test" url_name="table_2">
<column>
<header>Header Test 1</header>
<answer name="table_1_answer_1" />
</column>
<column>
<header><html>Header <strong>Test 2</strong></html></header>
<answer name="table_1_answer_2" />
<html><p>Inline HTML</p></html>
<answer name="table_1_answer_2" />
</column>
</mentoring-table>
</mentoring>
]]>
</option:xml_content>
</mentoring>
# -*- coding: utf-8 -*-
"""
The mentoring XBlock was previously designed to be edited using XML.
This file contains a script to help migrate mentoring blocks to the new format which is
optimized for editing in Studio.
To run the script on devstack:
SERVICE_VARIANT=cms DJANGO_SETTINGS_MODULE="cms.envs.devstack" python -m mentoring.v1.upgrade
"""
import logging
from lxml import etree
from mentoring import MentoringBlock
from StringIO import StringIO
import sys
from .studio_xml_utils import studio_update_from_node
from .xml_changes import convert_xml_v1_to_v2
def upgrade_block(block):
"""
Given a MentoringBlock "block" with old-style (v1) data in its "xml_content" field, parse
the XML and re-create the block with new-style (v2) children and settings.
"""
assert isinstance(block, MentoringBlock)
assert bool(block.xml_content) # If it's a v1 block it will have xml_content
xml_content_str = block.xml_content
parser = etree.XMLParser(remove_blank_text=True)
root = etree.parse(StringIO(xml_content_str), parser=parser).getroot()
assert root.tag == "mentoring"
convert_xml_v1_to_v2(root)
# We need some special-case handling to deal with HTML being an XModule and not a pure XBlock:
try:
from xmodule.html_module import HtmlDescriptor
except ImportError:
pass # Perhaps HtmlModule has been converted to an XBlock?
else:
@classmethod
def parse_xml_for_HtmlDescriptor(cls, node, runtime, keys, id_generator):
block = runtime.construct_xblock_from_class(cls, keys)
block.data = node.text if node.text else ""
for child in list(node):
if isinstance(child.tag, basestring):
block.data += etree.tostring(child)
return block
HtmlDescriptor.parse_xml = parse_xml_for_HtmlDescriptor
# Save the xml_content to make this processes rerunnable, in case it doesn't work correctly the first time.
root.attrib["xml_content"] = xml_content_str
# Replace block with the new version and the new children:
studio_update_from_node(block, root)
if __name__ == '__main__':
# Disable some distracting overly-verbose warnings that we don't need:
for noisy_module in ('edx.modulestore', 'elasticsearch', 'urllib3.connectionpool'):
logging.getLogger(noisy_module).setLevel(logging.ERROR)
from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore.django import modulestore
print("┏━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
print("┃ Mentoring Upgrade Script ┃")
print("┗━━━━━━━━━━━━━━━━━━━━━━━━━━┛")
try:
course_id = sys.argv[1]
except IndexError:
sys.exit("Need a course ID argument like 'HarvardX/GSE1.1x/3T2014' or 'course-v1:HarvardX+B101+2015'")
store = modulestore()
course = store.get_course(CourseKey.from_string(course_id))
print(" ➔ Found course: {}".format(course.display_name))
print(" ➔ Searching for mentoring blocks")
blocks_found = []
def find_mentoring_blocks(block):
if isinstance(block, MentoringBlock):
blocks_found.append(block.scope_ids.usage_id)
elif block.has_children:
for child_id in block.children:
find_mentoring_blocks(block.runtime.get_block(child_id))
find_mentoring_blocks(course)
total = len(blocks_found)
print(" ➔ Found {} mentoring blocks".format(total))
with store.bulk_operations(course.location.course_key):
count = 1
for block_id in blocks_found:
block = course.runtime.get_block(block_id)
print(" ➔ Upgrading block {} of {} - \"{}\"".format(count, total, block.url_name))
count += 1
upgrade_block(block)
print(" ➔ Complete.")
# -*- coding: utf-8 -*-
"""
Each class in this file represents a change made to the XML schema between v1 and v2.
"""
from lxml import etree
import warnings
class Change(object):
@staticmethod
def applies_to(node):
"""
Does this change affect the given XML node?
n.b. prior Changes will already be applied to the node.
"""
raise NotImplementedError
def __init__(self, node):
"""
Prepare to upgrade 'node' at some point in the future
"""
self.node = node
def apply(self):
raise NotImplementedError
class RemoveTitle(Change):
""" The old <title> element is now an attribute of <mentoring> """
@staticmethod
def applies_to(node):
return node.tag == "title" and node.getparent().tag == "mentoring"
def apply(self):
title = self.node.text.strip()
p = self.node.getparent()
old_display_name = p.get("display_name")
if old_display_name and old_display_name != title:
warnings.warn('Replacing display_name="{}" with <title> value "{}"'.format(p.attrib["display_name"], title))
p.attrib["display_name"] = title
p.remove(self.node)
class UnwrapHTML(Change):
""" <choice>,<tip>, <header>, and <message> now contain HTML without an explicit <html> wrapper. """
@staticmethod
def applies_to(node):
return node.tag == "html" and node.getparent().tag in ("choice", "tip", "message", "header")
def apply(self):
p = self.node.getparent()
if self.node.text:
p.text = (p.text if p.text else u"") + self.node.text
index = list(p).index(self.node)
for child in list(self.node):
index += 1
p.insert(index, child)
p.remove(self.node)
class TableColumnHeader(Change):
"""
Replace:
<mentoring-table>
<column>
<header>Answer 1</header>
<answer name="answer_1" />
</column>
</mentoring-table>
with
<mentoring-table>
<column header="Answer 1">
<answer-recap name="answer_1" />
</column>
</mentoring-table>
"""
@staticmethod
def applies_to(node):
return node.tag == "column" and node.getparent().tag == "mentoring-table"
def apply(self):
header_html = u""
to_remove = []
for child in list(self.node):
if child.tag == "header":
if child.text:
header_html += child.text
for grandchild in list(child):
header_html += etree.tostring(grandchild)
to_remove.append(child)
elif child.tag == "answer":
child.tag = "answer-recap"
if "read_only" in child.attrib:
del child.attrib["read_only"]
elif child.tag != "html":
warnings.warn("Invalid <column> element: Unexpected <{}>".format(child.tag))
return
for child in to_remove:
self.node.remove(child)
self.node.text = None
if header_html:
self.node.attrib["header"] = header_html
class QuizzToMCQ(Change):
""" <quizz> element was an alias for <mcq>. In v2 we only have <mcq> """
@staticmethod
def applies_to(node):
return node.tag == "quizz"
def apply(self):
self.node.tag = "mcq"
class MCQToRating(Change):
""" <mcq type="rating"> is now just <rating>, and we never use type="choices" on MCQ/MRQ """
@staticmethod
def applies_to(node):
return node.tag in ("mcq", "mrq") and "type" in node.attrib
def apply(self):
if self.node.tag == "mcq" and self.node.get("type") == "rating":
self.node.tag = "rating"
self.node.attrib.pop("type") # Type attribute is no longer used.
class ReadOnlyAnswerToRecap(Change):
""" <answer read_only="true"> is now <answer-recap/> """
@staticmethod
def applies_to(node):
return node.tag == "answer" and node.get("read_only") == "true"
def apply(self):
self.node.tag = "answer-recap"
self.node.attrib
self.node.attrib.pop("read_only")
for name in self.node.attrib:
if name != "name":
warnings.warn("Invalid attribute found on <answer>: {}".format(name))
class QuestionToField(Change):
"""
<answer/mcq/mrq/rating>
<question>What is the answer?</question>
</answer/mcq/mrq/rating>
has become
<answer/mcq/mrq question="What is the answer?"></answer>
"""
@staticmethod
def applies_to(node):
parent_tags = ("answer", "mcq", "mrq", "rating")
return node.tag == "question" and node.getparent().tag in parent_tags
def apply(self):
if list(self.node):
warnings.warn("Ignoring unexpected children of a <question> element. HTML may be lost.")
p = self.node.getparent()
p.attrib["question"] = self.node.text
p.remove(self.node)
class QuestionSubmitMessageToField(Change):
"""
<mcq/mrq>
<message type="on-submit">Thank you for answering!</message>
</mcq/mrq>
has become
<mcq/mrq message="Thank you for answering!"></answer>
"""
@staticmethod
def applies_to(node):
return node.tag == "message" and node.get("type") == "on-submit" and node.getparent().tag in ("mcq", "mrq")
def apply(self):
if list(self.node):
warnings.warn("Ignoring unexpected children of a <message> element. HTML may be lost.")
p = self.node.getparent()
p.attrib["message"] = self.node.text
p.remove(self.node)
class TipChanges(Change):
"""
Changes to <tip></tip> elements.
The main one being that the correctness of each choice is now stored on the MRQ/MCQ block, not on the <tip>s.
"""
@staticmethod
def applies_to(node):
return node.tag == "tip" and node.getparent().tag in ("mcq", "mrq", "rating")
def apply(self):
p = self.node.getparent()
def add_to_list(list_name, value):
if list_name in p.attrib:
p.attrib[list_name] += ",{}".format(value)
else:
p.attrib[list_name] = value
if len(self.node.attrib) > 1:
warnings.warn("Invalid <tip> element found.")
return
mode = self.node.attrib.keys()[0]
value = self.node.attrib[mode]
if p.tag == "mrq":
if mode == "display":
add_to_list("ignored_choices", value)
elif mode == "require":
add_to_list("required_choices", value)
elif mode != "reject":
warnings.warn("Invalid <tip> element: has {}={}".format(mode, value))
return
else:
# This is an MCQ or Rating question:
if mode == "display":
add_to_list("correct_choices", value)
elif mode != "reject":
warnings.warn("Invalid <tip> element: has {}={}".format(mode, value))
return
self.node.attrib["values"] = value
self.node.attrib.pop(mode)
class SharedHeaderToHTML(Change):
""" <shared-header> element no longer exists. Just use <html> """
@staticmethod
def applies_to(node):
return node.tag == "shared-header" and node.getparent().tag == "mentoring"
def apply(self):
self.node.tag = "html"
# An *ordered* list of all XML schema changes:
xml_changes = (
RemoveTitle,
UnwrapHTML,
TableColumnHeader,
QuizzToMCQ,
MCQToRating,
ReadOnlyAnswerToRecap,
QuestionToField,
QuestionSubmitMessageToField,
TipChanges,
SharedHeaderToHTML,
)
def convert_xml_v1_to_v2(node):
"""
Given an XML node, re-structure it as needed to convert it from v1 style to v2 style XML.
"""
# Apply each individual type of change one at a time:
for change in xml_changes:
# Walk the XML tree once and figure out all the changes we will need.
# This lets us avoid modifying the tree while walking it.
changes_needed = []
for element in node.iter():
if change.applies_to(element):
changes_needed.append(change(element))
for change in changes_needed:
change.apply()
ddt
unicodecsv==0.9.4
-e git://github.com/edx-solutions/xblock-utils.git#egg=xblock-utils
-e git+https://github.com/open-craft/xblock-utils.git@c6a215884b59ca0449a2bda76fdd37b798a7aea9#egg=xblock-utils
-e .
......@@ -9,16 +9,10 @@ because the workbench SDK's settings file is not inside any python module.
import os
import sys
import workbench
if __name__ == "__main__":
# Find the location of the XBlock SDK. Note: it must be installed in development mode.
# ('python setup.py develop' or 'pip install -e')
xblock_sdk_dir = os.path.dirname(os.path.dirname(workbench.__file__))
sys.path.append(xblock_sdk_dir)
# Use the workbench settings file:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "workbench.settings")
# Configure a range of ports in case the default port of 8081 is in use
os.environ.setdefault("DJANGO_LIVE_TEST_SERVER_ADDRESS", "localhost:8081-8099")
......@@ -29,6 +23,6 @@ if __name__ == "__main__":
args = sys.argv[1:]
paths = [arg for arg in args if arg[0] != '-']
if not paths:
paths = ["mentoring/tests/"]
paths = ["mentoring/tests/", "mentoring/v1/tests/"]
options = [arg for arg in args if arg not in paths]
execute_from_command_line([sys.argv[0], "test"] + paths + options)
......@@ -47,23 +47,20 @@ BLOCKS = [
'mentoring-dataexport = mentoring:MentoringDataExportBlock',
'mentoring-table = mentoring.components:MentoringTableBlock',
'column = mentoring.components:MentoringTableColumnBlock',
'header = mentoring.components:MentoringTableColumnHeaderBlock',
'answer = mentoring.components:AnswerBlock',
'quizz = mentoring.components:MCQBlock',
'mcq = mentoring.components:MCQBlock',
'mrq = mentoring.components:MRQBlock',
'message = mentoring.components:MentoringMessageBlock',
'tip = mentoring.components:TipBlock',
'choice = mentoring.components:ChoiceBlock',
'html = mentoring.components:HTMLBlock',
'title = mentoring.components:TitleBlock',
'shared-header = mentoring.components:SharedHeaderBlock',
'mentoring-column = mentoring.components:MentoringTableColumn',
'mentoring-answer = mentoring.components:AnswerBlock',
'mentoring-answer-recap = mentoring.components:AnswerRecapBlock',
'mentoring-mcq = mentoring.components:MCQBlock',
'mentoring-rating = mentoring.components:RatingBlock',
'mentoring-mrq = mentoring.components:MRQBlock',
'mentoring-message = mentoring.components:MentoringMessageBlock',
'mentoring-tip = mentoring.components:TipBlock',
'mentoring-choice = mentoring.components:ChoiceBlock',
]
setup(
name='xblock-mentoring',
version='0.1',
version='2.0',
description='XBlock - Mentoring',
packages=['mentoring'],
install_requires=[
......
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