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: ...@@ -144,16 +144,17 @@ answer element supports the following attributes:
question. The answer is rendered as a HTML quote instead of a question. The answer is rendered as a HTML quote instead of a
textarea element (boolean; defaults to `false`). textarea element (boolean; defaults to `false`).
It can also have a `<question>` element containing a paragraph of non-formatted plain text.
#### Example #### Example
Here is a simple example of a free-form question: Here is a simple example of a free-form question:
```xml ```xml
<mentoring url_name="goal_definition" followed_by="getting_feedback" weight="20"> <mentoring url_name="goal_definition" followed_by="getting_feedback" weight="20">
<html> <answer name="goal" weight="10">
<p>What is your goal?</p> <question>What is your goal?</question>
</html> </answer>
<answer name="goal" weight="10"/>
</mentoring> </mentoring>
``` ```
......
...@@ -30,6 +30,7 @@ from lazy import lazy ...@@ -30,6 +30,7 @@ from lazy import lazy
from xblock.fragment import Fragment from xblock.fragment import Fragment
from .light_children import LightChild, Boolean, Scope, String, Integer, Float from .light_children import LightChild, Boolean, Scope, String, Integer, Float
from .step import StepMixin
from .models import Answer from .models import Answer
from .utils import render_js_template from .utils import render_js_template
...@@ -41,7 +42,7 @@ log = logging.getLogger(__name__) ...@@ -41,7 +42,7 @@ log = logging.getLogger(__name__)
# Classes ########################################################### # Classes ###########################################################
class AnswerBlock(LightChild): class AnswerBlock(LightChild, StepMixin):
""" """
A field where the student enters an answer A field where the student enters an answer
...@@ -53,9 +54,24 @@ class AnswerBlock(LightChild): ...@@ -53,9 +54,24 @@ class AnswerBlock(LightChild):
default=None, scope=Scope.content) default=None, scope=Scope.content)
min_characters = Integer(help="Minimum number of characters allowed for the answer", min_characters = Integer(help="Minimum number of characters allowed for the answer",
default=0, scope=Scope.content) 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.", weight = Float(help="Defines the maximum total grade of the light child block.",
default=1, scope=Scope.content, enforce_type=True) 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 @lazy
def student_input(self): def student_input(self):
""" """
......
...@@ -29,6 +29,7 @@ from .light_children import LightChild, Scope, String ...@@ -29,6 +29,7 @@ from .light_children import LightChild, Scope, String
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class SharedHeaderBlock(LightChild): class SharedHeaderBlock(LightChild):
""" """
A shared header block shown under the title. A shared header block shown under the title.
...@@ -47,8 +48,7 @@ class SharedHeaderBlock(LightChild): ...@@ -47,8 +48,7 @@ class SharedHeaderBlock(LightChild):
return block return block
def student_view(self, context=None): def student_view(self, context=None):
return Fragment(u"<script type='text/template' id='{}'>\n{}\n</script>".format( return Fragment(u"<script type='text/template' id='light-child-template'>\n{}\n</script>".format(
'light-child-template',
self.content self.content
)) ))
......
...@@ -38,6 +38,7 @@ from .title import TitleBlock ...@@ -38,6 +38,7 @@ from .title import TitleBlock
from .header import SharedHeaderBlock from .header import SharedHeaderBlock
from .html import HTMLBlock from .html import HTMLBlock
from .message import MentoringMessageBlock from .message import MentoringMessageBlock
from .step import StepParentMixin
from .utils import get_scenarios_from_path, load_resource, render_template from .utils import get_scenarios_from_path, load_resource, render_template
...@@ -48,7 +49,7 @@ log = logging.getLogger(__name__) ...@@ -48,7 +49,7 @@ log = logging.getLogger(__name__)
# Classes ########################################################### # Classes ###########################################################
class MentoringBlock(XBlockWithLightChildren): class MentoringBlock(XBlockWithLightChildren, StepParentMixin):
""" """
An XBlock providing mentoring capabilities An XBlock providing mentoring capabilities
...@@ -91,50 +92,30 @@ class MentoringBlock(XBlockWithLightChildren): ...@@ -91,50 +92,30 @@ class MentoringBlock(XBlockWithLightChildren):
MENTORING_MODES = ('standard', 'assessment') MENTORING_MODES = ('standard', 'assessment')
FLOATING_BLOCKS = (TitleBlock, MentoringMessageBlock, SharedHeaderBlock)
@property @property
def is_assessment(self): def is_assessment(self):
return self.mode == 'assessment' return self.mode == 'assessment'
@property @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): def score(self):
"""Compute the student score taking into account the light child weight.""" """Compute the student score taking into account the light child weight."""
total_child_weight = sum(float(step.weight) for step in self.steps) total_child_weight = sum(float(step.weight) for step in self.steps)
if total_child_weight == 0: if total_child_weight == 0:
return (0, 0, 0, 0) return (0, 0, 0, 0)
score = sum(r[1]['score']*r[1]['weight'] \ score = sum(r[1]['score'] * r[1]['weight'] for r in self.student_results) / total_child_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)
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'] is False)
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
return (score, int(round(score * 100)), correct, incorrect)
def student_view(self, context): def student_view(self, context):
self._index_steps()
fragment, named_children = self.get_children_fragment( fragment, named_children = self.get_children_fragment(
context, view_name='mentoring_view', 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', { fragment.add_content(render_template('templates/html/mentoring.html', {
'self': self, 'self': self,
'named_children': named_children, 'named_children': named_children,
...@@ -174,7 +155,7 @@ class MentoringBlock(XBlockWithLightChildren): ...@@ -174,7 +155,7 @@ class MentoringBlock(XBlockWithLightChildren):
data['component_id'] = self.url_name data['component_id'] = self.url_name
self.runtime.publish(self, event_type, data) self.runtime.publish(self, event_type, data)
return {'result':'success'} return {'result': 'success'}
@property @property
def title(self): def title(self):
...@@ -246,8 +227,7 @@ class MentoringBlock(XBlockWithLightChildren): ...@@ -246,8 +227,7 @@ class MentoringBlock(XBlockWithLightChildren):
if self.has_missing_dependency: if self.has_missing_dependency:
completed = False completed = False
message = 'You need to complete all previous steps before being able to complete '+\ message = 'You need to complete all previous steps before being able to complete the current one.'
'the current one.'
elif completed and self.next_step == self.url_name: elif completed and self.next_step == self.url_name:
self.next_step = self.followed_by self.next_step = self.followed_by
...@@ -291,8 +271,8 @@ class MentoringBlock(XBlockWithLightChildren): ...@@ -291,8 +271,8 @@ class MentoringBlock(XBlockWithLightChildren):
completed = False completed = False
current_child = None current_child = None
children = [child for child in self.get_children_objects() \ children = [child for child in self.get_children_objects()
if not isinstance(child, (TitleBlock, SharedHeaderBlock))] if not isinstance(child, self.FLOATING_BLOCKS)]
for child in children: for child in children:
if child.name and child.name in submissions: if child.name and child.name in submissions:
...@@ -307,7 +287,7 @@ class MentoringBlock(XBlockWithLightChildren): ...@@ -307,7 +287,7 @@ class MentoringBlock(XBlockWithLightChildren):
completed = False completed = False
break break
self.step = step+1 self.step = step + 1
child_result = child.submit(submission) child_result = child.submit(submission)
if 'tips' in child_result: if 'tips' in child_result:
......
...@@ -28,6 +28,7 @@ import logging ...@@ -28,6 +28,7 @@ import logging
from xblock.fragment import Fragment from xblock.fragment import Fragment
from .choice import ChoiceBlock from .choice import ChoiceBlock
from .step import StepMixin
from .light_children import LightChild, Scope, String, Float from .light_children import LightChild, Scope, String, Float
from .tip import TipBlock from .tip import TipBlock
from .utils import render_template, render_js_template from .utils import render_template, render_js_template
...@@ -40,7 +41,7 @@ log = logging.getLogger(__name__) ...@@ -40,7 +41,7 @@ log = logging.getLogger(__name__)
# Classes ########################################################### # Classes ###########################################################
class QuestionnaireAbstractBlock(LightChild): class QuestionnaireAbstractBlock(LightChild, StepMixin):
""" """
An abstract class used for MCQ/MRQ blocks 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 }}"> <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 <textarea
class="answer editable" cols="50" rows="10" name="input" class="answer editable" cols="50" rows="10" name="input"
data-min_characters="{{ self.min_characters }}" data-min_characters="{{ self.min_characters }}"
......
<div class="xblock-answer" data-completed="{{ self.completed }}"> <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"> <blockquote class="answer read_only">
{{ self.student_input|linebreaksbr }} {{ self.student_input|linebreaksbr }}
</blockquote> </blockquote>
......
<fieldset class="choices questionnaire"> <fieldset class="choices questionnaire">
<legend class="question"> <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> <p>{{ self.question }}</p>
</legend> </legend>
<div class="choices-list"> <div class="choices-list">
......
<fieldset class="rating questionnaire"> <fieldset class="rating questionnaire">
<legend class="question"> <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> <p>{{ self.question }}</p>
</legend> </legend>
<div class="choices-list"> <div class="choices-list">
......
<fieldset class="choices questionnaire" data-hide_results="{{self.hide_results}}"> <fieldset class="choices questionnaire" data-hide_results="{{self.hide_results}}">
<legend class="question"> <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> <p>{{ self.question }}</p>
</legend> </legend>
<div class="choices-list"> <div class="choices-list">
......
...@@ -5,10 +5,12 @@ ...@@ -5,10 +5,12 @@
</shared-header> </shared-header>
<html> <html>
<p>What is your goal?</p> <p>Please answer the questions below.</p>
</html> </html>
<answer name="goal" /> <answer name="goal">
<question>What is your goal?</question>
</answer>
<mcq name="mcq_1_1" type="choices"> <mcq name="mcq_1_1" type="choices">
<question>Do you like this MCQ?</question> <question>Do you like this MCQ?</question>
......
<mentoring url_name="{{ url_name }}" display_name="Nav tooltip title" weight="1" mode="standard"> <mentoring url_name="{{ url_name }}" display_name="Nav tooltip title" weight="1" mode="standard">
<title>Default Title</title> <title>Default Title</title>
<html> <html>
<p>What is your goal?</p> <p>Please answer the questions below.</p>
</html> </html>
<answer name="goal" /> <answer name="goal">
<question>What is your goal?</question>
</answer>
<mcq name="mcq_1_1" type="choices"> <mcq name="mcq_1_1" type="choices">
<question>Do you like this MCQ?</question> <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 ...@@ -30,6 +30,7 @@ from mentoring.test_base import MentoringBaseTest
# Classes ########################################################### # Classes ###########################################################
class MCQBlockTest(MentoringBaseTest): class MCQBlockTest(MentoringBaseTest):
def test_mcq_choices_rating(self): 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