Commit af43a26d by Braden MacDonald

Refactor Answer and Table. Changed <answer read_only="true"> to <answer-recap>

parent 3736fa36
from .answer import AnswerBlock
from .answer import AnswerBlock, AnswerRecapBlock
from .choice import ChoiceBlock
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
......@@ -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
......@@ -25,3 +25,7 @@
margin-right: 13px;
vertical-align: -25px;
}
.answer-table {
margin-bottom: 20px;
}
......@@ -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>
{% 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>
......
......@@ -5,8 +5,7 @@
<p>Please answer the questions below.</p>
</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" question="Do you like this MCQ?" correct_choices="yes">
......
......@@ -8,8 +8,7 @@
<p>Please answer the question below.</p>
</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 2 (Assessment)" mode="assessment">
......@@ -17,11 +16,9 @@
<p>Please answer the question below.</p>
</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>
......
......@@ -3,8 +3,7 @@
<p>Please answer the questions below.</p>
</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" question="Do you like this MCQ?" correct_choices="yes">
......
......@@ -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')
......
......@@ -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_2" enforce_dependency="false">
<answer name="answer_1" read_only="true" />
<answer-recap name="answer_1" />
<answer name="answer_1" />
</mentoring>
</vertical_demo>
......@@ -8,21 +8,15 @@
<p>Please answer the question below.</p>
</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 2 (Assessment)" mode="assessment">
<html_demo>
<p>Please answer the question below.</p>
</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 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>
......
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