Commit ad5c1f89 by Xavier Antoviaque

Merge pull request #87 from open-craft/dragonfi/extract-to-xblock-utils

Extract common functionality to xblock-utils
parents 6f4ea850 d78f09d1
......@@ -32,7 +32,7 @@ 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
from .utils import loader
# Globals ###########################################################
......@@ -92,11 +92,11 @@ class AnswerBlock(LightChild, StepMixin):
def mentoring_view(self, context=None):
if not self.read_only:
html = render_js_template('templates/html/answer_editable.html', {
html = loader.custom_render_js_template('templates/html/answer_editable.html', {
'self': self,
})
else:
html = render_js_template('templates/html/answer_read_only.html', {
html = loader.custom_render_js_template('templates/html/answer_read_only.html', {
'self': self,
})
......@@ -108,7 +108,7 @@ class AnswerBlock(LightChild, StepMixin):
return fragment
def mentoring_table_view(self, context=None):
html = render_js_template('templates/html/answer_table.html', {
html = loader.custom_render_js_template('templates/html/answer_table.html', {
'self': self,
})
fragment = Fragment(html)
......
......@@ -32,7 +32,7 @@ from xblock.fields import String, Scope
from xblock.fragment import Fragment
from .models import Answer
from .utils import list2csv, render_template
from .utils import list2csv, loader
# Globals ###########################################################
......@@ -50,7 +50,7 @@ class MentoringDataExportBlock(XBlock):
scope=Scope.settings)
def student_view(self, context):
html = render_template('templates/html/dataexport.html', {
html = loader.render_template('templates/html/dataexport.html', {
'self': self,
})
......
......@@ -38,6 +38,8 @@ from xblock.core import XBlock
from xblock.fragment import Fragment
from xblock.plugin import Plugin
from xblockutils.publish_event import PublishEventMixin
from .models import LightChild as LightChildModel
......@@ -83,7 +85,18 @@ class LightChildrenMixin(XBlockWithChildrenFragmentsMixin):
log.debug('parse_xml called')
block = runtime.construct_xblock_from_class(cls, keys)
cls.init_block_from_node(block, node, node.items())
block.xml_content = getattr(block, 'xml_content', '') or etree.tostring(node)
def _is_default(value):
xml_content_field = getattr(block.__class__, 'xml_content', None)
default_value = getattr(xml_content_field, 'default', None)
return value == default_value
is_default = getattr(block, 'is_default_xml_content', _is_default)
xml_content = getattr(block, 'xml_content', None)
if is_default(xml_content):
block.xml_content = etree.tostring(node)
return block
@classmethod
......@@ -166,7 +179,7 @@ class LightChildrenMixin(XBlockWithChildrenFragmentsMixin):
return fragment, named_child_frags
class XBlockWithLightChildren(LightChildrenMixin, XBlock):
class XBlockWithLightChildren(LightChildrenMixin, XBlock, PublishEventMixin):
"""
XBlock base class with support for LightChild
"""
......
......@@ -28,7 +28,7 @@ import logging
from .light_children import Scope, String
from .questionnaire import QuestionnaireAbstractBlock
from .utils import render_template
from .utils import loader
# Globals ###########################################################
......@@ -59,7 +59,7 @@ class MCQBlock(QuestionnaireAbstractBlock):
if submission in tip.display_with_defaults:
tips_fragments.append(tip.render())
formatted_tips = render_template('templates/html/tip_choice_group.html', {
formatted_tips = loader.render_template('templates/html/tip_choice_group.html', {
'self': self,
'tips_fragments': tips_fragments,
'completed': correct,
......
......@@ -25,6 +25,8 @@
import logging
import uuid
import re
from collections import namedtuple
from lxml import etree
......@@ -40,7 +42,7 @@ 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
from .utils import loader
# Globals ###########################################################
......@@ -48,9 +50,22 @@ from .utils import get_scenarios_from_path, load_resource, render_template
log = logging.getLogger(__name__)
def default_xml_content():
return render_template('templates/xml/mentoring_default.xml', {
'url_name': 'mentoring-{}'.format(uuid.uuid4())})
def _default_xml_content():
return loader.render_template(
'templates/xml/mentoring_default.xml',
{'url_name': 'mentoring-{}'.format(uuid.uuid4())})
def _is_default_xml_content(value):
UUID_PATTERN = '[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}'
DUMMY_UUID = '12345678-1234-1234-1234-123456789abc'
expected = _default_xml_content()
expected = re.sub(UUID_PATTERN, DUMMY_UUID, expected)
value = re.sub(UUID_PATTERN, DUMMY_UUID, value)
return value == expected
# Classes ###########################################################
......@@ -66,6 +81,11 @@ class MentoringBlock(XBlockWithLightChildren, 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 _is_default_xml_content(value)
attempted = Boolean(help="Has the student attempted this mentoring step?",
default=False, scope=Scope.user_state)
completed = Boolean(help="Has the student completed this mentoring step?",
......@@ -79,7 +99,7 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin):
enforce_dependency = Boolean(help="Should the next step be the current block to complete?",
default=False, scope=Scope.content, enforce_type=True)
display_submit = Boolean(help="Allow to submit current block?", default=True, scope=Scope.content)
xml_content = String(help="XML content", default=default_xml_content, scope=Scope.content)
xml_content = String(help="XML content", default=_default_xml_content, scope=Scope.content)
weight = Float(help="Defines the maximum total grade of the block.",
default=1, scope=Scope.content, enforce_type=True)
num_attempts = Integer(help="Number of attempts a user has answered for this questions",
......@@ -127,7 +147,7 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin):
not_instance_of=self.FLOATING_BLOCKS,
)
fragment.add_content(render_template('templates/html/mentoring.html', {
fragment.add_content(loader.render_template('templates/html/mentoring.html', {
'self': self,
'named_children': named_children,
'missing_dependency_url': self.has_missing_dependency and self.next_step_url,
......@@ -145,28 +165,19 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin):
)
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring.js'))
fragment.add_resource(load_resource('templates/html/mentoring_attempts.html'), "text/html")
fragment.add_resource(load_resource('templates/html/mentoring_grade.html'), "text/html")
fragment.add_resource(loader.load_unicode('templates/html/mentoring_attempts.html'), "text/html")
fragment.add_resource(loader.load_unicode('templates/html/mentoring_grade.html'), "text/html")
fragment.initialize_js('MentoringBlock')
return fragment
@XBlock.json_handler
def publish_event(self, data, suffix=''):
try:
event_type = data.pop('event_type')
except KeyError as e:
return {'result': 'error', 'message': 'Missing event_type in JSON data'}
return self._publish_event(event_type, data)
def _publish_event(self, event_type, data):
data['user_id'] = self.scope_ids.user_id
data['component_id'] = self.url_name
self.runtime.publish(self, event_type, data)
return {'result': 'success'}
@property
def additional_publish_event_data(self):
return {
'user_id': self.scope_ids.user_id,
'component_id': self.url_name,
}
@property
def title(self):
......@@ -262,7 +273,7 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin):
raw_score = self.score.raw
self._publish_event('xblock.mentoring.submitted', {
self.publish_event_from_dict('xblock.mentoring.submitted', {
'num_attempts': self.num_attempts,
'submitted_answer': submissions,
'grade': raw_score,
......@@ -327,7 +338,7 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin):
event_data['num_attempts'] = self.num_attempts
event_data['submitted_answer'] = submissions
self._publish_event('xblock.mentoring.assessment.submitted', event_data)
self.publish_event_from_dict('xblock.mentoring.assessment.submitted', event_data)
return {
'completed': completed,
......@@ -383,7 +394,7 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin):
Editing view in Studio
"""
fragment = Fragment()
fragment.add_content(render_template('templates/html/mentoring_edit.html', {
fragment.add_content(loader.render_template('templates/html/mentoring_edit.html', {
'self': self,
'xml_content': self.xml_content,
}))
......@@ -451,4 +462,4 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin):
"""
Scenarios displayed by the workbench. Load them from external (private) repository
"""
return get_scenarios_from_path('templates/xml')
return loader.load_scenarios_from_path('templates/xml')
......@@ -26,7 +26,7 @@
import logging
from .light_children import LightChild, Scope, String
from .utils import render_template
from .utils import loader
# Globals ###########################################################
......@@ -47,7 +47,7 @@ class MentoringMessageBlock(LightChild):
def mentoring_view(self, context=None):
fragment, named_children = self.get_children_fragment(context, view_name='mentoring_view')
fragment.add_content(render_template('templates/html/message.html', {
fragment.add_content(loader.render_template('templates/html/message.html', {
'self': self,
'named_children': named_children,
}))
......
......@@ -28,7 +28,7 @@ import logging
from .light_children import List, Scope, Boolean
from .questionnaire import QuestionnaireAbstractBlock
from .utils import render_template
from .utils import loader
# Globals ###########################################################
......@@ -73,7 +73,7 @@ class MRQBlock(QuestionnaireAbstractBlock):
# Only include tips/results in returned response if we want to display them
if not self.hide_results:
choice_result['completed'] = choice_completed
choice_result['tips'] = render_template('templates/html/tip_choice_group.html', {
choice_result['tips'] = loader.render_template('templates/html/tip_choice_group.html', {
'self': self,
'tips_fragments': choice_tips_fragments,
'completed': choice_completed,
......
......@@ -31,7 +31,7 @@ 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
from .utils import loader
# Globals ###########################################################
......@@ -82,14 +82,14 @@ class QuestionnaireAbstractBlock(LightChild, StepMixin):
template_path = 'templates/html/{}_{}.html'.format(name.lower(), self.type)
render_function = render_js_template if as_template else render_template
render_function = loader.custom_render_js_template if as_template else loader.render_template
html = render_function(template_path, {
'self': self,
'custom_choices': self.custom_choices
})
fragment = Fragment(html)
fragment.add_css(render_template('public/css/questionnaire.css', {
fragment.add_css(loader.render_template('public/css/questionnaire.css', {
'self': self
}))
fragment.add_javascript_url(self.runtime.local_resource_url(self.xblock_container,
......
......@@ -29,7 +29,7 @@ import logging
from xblock.fields import Scope
from .light_children import LightChild, String
from .utils import load_resource, render_template
from .utils import loader
# Globals ###########################################################
......@@ -58,14 +58,14 @@ class MentoringTableBlock(LightChild):
# Load an optional description for the background image, for accessibility
try:
bg_image_description = load_resource('static/text/table-{}.txt'.format(self.type))
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(render_template('templates/html/mentoring-table.html', {
fragment.add_content(loader.render_template('templates/html/mentoring-table.html', {
'self': self,
'columns_frags': columns_frags,
'header_frags': header_frags,
......@@ -101,7 +101,7 @@ class MentoringTableColumnBlock(LightChild):
fragment, named_children = self.get_children_fragment(
context, view_name='mentoring_table_view',
not_instance_of=MentoringTableColumnHeaderBlock)
fragment.add_content(render_template('templates/html/mentoring-table-column.html', {
fragment.add_content(loader.render_template('templates/html/mentoring-table-column.html', {
'self': self,
'named_children': named_children,
}))
......@@ -114,7 +114,7 @@ class MentoringTableColumnBlock(LightChild):
fragment, named_children = self.get_children_fragment(
context, view_name='mentoring_table_header_view',
instance_of=MentoringTableColumnHeaderBlock)
fragment.add_content(render_template('templates/html/mentoring-table-header.html', {
fragment.add_content(loader.render_template('templates/html/mentoring-table-header.html', {
'self': self,
'named_children': named_children,
}))
......
# -*- 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 ###########################################################
import time
from selenium.webdriver.support.ui import WebDriverWait
from workbench import scenarios
from workbench.test.selenium_test import SeleniumTest
from .utils import load_scenarios_from_path
# Classes ###########################################################
class MentoringBaseTest(SeleniumTest):
def setUp(self):
super(MentoringBaseTest, self).setUp()
# Use test scenarios
self.browser.get(self.live_server_url) # Needed to load tests once
scenarios.SCENARIOS.clear()
scenarios_list = load_scenarios_from_path('../tests/integration/xml')
for identifier, title, xml in scenarios_list:
scenarios.add_xml_scenario(identifier, title, xml)
self.addCleanup(scenarios.remove_scenario, identifier)
# Suzy opens the browser to visit the workbench
self.browser.get(self.live_server_url)
# She knows it's the site by the header
header1 = self.browser.find_element_by_css_selector('h1')
self.assertEqual(header1.text, 'XBlock scenarios')
def wait_until_disabled(self, submit):
wait = WebDriverWait(submit, 10)
wait.until(lambda s: not s.is_enabled(), "{} should be disabled".format(submit.text))
def wait_until_clickable(self, submit):
wait = WebDriverWait(submit, 10)
wait.until(lambda s: s.is_displayed() and s.is_enabled(), "{} should be cliclable".format(submit.text))
def wait_until_text_in(self, text, elem):
wait = WebDriverWait(elem, 10)
wait.until(lambda elem: text in elem.text, "{} should be in {}".format(text, elem.text))
def go_to_page(self, page_name, css_selector='div.mentoring'):
"""
Navigate to the page `page_name`, as listed on the workbench home
Returns the DOM element on the visited page located by the `css_selector`
"""
self.browser.get(self.live_server_url)
self.browser.find_element_by_link_text(page_name).click()
time.sleep(1)
mentoring = self.browser.find_element_by_css_selector(css_selector)
return mentoring
......@@ -26,7 +26,7 @@
import logging
from .light_children import LightChild, Scope, String
from .utils import render_template
from .utils import loader
# Globals ###########################################################
......@@ -65,7 +65,7 @@ class TipBlock(LightChild):
Returns a fragment containing the formatted tip
"""
fragment, named_children = self.get_children_fragment({})
fragment.add_content(render_template('templates/html/tip.html', {
fragment.add_content(loader.render_template('templates/html/tip.html', {
'self': self,
'named_children': named_children,
}))
......
......@@ -21,8 +21,6 @@
# "AGPLv3". If not, see <http://www.gnu.org/licenses/>.
#
# Imports ###########################################################
import logging
import os
import pkg_resources
......@@ -32,39 +30,18 @@ from cStringIO import StringIO
from django.template import Context, Template
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
# Globals ###########################################################
log = logging.getLogger(__name__)
# Functions #########################################################
def load_resource(resource_path):
"""
Gets the content of a resource
"""
resource_content = pkg_resources.resource_string(__name__, resource_path)
return unicode(resource_content)
class MentoringResourceLoader(ResourceLoader):
def custom_render_js_template(self, template_path, context={}):
return self.render_js_template(template_path, 'light-child-template', context)
def render_template(template_path, context={}):
"""
Evaluate a template by resource path, applying the provided context
"""
template_str = load_resource(template_path)
template = Template(template_str)
return template.render(Context(context))
def render_js_template(template_path, context={}, id='light-child-template'):
"""
Render a js template.
"""
return u"<script type='text/template' id='{}'>\n{}\n</script>".format(
id,
render_template(template_path, context)
)
loader = MentoringResourceLoader(__name__)
def list2csv(row):
......@@ -78,40 +55,6 @@ def list2csv(row):
return f.read()
def get_scenarios_from_path(scenarios_path, include_identifier=False):
"""
Returns an array of (title, xmlcontent) from files contained in a specified directory,
formatted as expected for the return value of the workbench_scenarios() method
"""
base_fullpath = os.path.dirname(os.path.realpath(__file__))
scenarios_fullpath = os.path.join(base_fullpath, scenarios_path)
scenarios = []
if os.path.isdir(scenarios_fullpath):
for template in os.listdir(scenarios_fullpath):
if not template.endswith('.xml'):
continue
identifier = template[:-4]
title = identifier.replace('_', ' ').title()
template_path = os.path.join(scenarios_path, template)
scenario = unicode(render_template(template_path, {"url_name": identifier}))
if not include_identifier:
scenarios.append((title, scenario))
else:
scenarios.append((identifier, title, scenario))
return scenarios
def load_scenarios_from_path(scenarios_path):
"""
Load all xml files contained in a specified directory, as workbench scenarios
"""
return get_scenarios_from_path(scenarios_path, include_identifier=True)
# Classes ###########################################################
class XBlockWithChildrenFragmentsMixin(object):
def get_children_fragment(self, context, view_name='student_view', instance_of=None,
not_instance_of=None):
......
# -*- 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/>.
#
from xblockutils.base_test import SeleniumBaseTest
class MentoringBaseTest(SeleniumBaseTest):
module_name = __name__
default_css_selector = 'div.mentoring'
......@@ -23,7 +23,7 @@
# Imports ###########################################################
from mentoring.test_base import MentoringBaseTest
from .base_test import MentoringBaseTest
# Classes ###########################################################
......
from mentoring.test_base import MentoringBaseTest
from .base_test import MentoringBaseTest
class MentoringAssessmentTest(MentoringBaseTest):
......
......@@ -23,7 +23,7 @@
# Imports ###########################################################
from mentoring.test_base import MentoringBaseTest
from .base_test import MentoringBaseTest
# Classes ###########################################################
......
......@@ -23,7 +23,7 @@
# Imports ###########################################################
from mentoring.test_base import MentoringBaseTest
from .base_test import MentoringBaseTest
# Classes ###########################################################
......
......@@ -23,7 +23,7 @@
# Imports ###########################################################
from mentoring.test_base import MentoringBaseTest
from .base_test import MentoringBaseTest
# Classes ###########################################################
......
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