Skip to content

Commit

Permalink
test: add all basic tests
Browse files Browse the repository at this point in the history
  • Loading branch information
navinkarkera committed Jul 30, 2024
1 parent 1e16ca6 commit 5b8cbef
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 21 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ extract_translations: ## extract strings to be translated, outputting .po files

compile_translations: ## compile translation files, outputting .mo files for each supported language
cd $(WORKING_DIR) && i18n_tool generate -v
python manage.py compilejsi18n --namespace DragAndDropI18N --output $(JS_TARGET)
python manage.py compilejsi18n --namespace MultiProblemI18N --output $(JS_TARGET)

detect_changed_source_translations:
cd $(WORKING_DIR) && i18n_tool changed
Expand Down
1 change: 1 addition & 0 deletions multi_problem_xblock/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@


def getLibraryContentBlock():
"""Get LibraryContentBlock from edx-platform if possible"""
try:
from xmodule.library_content_block import LibraryContentBlock # pylint: disable=import-outside-toplevel
except ModuleNotFoundError:
Expand Down
39 changes: 37 additions & 2 deletions multi_problem_xblock/multi_problem_xblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import logging
from copy import copy

from lxml import etree
from lxml.etree import XMLSyntaxError
from web_fragments.fragment import Fragment
from webob import Response
from xblock.completable import XBlockCompletionMode
Expand Down Expand Up @@ -287,14 +289,14 @@ def get_test_scores(self, _data, _suffix):
)
return Response(template, content_type='text/html')

def student_view_data(self, context):
def student_view_data(self, context=None):
"""
Student view data for templates and javascript initialization
"""
fragment = Fragment()
items = []
child_context = {} if not context else copy(context)
jump_to_id = context.get('jumpToId')
jump_to_id = child_context.get('jumpToId')
bookmarks_service = self.runtime.service(self, 'bookmarks')
total_problems = 0
completed_problems = 0
Expand Down Expand Up @@ -370,3 +372,36 @@ def publish_completion(self, progress: float):
completion_service = self.runtime.service(self, 'completion')
if completion_service and completion_service.completion_tracking_enabled():
self.runtime.publish(self, 'completion', {'completion': progress})

@classmethod
def definition_from_xml(cls, xml_object, system):
"""Generate object from xml"""
children = []

for child in xml_object.getchildren():
try:
children.append(system.process_xml(etree.tostring(child)).scope_ids.usage_id)
except (XMLSyntaxError, AttributeError):
msg = (
"Unable to load child when parsing Multi Problem Block. "
"This can happen when a comment is manually added to the course export."
)
logger.error(msg)
if system.error_tracker is not None:
system.error_tracker(msg)

definition = dict(xml_object.attrib.items())
return definition, children

def definition_to_xml(self, resource_fs):
""" Exports Library Content Block to XML """
xml_object = etree.Element('multi_problem')
for child in self.get_children():
self.runtime.add_block_as_child_node(child, xml_object)
# Set node attributes based on our fields.
for field_name, field in self.fields.items():
if field_name in ('children', 'parent', 'content'):
continue
if field.is_set_on(self):
xml_object.set(field_name, str(field.read_from(self)))
return xml_object
25 changes: 13 additions & 12 deletions multi_problem_xblock/public/js/translations/en/text.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

