Commit 0f58a6fc by Xavier Antoviaque

Merge pull request #76 from open-craft/assessment-refactoring

Assessment refactoring and question block in answer
parents ea2e6cf6 9cf8b120
......@@ -144,16 +144,17 @@ answer element supports the following attributes:
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">
<html>
<p>What is your goal?</p>
</html>
<answer name="goal" weight="10"/>
<answer name="goal" weight="10">
<question>What is your goal?</question>
</answer>
</mentoring>
```
......
......@@ -30,6 +30,7 @@ from lazy import lazy
from xblock.fragment import Fragment
from .light_children import LightChild, Boolean, Scope, String, Integer, Float
from .step import StepMixin
from .models import Answer
from .utils import render_js_template
......@@ -41,7 +42,7 @@ log = logging.getLogger(__name__)
# Classes ###########################################################
class AnswerBlock(LightChild):
class AnswerBlock(LightChild, StepMixin):
"""
A field where the student enters an answer
......@@ -53,9 +54,24 @@ class AnswerBlock(LightChild):
default=None, scope=Scope.content)
min_characters = Integer(help="Minimum number of characters allowed for the answer",
default=0, scope=Scope.content)
question = String(help="Question to ask the student", 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)
@classmethod
def init_block_from_node(cls, block, node, attr):
block.light_children = []
for child_id, xml_child in enumerate(node):
if xml_child.tag == 'question':
block.question = xml_child.text
else:
cls.add_node_as_child(block, xml_child, child_id)
for name, value in attr:
setattr(block, name, value)
return block
@lazy
def student_input(self):
"""
......
......@@ -29,6 +29,7 @@ from .light_children import LightChild, Scope, String
log = logging.getLogger(__name__)
class SharedHeaderBlock(LightChild):
"""
A shared header block shown under the title.
......@@ -47,8 +48,7 @@ class SharedHeaderBlock(LightChild):
return block
def student_view(self, context=None):
return Fragment(u"<script type='text/template' id='{}'>\n{}\n</script>".format(
'light-child-template',
return Fragment(u"<script type='text/template' id='light-child-template'>\n{}\n</script>".format(
self.content
))
......
......@@ -38,6 +38,7 @@ from .title import TitleBlock
from .header import SharedHeaderBlock
from .html import HTMLBlock
from .message import MentoringMessageBlock
from .step import StepParentMixin
from .utils import get_scenarios_from_path, load_resource, render_template
......@@ -48,7 +49,7 @@ log = logging.getLogger(__name__)
# Classes ###########################################################
class MentoringBlock(XBlockWithLightChildren):
class MentoringBlock(XBlockWithLightChildren, StepParentMixin):
"""
An XBlock providing mentoring capabilities
......@@ -91,50 +92,30 @@ class MentoringBlock(XBlockWithLightChildren):
MENTORING_MODES = ('standard', 'assessment')
FLOATING_BLOCKS = (TitleBlock, MentoringMessageBlock, SharedHeaderBlock)
@property
def is_assessment(self):
return self.mode == 'assessment'
@property
def steps(self):
return [child for child in self.get_children_objects() if
not isinstance(child, (HTMLBlock, TitleBlock, MentoringMessageBlock, SharedHeaderBlock))]
@property
def score(self):
"""Compute the student score taking into account the light child weight."""
total_child_weight = sum(float(step.weight) for step in self.steps)
if total_child_weight == 0:
return (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]['completed'] == True)
incorrect = sum(1 for r in self.student_results if r[1]['completed'] == False)
return (score, int(round(score*100)), correct, incorrect)
def _index_steps(self):
steps = self.steps
if len(steps) == 1:
steps[0].index = ""
return
index = 1
for child in steps:
child.index = index
index += 1
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]['completed'] is True)
incorrect = sum(1 for r in self.student_results if r[1]['completed'] is False)
return (score, int(round(score * 100)), correct, incorrect)
def student_view(self, context):
self._index_steps()
fragment, named_children = self.get_children_fragment(
context, view_name='mentoring_view',
not_instance_of=(MentoringMessageBlock, TitleBlock, SharedHeaderBlock)
not_instance_of=self.FLOATING_BLOCKS,
)
fragment.add_content(render_template('templates/html/mentoring.html', {
'self': self,
'named_children': named_children,
......@@ -142,7 +123,7 @@ class MentoringBlock(XBlockWithLightChildren):
}))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/mentoring.css'))
fragment.add_javascript_url(
self.runtime.local_resource_url(self, 'public/js/vendor/underscore-min.js'))
self.runtime.local_resource_url(self, 'public/js/vendor/underscore-min.js'))
if self.is_assessment:
fragment.add_javascript_url(
self.runtime.local_resource_url(self, 'public/js/mentoring_assessment_view.js')
......@@ -174,7 +155,7 @@ class MentoringBlock(XBlockWithLightChildren):
data['component_id'] = self.url_name
self.runtime.publish(self, event_type, data)
return {'result':'success'}
return {'result': 'success'}
@property
def title(self):
......@@ -246,8 +227,7 @@ class MentoringBlock(XBlockWithLightChildren):
if self.has_missing_dependency:
completed = False
message = 'You need to complete all previous steps before being able to complete '+\
'the current one.'
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:
self.next_step = self.followed_by
......@@ -291,8 +271,8 @@ class MentoringBlock(XBlockWithLightChildren):
completed = False
current_child = None
children = [child for child in self.get_children_objects() \
if not isinstance(child, (TitleBlock, SharedHeaderBlock))]
children = [child for child in self.get_children_objects()
if not isinstance(child, self.FLOATING_BLOCKS)]
for child in children:
if child.name and child.name in submissions:
......@@ -307,7 +287,7 @@ class MentoringBlock(XBlockWithLightChildren):
completed = False
break
self.step = step+1
self.step = step + 1
child_result = child.submit(submission)
if 'tips' in child_result:
......
......@@ -28,6 +28,7 @@ import logging
from xblock.fragment import Fragment
from .choice import ChoiceBlock
from .step import StepMixin
from .light_children import LightChild, Scope, String, Float
from .tip import TipBlock
from .utils import render_template, render_js_template
......@@ -40,7 +41,7 @@ log = logging.getLogger(__name__)
# Classes ###########################################################
class QuestionnaireAbstractBlock(LightChild):
class QuestionnaireAbstractBlock(LightChild, StepMixin):
"""
An abstract class used for MCQ/MRQ blocks
......
class StepParentMixin(object):
"""
A parent containing the Step objects
The parent must have a get_children_objects() method.
"""
@property
def steps(self):
return [child for child in self.get_children_objects() if isinstance(child, StepMixin)]
class StepMixin(object):
@property
def step_number(self):
return self.parent.steps.index(self) + 1
@property
def lonely_step(self):
if self not in self.parent.steps:
raise ValueError("Step's parent should contain Step", self, self.parents.steps)
return len(self.parent.steps) == 1
<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>
<textarea
class="answer editable" cols="50" rows="10" name="input"
data-min_characters="{{ self.min_characters }}"
......
<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>
<blockquote class="answer read_only">
{{ self.student_input|linebreaksbr }}
</blockquote>
......
<fieldset class="choices questionnaire">
<legend class="question">
<h3 class="question-title">QUESTION {{ self.index }}</h3>
<h3 class="question-title">QUESTION {% if not self.lonely_step %}{{ self.step_number }}{% endif %}</h3>
<p>{{ self.question }}</p>
</legend>
<div class="choices-list">
......
<fieldset class="rating questionnaire">
<legend class="question">
<h3 class="question-title">QUESTION {{ self.index }}</h3>
<h3 class="question-title">QUESTION {% if not self.lonely_step %}{{ self.step_number }}{% endif %}</h3>
<p>{{ self.question }}</p>
</legend>
<div class="choices-list">
......
<fieldset class="choices questionnaire" data-hide_results="{{self.hide_results}}">
<legend class="question">
<h3 class="question-title">QUESTION {{ self.index }}</h3>
<h3 class="question-title">QUESTION {% if not self.lonely_step %}{{ self.step_number }}{% endif %}</h3>
<p>{{ self.question }}</p>
</legend>
<div class="choices-list">
......
......@@ -5,10 +5,12 @@
</shared-header>
<html>
<p>What is your goal?</p>
<p>Please answer the questions below.</p>
</html>
<answer name="goal" />
<answer name="goal">
<question>What is your goal?</question>
</answer>
<mcq name="mcq_1_1" type="choices">
<question>Do you like this MCQ?</question>
......
<mentoring url_name="{{ url_name }}" display_name="Nav tooltip title" weight="1" mode="standard">
<title>Default Title</title>
<html>
<p>What is your goal?</p>
<p>Please answer the questions below.</p>
</html>
<answer name="goal" />
<answer name="goal">
<question>What is your goal?</question>
</answer>
<mcq name="mcq_1_1" type="choices">
<question>Do you like this MCQ?</question>
......
<mentoring url_name="{{ url_name }}" display_name="Nav tooltip title" weight="1" mode="standard">
<title>Default Title</title>
<html>
<p>Please answer the questions below.</p>
</html>
<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>
......@@ -30,6 +30,7 @@ from mentoring.test_base import MentoringBaseTest
# Classes ###########################################################
class MCQBlockTest(MentoringBaseTest):
def test_mcq_choices_rating(self):
......
import unittest
from lxml import etree
from mentoring import MentoringBlock
import mentoring
from mentoring.step import StepMixin, StepParentMixin
class Parent(StepParentMixin):
def get_children_objects(self):
return list(self._children)
def _set_children_for_test(self, *children):
self._children = children
for child in self._children:
try:
child.parent = self
except AttributeError:
pass
class Step(StepMixin):
def __init__(self): pass
class NotAStep(object): pass
class TestStepMixin(unittest.TestCase):
def test_single_step_is_returned_correctly(self):
block = Parent()
step = Step()
block._children = [step]
self.assertSequenceEqual(block.steps, [step])
def test_only_steps_are_returned(self):
block = Parent()
step1 = Step()
step2 = Step()
block._set_children_for_test(step1, 1, "2", "Step", NotAStep(), False, step2, NotAStep())
self.assertSequenceEqual(block.steps, [step1, step2])
def test_proper_number_is_returned_for_step(self):
block = Parent()
step1 = Step()
step2 = Step()
block._set_children_for_test(step1, 1, "2", "Step", NotAStep(), False, step2, NotAStep())
self.assertEquals(step1.step_number, 1)
self.assertEquals(step2.step_number, 2)
def test_the_number_does_not_represent_the_order_of_creation(self):
block = Parent()
step1 = Step()
step2 = Step()
block._set_children_for_test(step2, 1, "2", "Step", NotAStep(), False, step1, NotAStep())
self.assertEquals(step1.step_number, 2)
self.assertEquals(step2.step_number, 1)
def test_lonely_step_is_true_for_stand_alone_steps(self):
block = Parent()
step1 = Step()
block._set_children_for_test(1, "2", step1, "Step", NotAStep(), False)
self.assertTrue(step1.lonely_step)
def test_lonely_step_is_true_if_parent_have_more_steps(self):
block = Parent()
step1 = Step()
step2 = Step()
block._set_children_for_test(1, step2, "2", step1, "Step", NotAStep(), False)
self.assertFalse(step1.lonely_step)
self.assertFalse(step2.lonely_step)
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