(function(global){
var DragAndDropI18N = {
var MultiProblemI18N = {
init: function() {


Expand All @@ -10,14 +10,7 @@
const django = globals.django || (globals.django = {});


django.pluralidx = function(n) {
const v = (n != 1);
if (typeof v === 'boolean') {
return v ? 1 : 0;
} else {
return v;
}
};
django.pluralidx = function(count) { return (count == 1) ? 0 : 1; };


/* gettext library */
Expand Down Expand Up @@ -91,7 +84,15 @@
"DATE_INPUT_FORMATS": [
"%Y-%m-%d",
"%m/%d/%Y",
"%m/%d/%y"
"%m/%d/%y",
"%b %d %Y",
"%b %d, %Y",
"%d %b %Y",
"%d %b, %Y",
"%B %d %Y",
"%B %d, %Y",
"%d %B %Y",
"%d %B, %Y"
],
"DECIMAL_SEPARATOR": ".",
"FIRST_DAY_OF_WEEK": 0,
Expand Down Expand Up @@ -135,7 +136,7 @@

}
};
DragAndDropI18N.init();
global.DragAndDropI18N = DragAndDropI18N;
MultiProblemI18N.init();
global.MultiProblemI18N = MultiProblemI18N;
}(this));

Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
</button>
<div class="text-center">
<small class="slide-position text-gray">
{% blocktrans %}{{ self.current_slide }} of {{ items|length }}{% endblocktrans %}
{% blocktrans with items_length=items|length current_slide=self.current_slide %}
{{ current_slide }} of {{ items_length }}
{% endblocktrans %}
</small>
{% if self.display_name %}
<h2 class="hd hd-2">{{ self.display_name }}</h2>
Expand Down
98 changes: 94 additions & 4 deletions tests/unit/test_basics.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@
from unittest import mock

import ddt
from sample_xblocks.basic.problem import ProblemBlock

from multi_problem_xblock.compat import L_SHOWANSWER
from multi_problem_xblock.compat import L_SHOWANSWER, L_ShowCorrectness
from multi_problem_xblock.multi_problem_xblock import DISPLAYFEEDBACK, SCORE_DISPLAY_FORMAT, MultiProblemBlock

from ..utils import TestCaseMixin, instantiate_block
from ..utils import SampleProblemBlock, TestCaseMixin, instantiate_block


@ddt.ddt
Expand All @@ -19,7 +18,7 @@ def setUp(self):
self.children = {}
for i in range(3):
usage_key = f'block-v1:edx+cs1+test+type@problem+block@{i}'
problem_block = instantiate_block(ProblemBlock, fields={
problem_block = instantiate_block(SampleProblemBlock, fields={
'usage_key': usage_key,
})
self.children[usage_key] = problem_block
Expand Down Expand Up @@ -59,6 +58,7 @@ def assertPublishEvent(self, completion):
self.assertEqual(patched_publish.mock_calls, expected_calls)

def test_template_contents(self):
"""Verify rendered template contents"""
context = {}
student_fragment = self.block.runtime.render(self.block, 'student_view', context)
self.assertIn(
Expand All @@ -68,6 +68,7 @@ def test_template_contents(self):
self.assertIn('<div class="problem-test-score-container">', student_fragment.content)

def test_student_view_data(self):
"""Verify student data used in templates"""
_, template_context, js_context = self.block.student_view_data({})
items = template_context.pop('items')
self.assertEqual(template_context, {
Expand All @@ -86,3 +87,92 @@ def test_student_view_data(self):
})
for index, item in enumerate(items):
self.assertEqual(item['id'], self.children_ids[index])

def test_editor_saved(self):
"""Verify whether child values are updated based on parent block"""
self.block.showanswer = L_SHOWANSWER.NEVER
self.block.display_feedback = DISPLAYFEEDBACK.END_OF_TEST
# Call editor_saved as this is called by cms xblock api before saving the block
self.block.editor_saved(None, None, None)
for child in self.block.get_children():
self.assertEqual(child.showanswer, L_SHOWANSWER.NEVER)
self.assertEqual(child.show_correctness, L_ShowCorrectness.NEVER)

# if display_feedback = immediately, child block showanswer should be set to always
self.block.display_feedback = DISPLAYFEEDBACK.IMMEDIATELY
self.block.editor_saved(None, None, None)
for child in self.block.get_children():
self.assertEqual(child.show_correctness, L_ShowCorrectness.ALWAYS)

def test_incomplete_overall_progress_handler(self):
"""Check progress handler information when all problems are not completed"""
# Check progress handler when 2/3 problems are completed
self.block.children[self.children_ids[0]].is_submitted = lambda: True
self.block.children[self.children_ids[1]].is_submitted = lambda: True
self.block.children[self.children_ids[2]].is_submitted = lambda: False
res = self.call_handler('get_overall_progress', {}, method='GET')
self.assertEqual(res, {'overall_progress': int((2 / 3) * 100)})

def test_completed_overall_progress_handler(self):
"""Check progress handler information when all problems are completed"""
self.block.publish_completion = mock.Mock()
# Set cut_off_score to 100%
self.block.cut_off_score = 1
# Check progress handler when 3/3 problems are completed and all are correct
for child in self.block.get_children():
child.is_submitted = lambda: True
child.is_correct = lambda: True
child.score = mock.Mock(raw_earned=1, raw_possible=1)
res = self.call_handler('get_overall_progress', {}, method='GET')
self.assertEqual(res, {'overall_progress': 100})
self.block.publish_completion.assert_called_once_with(1)

# Update one child to be incorrect
self.block.children[self.children_ids[2]].is_correct = lambda: False
self.block.children[self.children_ids[2]].score = mock.Mock(raw_earned=0, raw_possible=1)
res = self.call_handler('get_overall_progress', {}, method='GET')
self.assertEqual(res, {'overall_progress': 100})
# Completion should be reduced to 0.9 as the student score was less than required cut_off_score
self.block.publish_completion.assert_called_with(0.9)

def test_get_scores_when_incomplete(self):
"""Test get_test_scores handler when all problems are not completed"""
for _, child in enumerate(self.block.get_children()):
child.is_submitted = lambda: False
res = self.call_handler('get_test_scores', {}, expect_json=False, method='GET')
self.assertEqual(res.status_code, 400)

def test_get_scores(self):
"""Test get_test_scores handler"""
for index, child in enumerate(self.block.get_children()):
child.is_submitted = lambda: True
# Set last problem incorrect
child.score = mock.Mock(raw_earned=1 if index < 2 else 0, raw_possible=1)
child.is_correct = lambda: index < 2 # pylint: disable=cell-var-from-loop
res = self.call_handler('get_test_scores', {}, expect_json=False, method='GET')
self.assertIn('question2', res.text)
self.assertIn('answer2', res.text)
self.assertIn('correct_answer2', res.text)
self.assertIn('question1', res.text)
self.assertIn('answer1', res.text)
self.assertIn('question0', res.text)
self.assertIn('answer0', res.text)
self.assertIn('<b class="test-score">2/3</b>', res.text)

def test_get_scores_in_percentage(self):
"""Test get_test_scores handler returns percentage"""
self.block.score_display_format = SCORE_DISPLAY_FORMAT.PERCENTAGE
for index, child in enumerate(self.block.get_children()):
child.is_submitted = lambda: True
# Set last problem incorrect
child.score = mock.Mock(raw_earned=1 if index < 2 else 0, raw_possible=1)
child.is_correct = lambda: index < 2 # pylint: disable=cell-var-from-loop
res = self.call_handler('get_test_scores', {}, expect_json=False, method='GET')
self.assertIn('question2', res.text)
self.assertIn('answer2', res.text)
self.assertIn('correct_answer2', res.text)
self.assertIn('question1', res.text)
self.assertIn('answer1', res.text)
self.assertIn('question0', res.text)
self.assertIn('answer0', res.text)
self.assertIn('<b class="test-score">67%</b>', res.text)
19 changes: 18 additions & 1 deletion tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import json
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock, Mock, patch

from sample_xblocks.basic.problem import ProblemBlock, String
from webob import Request
from workbench.runtime import WorkbenchRuntime
from xblock.core import Scope
from xblock.field_data import DictFieldData


Expand Down Expand Up @@ -33,9 +35,24 @@ def instantiate_block(cls, fields=None):
block.runtime.get_block = lambda child_id: children[child_id]
block.usage_key.__str__.return_value = usage_key
block.usage_key.course_key.make_usage_key = lambda _, child_id: child_id
block.get_children = lambda: list(children.values())
return block


class SampleProblemBlock(ProblemBlock):
question = String(scope=Scope.content)
showanswer = String(scope=Scope.settings, default="")
show_correctness = String(scope=Scope.settings, default="")
lcp = Mock(student_answers={1: 1})

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Return incremental mock question answer text each time it is called.
self.lcp.find_question_label.side_effect = [f'question{x}' for x in range(3)]
self.lcp.find_answer_text.side_effect = [f'answer{x}' for x in range(3)]
self.lcp.find_correct_answer_text.side_effect = [f'correct_answer{x}' for x in range(3)]


class TestCaseMixin:
""" Helpful mixins for unittest TestCase subclasses """
maxDiff = None
Expand Down

0 comments on commit 5b8cbef

Please sign in to comment.