Commit 338c6d50 by Braden MacDonald

Replace LightChildren w/ XBlock Children - break all the things

parent 879241be
...@@ -481,29 +481,11 @@ Install to the workbench's virtualenv by running the following command form the ...@@ -481,29 +481,11 @@ Install to the workbench's virtualenv by running the following command form the
pip install -r requirements.txt pip install -r requirements.txt
``` ```
In the main XBlock repository, create the following configuration file
in `workbench/settings_mentoring.py` in the XBlock repository:
```python
from settings import *
INSTALLED_APPS += ('mentoring',)
DATABASES['default']['NAME'] = 'workbench.sqlite'
```
Because this XBlock uses a Django model, you need to sync the database
before starting the workbench. Run this from the XBlock repository
root:
```bash
$ ./manage.py syncdb --settings=workbench.settings_mentoring
```
Running the workbench Running the workbench
--------------------- ---------------------
```bash ```bash
$ ./manage.py runserver 8000 --settings=workbench.settings_mentoring $ ./manage.py runserver 8000
``` ```
Access it at [http://localhost:8000/](http://localhost:8000). Access it at [http://localhost:8000/](http://localhost:8000).
...@@ -515,13 +497,13 @@ From the xblock-mentoring repository root, run the tests with the ...@@ -515,13 +497,13 @@ From the xblock-mentoring repository root, run the tests with the
following command: following command:
```bash ```bash
$ DJANGO_SETTINGS_MODULE="workbench.settings_mentoring" nosetests --with-django $ nosetests --with-django
``` ```
If you want to run only the integration or the unit tests, append the directory to the command. You can also run separate modules in this manner. If you want to run only the integration or the unit tests, append the directory to the command. You can also run separate modules in this manner.
```bash ```bash
$ DJANGO_SETTINGS_MODULE="workbench.settings_mentoring" nosetests --with-django tests/unit $ nosetests --with-django tests/unit
``` ```
If you have not installed the xblock-sdk in the active virtualenv, If you have not installed the xblock-sdk in the active virtualenv,
...@@ -542,16 +524,8 @@ $ cat > templates/xml/my_mentoring_scenario.xml ...@@ -542,16 +524,8 @@ $ cat > templates/xml/my_mentoring_scenario.xml
Restart the workbench to take the new scenarios into account. Restart the workbench to take the new scenarios into account.
If you modified a scenario already loaded in the workbench,
you will also have to purge and rebuild the database:
```bash
rm workbench.sqlite
./manage.py syncdb --settings=workbench.settings_mentoring <<<"no"
```
License License
------- -------
The Image Explorer XBlock is available under the GNU Affero General The Mentoring XBlock is available under the GNU Affero General
Public License (AGPLv3). Public License (AGPLv3).
from .answer import AnswerBlock
from .choice import ChoiceBlock
from .dataexport import MentoringDataExportBlock from .dataexport import MentoringDataExportBlock
from .html import HTMLBlock
from .mcq import MCQBlock
from .mrq import MRQBlock
from .mentoring import MentoringBlock from .mentoring import MentoringBlock
from .message import MentoringMessageBlock
from .table import MentoringTableBlock, MentoringTableColumnBlock, MentoringTableColumnHeaderBlock
from .tip import TipBlock
from .title import TitleBlock
from .header import SharedHeaderBlock
from .answer import AnswerBlock
from .choice import ChoiceBlock
from .html import HTMLBlock
from .mcq import MCQBlock
from .mrq import MRQBlock
from .message import MentoringMessageBlock
from .table import MentoringTableBlock, MentoringTableColumnBlock, MentoringTableColumnHeaderBlock
from .tip import TipBlock
from .title import TitleBlock
from .header import SharedHeaderBlock
...@@ -25,99 +25,116 @@ ...@@ -25,99 +25,116 @@
import logging import logging
from lazy import lazy from xblock.core import XBlock
from xblock.fields import Scope, Boolean, Float, Integer, Reference, String
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
from .light_children import LightChild, Boolean, Scope, String, Integer, Float
from .step import StepMixin from .step import StepMixin
from .models import Answer
from .utils import loader
# Globals ########################################################### # Globals ###########################################################
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
loader = ResourceLoader(__name__)
# Classes ########################################################### # Classes ###########################################################
class AnswerBlock(LightChild, StepMixin):
class AnswerBlock(XBlock, StepMixin):
""" """
A field where the student enters an answer A field where the student enters an answer
Must be included as a child of a mentoring block. Answers are persisted as django model instances Must be included as a child of a mentoring block. Answers are persisted as django model instances
to make them searchable and referenceable across xblocks. to make them searchable and referenceable across xblocks.
""" """
read_only = Boolean(help="Display as a read-only field", default=False, scope=Scope.content) read_only = Boolean(
default_from = String(help="If specified, the name of the answer to get the default value from", help="Display as a read-only field",
default=None, scope=Scope.content) default=False,
min_characters = Integer(help="Minimum number of characters allowed for the answer", scope=Scope.content
default=0, scope=Scope.content) )
question = String(help="Question to ask the student", scope=Scope.content, default="") default_from = Reference(
weight = Float(help="Defines the maximum total grade of the light child block.", help="If specified, get the default value from this answer.",
default=1, scope=Scope.content, enforce_type=True) default=None,
scope=Scope.settings
)
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.settings,
enforce_type=True
)
# This is the internal value of student_input. Don't access directly - use student_input instead.
student_input_raw = String(
scope=Scope.user_state,
default=""
)
@classmethod @classmethod
def init_block_from_node(cls, block, node, attr): def parse_xml(cls, node, runtime, keys, id_generator):
block.light_children = [] block = runtime.construct_xblock_from_class(cls, keys)
for child_id, xml_child in enumerate(node):
# 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': if xml_child.tag == 'question':
block.question = xml_child.text block.question = xml_child.text
else: else:
cls.add_node_as_child(block, xml_child, child_id) block.runtime.add_node_as_child(block, xml_child, id_generator)
for name, value in attr:
setattr(block, name, value)
return block return block
@lazy @property
def student_input(self): def student_input(self):
""" """
Use lazy property instead of XBlock field, as __init__() doesn't support The student input value, or a default which may come from another block.
overwriting field values
""" """
# Only attempt to locate a model object for this block when the answer has a name student_input = self.student_input_raw
if not self.name:
return ''
student_input = self.get_model_object().student_input
# Default value can be set from another answer's current value # Default value can be set from another answer's current value
if not student_input and self.default_from: if not student_input and self.default_from:
student_input = self.get_model_object(name=self.default_from).student_input student_input = self.runtime.get_block(self.default_from).student_input
return student_input return student_input
def mentoring_view(self, context=None): def mentoring_view(self, context=None):
if not self.read_only: if not self.read_only:
html = loader.custom_render_js_template('templates/html/answer_editable.html', { html = loader.render_template('templates/html/answer_editable.html', {
'self': self, 'self': self,
}) })
else: else:
html = loader.custom_render_js_template('templates/html/answer_read_only.html', { html = loader.render_template('templates/html/answer_read_only.html', {
'self': self, 'self': self,
}) })
fragment = Fragment(html) fragment = Fragment(html)
fragment.add_css_url(self.runtime.local_resource_url(self.xblock_container, 'public/css/answer.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/answer.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self.xblock_container, fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/answer.js'))
'public/js/answer.js'))
fragment.initialize_js('AnswerBlock') fragment.initialize_js('AnswerBlock')
return fragment return fragment
def mentoring_table_view(self, context=None): def mentoring_table_view(self, context=None):
html = loader.custom_render_js_template('templates/html/answer_table.html', { html = loader.render_template('templates/html/answer_table.html', {
'self': self, 'self': self,
}) })
fragment = Fragment(html) fragment = Fragment(html)
fragment.add_css_url(self.runtime.local_resource_url(self.xblock_container, 'public/css/answer_table.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/answer_table.css'))
return fragment return fragment
def submit(self, submission): def submit(self, submission):
if not self.read_only: if not self.read_only:
self.student_input = submission[0]['value'].strip() self.student_input_raw = submission[0]['value'].strip()
log.info(u'Answer submitted for`{}`: "{}"'.format(self.name, self.student_input)) log.info(u'Answer submitted for`{}`: "{}"'.format(self.name, self.student_input))
return { return {
'student_input': self.student_input, 'student_input': self.student_input,
...@@ -134,37 +151,6 @@ class AnswerBlock(LightChild, StepMixin): ...@@ -134,37 +151,6 @@ class AnswerBlock(LightChild, StepMixin):
return 'correct' if (self.read_only or answer_length_ok) else 'incorrect' return 'correct' if (self.read_only or answer_length_ok) else 'incorrect'
def save(self): @property
""" def completed(self):
Replicate data changes on the related Django model used for sharing of data accross XBlocks return self.status == 'correct'
"""
super(AnswerBlock, self).save()
# 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:
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')
# TODO: Why do we need to use `xmodule_runtime` and not `runtime`?
student_id = self.xmodule_runtime.anonymous_student_id
course_id = self.xmodule_runtime.course_id
answer_data, created = Answer.objects.get_or_create(
student_id=student_id,
course_id=course_id,
name=name,
)
return answer_data
...@@ -23,35 +23,15 @@ ...@@ -23,35 +23,15 @@
# Imports ########################################################### # Imports ###########################################################
import logging from xblock.core import XBlock
from xblock.fields import Scope, String
from .light_children import LightChild, Scope, String
from .utils import loader, ContextConstants
# Globals ###########################################################
log = logging.getLogger(__name__)
# Classes ########################################################### # Classes ###########################################################
class ChoiceBlock(LightChild):
class ChoiceBlock(XBlock):
""" """
Custom choice of an answer for a MCQ/MRQ Custom choice of an answer for a MCQ/MRQ
""" """
value = String(help="Value of the choice when selected", scope=Scope.content, default="") value = String(help="Value of the choice when selected", scope=Scope.content, default="")
content = String(help="Human-readable version of the choice value", scope=Scope.content, default="") content = String(help="Human-readable version of the choice value", scope=Scope.content, default="")
has_children = True
def render(self):
# return self.content
"""
Returns a fragment containing the formatted tip
"""
fragment, named_children = self.get_children_fragment({ContextConstants.AS_TEMPLATE: False})
fragment.add_content(loader.render_template('templates/html/choice.html', {
'self': self,
'named_children': named_children,
}))
return self.xblock_container.fragment_text_rewriting(fragment)
...@@ -22,38 +22,13 @@ ...@@ -22,38 +22,13 @@
# "AGPLv3". If not, see <http://www.gnu.org/licenses/>. # "AGPLv3". If not, see <http://www.gnu.org/licenses/>.
# #
import logging from .html import HTMLBlock
from lxml import etree
from xblock.fragment import Fragment
from .light_children import LightChild, Scope, String
log = logging.getLogger(__name__)
class SharedHeaderBlock(HTMLBlock):
class SharedHeaderBlock(LightChild):
""" """
A shared header block shown under the title. A shared header block shown under the title.
""" """
FIXED_CSS_CLASS = "shared-header"
content = String(help="HTML content of the header", scope=Scope.content, default="") pass
@classmethod
def init_block_from_node(cls, block, node, attr):
block.light_children = []
node.tag = 'div'
block.content = unicode(etree.tostring(node))
node.tag = 'shared-header'
return block
def student_view(self, context=None):
return Fragment(u"<script type='text/template' id='light-child-template'>\n{}\n</script>".format(
self.content
))
def mentoring_view(self, context=None):
return self.student_view(context)
def mentoring_table_view(self, context=None):
return self.student_view(context)
...@@ -23,55 +23,43 @@ ...@@ -23,55 +23,43 @@
# Imports ########################################################### # Imports ###########################################################
import logging
from lxml import etree from lxml import etree
from xblock.core import XBlock
from xblock.fields import Scope, String
from xblock.fragment import Fragment from xblock.fragment import Fragment
from .light_children import LightChild, Scope, String
# Globals ###########################################################
from .utils import ContextConstants
log = logging.getLogger(__name__)
# Classes ########################################################### # Classes ###########################################################
class HTMLBlock(LightChild): class HTMLBlock(XBlock):
""" """
A simplistic replacement for the HTML XModule, as a light XBlock child Render content as HTML
""" """
FIXED_CSS_CLASS = "html_child"
content = String(help="HTML content", scope=Scope.content, default="") content = String(help="HTML content", scope=Scope.content, default="")
css_class = String(help="CSS Class[es] applied to wrapper div element", scope=Scope.content, default="")
@classmethod @classmethod
def init_block_from_node(cls, block, node, attr): def parse_xml(cls, node, runtime, keys, id_generator):
block.light_children = [] """
Construct this XBlock from the given XML node.
node.tag = 'div' """
node_classes = (cls for cls in [node.get('class', ''), 'html_child'] if cls) block = runtime.construct_xblock_from_class(cls, keys)
node.set('class', " ".join(node_classes))
block.content = unicode(etree.tostring(node))
node.tag = 'html'
return block
def student_view(self, context=None): if node.get('class'): # Older API used "class" property, not "css_class"
as_template = context.get(ContextConstants.AS_TEMPLATE, True) if context is not None else True node.set('css_class', node.get('css_class', node.get('class')))
if as_template: del node.attrib['class']
return Fragment(u"<script type='text/template' id='{}'>\n{}\n</script>".format( block.css_class = node.get('css_class')
'light-child-template',
self.content
))
# bug? got AssertionError if I don't use unicode here. (assert isinstance(content, unicode)) block.content = unicode(node.text or u"")
# Although it is set when constructed? for child in node:
return Fragment(unicode(self.content)) block.content += etree.tostring(child, encoding='unicode')
def mentoring_view(self, context=None): return block
return self.student_view(context)
def mentoring_table_view(self, context=None): def fallback_view(self, view_name, context=None):
return self.student_view(context) """ Default view handler """
css_class = ' '.join(cls for cls in (self.css_class, self.FIXED_CSS_CLASS) if cls)
html = u'<div class="{classes}">{content}</div>'.format(classes=css_class, content=unicode(self.content))
return Fragment(html)
...@@ -25,10 +25,10 @@ ...@@ -25,10 +25,10 @@
import logging import logging
from xblock.fields import Scope, String
from xblockutils.resources import ResourceLoader
from .light_children import Scope, String
from .questionnaire import QuestionnaireAbstractBlock from .questionnaire import QuestionnaireAbstractBlock
from .utils import loader
# Globals ########################################################### # Globals ###########################################################
...@@ -59,7 +59,7 @@ class MCQBlock(QuestionnaireAbstractBlock): ...@@ -59,7 +59,7 @@ class MCQBlock(QuestionnaireAbstractBlock):
if submission in tip.display_with_defaults: if submission in tip.display_with_defaults:
tips_fragments.append(tip.render()) tips_fragments.append(tip.render())
formatted_tips = loader.render_template('templates/html/tip_choice_group.html', { formatted_tips = ResourceLoader(__name__).render_template('templates/html/tip_choice_group.html', {
'self': self, 'self': self,
'tips_fragments': tips_fragments, 'tips_fragments': tips_fragments,
'completed': correct, 'completed': correct,
......
...@@ -23,20 +23,17 @@ ...@@ -23,20 +23,17 @@
# Imports ########################################################### # Imports ###########################################################
import logging
from .light_children import LightChild, Scope, String
from .utils import loader
from xblock.core import XBlock
# Globals ########################################################### from xblock.fields import Scope, String
from xblock.fragment import Fragment
log = logging.getLogger(__name__) from xblockutils.resources import ResourceLoader
# Classes ########################################################### # Classes ###########################################################
class MentoringMessageBlock(LightChild): class MentoringMessageBlock(XBlock):
""" """
A message which can be conditionally displayed at the mentoring block level, A message which can be conditionally displayed at the mentoring block level,
for example upon completion of the block for example upon completion of the block
...@@ -46,9 +43,15 @@ class MentoringMessageBlock(LightChild): ...@@ -46,9 +43,15 @@ class MentoringMessageBlock(LightChild):
has_children = True has_children = True
def mentoring_view(self, context=None): def mentoring_view(self, context=None):
fragment, named_children = self.get_children_fragment(context, view_name='mentoring_view') fragment = Fragment()
fragment.add_content(loader.render_template('templates/html/message.html', { child_content = u""
for child_id in self.children:
child = self.runtime.get_block(child_id)
child_fragment = child.render('mentoring_view', context)
fragment.add_frag_resources(child_fragment)
child_content += child_fragment.content
fragment.add_content(ResourceLoader(__name__).render_template('templates/html/message.html', {
'self': self, 'self': self,
'named_children': named_children, 'child_content': child_content,
})) }))
return fragment return fragment
...@@ -25,10 +25,9 @@ ...@@ -25,10 +25,9 @@
import logging import logging
from xblock.fields import List, Scope, Boolean
from .light_children import List, Scope, Boolean
from .questionnaire import QuestionnaireAbstractBlock from .questionnaire import QuestionnaireAbstractBlock
from .utils import loader from xblockutils.resources import ResourceLoader
# Globals ########################################################### # Globals ###########################################################
...@@ -73,7 +72,7 @@ class MRQBlock(QuestionnaireAbstractBlock): ...@@ -73,7 +72,7 @@ class MRQBlock(QuestionnaireAbstractBlock):
# Only include tips/results in returned response if we want to display them # Only include tips/results in returned response if we want to display them
if not self.hide_results: if not self.hide_results:
choice_result['completed'] = choice_completed choice_result['completed'] = choice_completed
choice_result['tips'] = loader.render_template('templates/html/tip_choice_group.html', { choice_result['tips'] = ResourceLoader(__name__).render_template('templates/html/tip_choice_group.html', {
'self': self, 'self': self,
'tips_fragments': choice_tips_fragments, 'tips_fragments': choice_tips_fragments,
'completed': choice_completed, 'completed': choice_completed,
......
// Underscore.js 1.3.3
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Underscore is freely distributable under the MIT license.
// Portions of Underscore are inspired or borrowed from Prototype,
// Oliver Steele's Functional, and John Resig's Micro-Templating.
// For all details and documentation:
// http://documentcloud.github.com/underscore
(function(){function r(a,c,d){if(a===c)return 0!==a||1/a==1/c;if(null==a||null==c)return a===c;a._chain&&(a=a._wrapped);c._chain&&(c=c._wrapped);if(a.isEqual&&b.isFunction(a.isEqual))return a.isEqual(c);if(c.isEqual&&b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return!1;switch(e){case "[object String]":return a==""+c;case "[object Number]":return a!=+a?c!=+c:0==a?1/a==1/c:a==+c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source==
c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if("object"!=typeof a||"object"!=typeof c)return!1;for(var f=d.length;f--;)if(d[f]==a)return!0;d.push(a);var f=0,g=!0;if("[object Array]"==e){if(f=a.length,g=f==c.length)for(;f--&&(g=f in a==f in c&&r(a[f],c[f],d)););}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return!1;for(var h in a)if(b.has(a,h)&&(f++,!(g=b.has(c,h)&&r(a[h],c[h],d))))break;if(g){for(h in c)if(b.has(c,h)&&!f--)break;
g=!f}}d.pop();return g}var s=this,I=s._,o={},k=Array.prototype,p=Object.prototype,i=k.slice,J=k.unshift,l=p.toString,K=p.hasOwnProperty,y=k.forEach,z=k.map,A=k.reduce,B=k.reduceRight,C=k.filter,D=k.every,E=k.some,q=k.indexOf,F=k.lastIndexOf,p=Array.isArray,L=Object.keys,t=Function.prototype.bind,b=function(a){return new m(a)};"undefined"!==typeof exports?("undefined"!==typeof module&&module.exports&&(exports=module.exports=b),exports._=b):s._=b;b.VERSION="1.3.3";var j=b.each=b.forEach=function(a,
c,d){if(a!=null)if(y&&a.forEach===y)a.forEach(c,d);else if(a.length===+a.length)for(var e=0,f=a.length;e<f;e++){if(e in a&&c.call(d,a[e],e,a)===o)break}else for(e in a)if(b.has(a,e)&&c.call(d,a[e],e,a)===o)break};b.map=b.collect=function(a,c,b){var e=[];if(a==null)return e;if(z&&a.map===z)return a.map(c,b);j(a,function(a,g,h){e[e.length]=c.call(b,a,g,h)});if(a.length===+a.length)e.length=a.length;return e};b.reduce=b.foldl=b.inject=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(A&&
a.reduce===A){e&&(c=b.bind(c,e));return f?a.reduce(c,d):a.reduce(c)}j(a,function(a,b,i){if(f)d=c.call(e,d,a,b,i);else{d=a;f=true}});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(B&&a.reduceRight===B){e&&(c=b.bind(c,e));return f?a.reduceRight(c,d):a.reduceRight(c)}var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect=function(a,
c,b){var e;G(a,function(a,g,h){if(c.call(b,a,g,h)){e=a;return true}});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(C&&a.filter===C)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(D&&a.every===D)return a.every(c,b);j(a,function(a,g,h){if(!(e=e&&c.call(b,
a,g,h)))return o});return!!e};var G=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(E&&a.some===E)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return o});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;if(q&&a.indexOf===q)return a.indexOf(c)!=-1;return b=G(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck=
function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b<e.computed&&
(e={value:a,computed:b})});return e.value};b.shuffle=function(a){var b=[],d;j(a,function(a,f){d=Math.floor(Math.random()*(f+1));b[f]=b[d];b[d]=a});return b};b.sortBy=function(a,c,d){var e=b.isFunction(c)?c:function(a){return a[c]};return b.pluck(b.map(a,function(a,b,c){return{value:a,criteria:e.call(d,a,b,c)}}).sort(function(a,b){var c=a.criteria,d=b.criteria;return c===void 0?1:d===void 0?-1:c<d?-1:c>d?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};
j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e<f;){var g=e+f>>1;d(a[g])<d(c)?e=g+1:f=g}return e};b.toArray=function(a){return!a?[]:b.isArray(a)||b.isArguments(a)?i.call(a):a.toArray&&b.isFunction(a.toArray)?a.toArray():b.values(a)};b.size=function(a){return b.isArray(a)?a.length:b.keys(a).length};b.first=b.head=b.take=function(a,b,d){return b!=null&&!d?i.call(a,0,b):a[0]};b.initial=function(a,b,d){return i.call(a,
0,a.length-(b==null||d?1:b))};b.last=function(a,b,d){return b!=null&&!d?i.call(a,Math.max(a.length-b,0)):a[a.length-1]};b.rest=b.tail=function(a,b,d){return i.call(a,b==null||d?1:b)};b.compact=function(a){return b.filter(a,function(a){return!!a})};b.flatten=function(a,c){return b.reduce(a,function(a,e){if(b.isArray(e))return a.concat(c?e:b.flatten(e));a[a.length]=e;return a},[])};b.without=function(a){return b.difference(a,i.call(arguments,1))};b.uniq=b.unique=function(a,c,d){var d=d?b.map(a,d):a,
e=[];a.length<3&&(c=true);b.reduce(d,function(d,g,h){if(c?b.last(d)!==g||!d.length:!b.include(d,g)){d.push(g);e.push(a[h])}return d},[]);return e};b.union=function(){return b.uniq(b.flatten(arguments,true))};b.intersection=b.intersect=function(a){var c=i.call(arguments,1);return b.filter(b.uniq(a),function(a){return b.every(c,function(c){return b.indexOf(c,a)>=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1),true);return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=
i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e<c;e++)d[e]=b.pluck(a,""+e);return d};b.indexOf=function(a,c,d){if(a==null)return-1;var e;if(d){d=b.sortedIndex(a,c);return a[d]===c?d:-1}if(q&&a.indexOf===q)return a.indexOf(c);d=0;for(e=a.length;d<e;d++)if(d in a&&a[d]===c)return d;return-1};b.lastIndexOf=function(a,b){if(a==null)return-1;if(F&&a.lastIndexOf===F)return a.lastIndexOf(b);for(var d=a.length;d--;)if(d in a&&a[d]===b)return d;return-1};b.range=function(a,b,d){if(arguments.length<=
1){b=a||0;a=0}for(var d=arguments[2]||1,e=Math.max(Math.ceil((b-a)/d),0),f=0,g=Array(e);f<e;){g[f++]=a;a=a+d}return g};var H=function(){};b.bind=function(a,c){var d,e;if(a.bind===t&&t)return t.apply(a,i.call(arguments,1));if(!b.isFunction(a))throw new TypeError;e=i.call(arguments,2);return d=function(){if(!(this instanceof d))return a.apply(c,e.concat(i.call(arguments)));H.prototype=a.prototype;var b=new H,g=a.apply(b,e.concat(i.call(arguments)));return Object(g)===g?g:b}};b.bindAll=function(a){var c=
i.call(arguments,1);c.length==0&&(c=b.functions(a));j(c,function(c){a[c]=b.bind(a[c],a)});return a};b.memoize=function(a,c){var d={};c||(c=b.identity);return function(){var e=c.apply(this,arguments);return b.has(d,e)?d[e]:d[e]=a.apply(this,arguments)}};b.delay=function(a,b){var d=i.call(arguments,2);return setTimeout(function(){return a.apply(null,d)},b)};b.defer=function(a){return b.delay.apply(b,[a,1].concat(i.call(arguments,1)))};b.throttle=function(a,c){var d,e,f,g,h,i,j=b.debounce(function(){h=
g=false},c);return function(){d=this;e=arguments;f||(f=setTimeout(function(){f=null;h&&a.apply(d,e);j()},c));g?h=true:i=a.apply(d,e);j();g=true;return i}};b.debounce=function(a,b,d){var e;return function(){var f=this,g=arguments;d&&!e&&a.apply(f,g);clearTimeout(e);e=setTimeout(function(){e=null;d||a.apply(f,g)},b)}};b.once=function(a){var b=false,d;return function(){if(b)return d;b=true;return d=a.apply(this,arguments)}};b.wrap=function(a,b){return function(){var d=[a].concat(i.call(arguments,0));
return b.apply(this,d)}};b.compose=function(){var a=arguments;return function(){for(var b=arguments,d=a.length-1;d>=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=L||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&
c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.pick=function(a){var c={};j(b.flatten(i.call(arguments,1)),function(b){b in a&&(c[b]=a[b])});return c};b.defaults=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return r(a,b,[])};b.isEmpty=
function(a){if(a==null)return true;if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=p||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)};b.isArguments=function(a){return l.call(a)=="[object Arguments]"};b.isArguments(arguments)||(b.isArguments=function(a){return!(!a||!b.has(a,"callee"))});b.isFunction=function(a){return l.call(a)=="[object Function]"};
b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isFinite=function(a){return b.isNumber(a)&&isFinite(a)};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"};b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a,
b){return K.call(a,b)};b.noConflict=function(){s._=I;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e<a;e++)b.call(d,e)};b.escape=function(a){return(""+a).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;").replace(/\//g,"&#x2F;")};b.result=function(a,c){if(a==null)return null;var d=a[c];return b.isFunction(d)?d.call(a):d};b.mixin=function(a){j(b.functions(a),function(c){M(c,b[c]=a[c])})};var N=0;b.uniqueId=
function(a){var b=N++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var u=/.^/,n={"\\":"\\","'":"'",r:"\r",n:"\n",t:"\t",u2028:"\u2028",u2029:"\u2029"},v;for(v in n)n[n[v]]=v;var O=/\\|'|\r|\n|\t|\u2028|\u2029/g,P=/\\(\\|'|r|n|t|u2028|u2029)/g,w=function(a){return a.replace(P,function(a,b){return n[b]})};b.template=function(a,c,d){d=b.defaults(d||{},b.templateSettings);a="__p+='"+a.replace(O,function(a){return"\\"+n[a]}).replace(d.escape||
u,function(a,b){return"'+\n_.escape("+w(b)+")+\n'"}).replace(d.interpolate||u,function(a,b){return"'+\n("+w(b)+")+\n'"}).replace(d.evaluate||u,function(a,b){return"';\n"+w(b)+"\n;__p+='"})+"';\n";d.variable||(a="with(obj||{}){\n"+a+"}\n");var a="var __p='';var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n"+a+"return __p;\n",e=new Function(d.variable||"obj","_",a);if(c)return e(c,b);c=function(a){return e.call(this,a,b)};c.source="function("+(d.variable||"obj")+"){\n"+a+"}";return c};
b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var x=function(a,c){return c?b(a).chain():a},M=function(a,c){m.prototype[a]=function(){var a=i.call(arguments);J.call(a,this._wrapped);return x(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return x(d,
this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return x(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain=true;return this};m.prototype.value=function(){return this._wrapped}}).call(this);
...@@ -23,25 +23,21 @@ ...@@ -23,25 +23,21 @@
# Imports ########################################################### # Imports ###########################################################
import logging
from xblock.core import XBlock
from xblock.fields import Scope, String, Float
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
from .choice import ChoiceBlock from .choice import ChoiceBlock
from .step import StepMixin from .step import StepMixin
from .light_children import LightChild, Scope, String, Float
from .tip import TipBlock from .tip import TipBlock
from .utils import loader, ContextConstants
# Globals ###########################################################
log = logging.getLogger(__name__)
# Classes ########################################################### # Classes ###########################################################
class QuestionnaireAbstractBlock(LightChild, StepMixin):
class QuestionnaireAbstractBlock(XBlock, StepMixin):
""" """
An abstract class used for MCQ/MRQ blocks An abstract class used for MCQ/MRQ blocks
...@@ -56,34 +52,36 @@ class QuestionnaireAbstractBlock(LightChild, StepMixin): ...@@ -56,34 +52,36 @@ class QuestionnaireAbstractBlock(LightChild, StepMixin):
default=1, scope=Scope.content, enforce_type=True) default=1, scope=Scope.content, enforce_type=True)
valid_types = ('choices') valid_types = ('choices')
has_children = True
@classmethod @classmethod
def init_block_from_node(cls, block, node, attr): def parse_xml(cls, node, runtime, keys, id_generator):
block.light_children = [] block = runtime.construct_xblock_from_class(cls, keys)
for child_id, xml_child in enumerate(node):
# 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': if xml_child.tag == 'question':
block.question = xml_child.text block.question = xml_child.text
elif xml_child.tag == 'message' and xml_child.get('type') == 'on-submit': elif xml_child.tag == 'message' and xml_child.get('type') == 'on-submit':
block.message = (xml_child.text or '').strip() block.message = (xml_child.text or '').strip()
else: else:
cls.add_node_as_child(block, xml_child, child_id) block.runtime.add_node_as_child(block, xml_child, id_generator)
for name, value in attr:
setattr(block, name, value)
return block return block
def student_view(self, context=None): def student_view(self, context=None):
name = self.__class__.__name__ name = getattr(self, "unmixed_class", self.__class__).__name__
as_template = context.get(ContextConstants.AS_TEMPLATE, True) if context is not None else True
if str(self.type) not in self.valid_types: if str(self.type) not in self.valid_types:
raise ValueError(u'Invalid value for {}.type: `{}`'.format(name, self.type)) raise ValueError(u'Invalid value for {}.type: `{}`'.format(name, self.type))
template_path = 'templates/html/{}_{}.html'.format(name.lower(), self.type) template_path = 'templates/html/{}_{}.html'.format(name.lower(), self.type)
loader = ResourceLoader(__name__)
render_function = loader.custom_render_js_template if as_template else loader.render_template html = loader.render_template(template_path, {
html = render_function(template_path, {
'self': self, 'self': self,
'custom_choices': self.custom_choices 'custom_choices': self.custom_choices
}) })
...@@ -92,8 +90,7 @@ class QuestionnaireAbstractBlock(LightChild, StepMixin): ...@@ -92,8 +90,7 @@ class QuestionnaireAbstractBlock(LightChild, StepMixin):
fragment.add_css(loader.render_template('public/css/questionnaire.css', { fragment.add_css(loader.render_template('public/css/questionnaire.css', {
'self': self 'self': self
})) }))
fragment.add_javascript_url(self.runtime.local_resource_url(self.xblock_container, fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/questionnaire.js'))
'public/js/questionnaire.js'))
fragment.initialize_js(name) fragment.initialize_js(name)
return fragment return fragment
...@@ -103,7 +100,8 @@ class QuestionnaireAbstractBlock(LightChild, StepMixin): ...@@ -103,7 +100,8 @@ class QuestionnaireAbstractBlock(LightChild, StepMixin):
@property @property
def custom_choices(self): def custom_choices(self):
custom_choices = [] custom_choices = []
for child in self.get_children_objects(): for child_id in self.children:
child = self.runtime.get_block(child_id)
if isinstance(child, ChoiceBlock): if isinstance(child, ChoiceBlock):
custom_choices.append(child) custom_choices.append(child)
return custom_choices return custom_choices
...@@ -113,7 +111,8 @@ class QuestionnaireAbstractBlock(LightChild, StepMixin): ...@@ -113,7 +111,8 @@ class QuestionnaireAbstractBlock(LightChild, StepMixin):
Returns the tips contained in this block Returns the tips contained in this block
""" """
tips = [] tips = []
for child in self.get_children_objects(): for child_id in self.children:
child = self.runtime.get_block(child_id)
if isinstance(child, TipBlock): if isinstance(child, TipBlock):
tips.append(child) tips.append(child)
return tips return tips
......
class StepParentMixin(object):
"""
An XBlock mixin for a parent block containing Step children
"""
@property
def steps(self):
"""
Generator returning the usage_id for all of this XBlock's
children that are "Steps"
"""
for child_id in self.children:
child = self.runtime.get_block(child_id)
if isinstance(child, StepMixin):
yield child_id
class StepMixin(object):
"""
An XBlock mixin for a child block that is a "Step"
"""
@property
def step_number(self):
return list(self.get_parent().steps).index(self.scope_ids.usage_id) + 1
@property
def lonely_step(self):
if self.scope_ids.usage_id not in self.get_parent().steps:
raise ValueError("Step's parent should contain Step", self, self.get_parent().steps)
return len(self.get_parent().steps) == 1
...@@ -24,22 +24,22 @@ ...@@ -24,22 +24,22 @@
# Imports ########################################################### # Imports ###########################################################
import errno import errno
import logging
from xblock.fields import Scope from xblock.core import XBlock
from xblock.fields import Scope, String
from .light_children import LightChild, String from xblock.fragment import Fragment
from .utils import loader
from xblockutils.resources import ResourceLoader
loader = ResourceLoader(__name__)
# Globals ########################################################### # Globals ###########################################################
log = logging.getLogger(__name__) loader = ResourceLoader(__name__)
# Classes ########################################################### # Classes ###########################################################
class MentoringTableBlock(LightChild):
class MentoringTableBlock(XBlock):
""" """
Table-type display of information from mentoring blocks Table-type display of information from mentoring blocks
...@@ -50,11 +50,19 @@ class MentoringTableBlock(LightChild): ...@@ -50,11 +50,19 @@ class MentoringTableBlock(LightChild):
has_children = True has_children = True
def student_view(self, context): def student_view(self, context):
fragment, columns_frags = self.get_children_fragment(context, view_name='mentoring_table_view') fragment = Fragment()
f, header_frags = self.get_children_fragment(context, view_name='mentoring_table_header_view') columns_frags = []
header_frags = []
bg_image_url = self.runtime.local_resource_url(self.xblock_container, for child_id in self.children:
'public/img/{}-bg.png'.format(self.type)) 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 # Load an optional description for the background image, for accessibility
try: try:
...@@ -72,12 +80,9 @@ class MentoringTableBlock(LightChild): ...@@ -72,12 +80,9 @@ class MentoringTableBlock(LightChild):
'bg_image_url': bg_image_url, 'bg_image_url': bg_image_url,
'bg_image_description': bg_image_description, 'bg_image_description': bg_image_description,
})) }))
fragment.add_css_url(self.runtime.local_resource_url(self.xblock_container, fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/mentoring-table.css'))
'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.xblock_container, fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring-table.js'))
'public/js/vendor/jquery-shorten.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self.xblock_container,
'public/js/mentoring-table.js'))
fragment.initialize_js('MentoringTableBlock') fragment.initialize_js('MentoringTableBlock')
return fragment return fragment
...@@ -87,47 +92,66 @@ class MentoringTableBlock(LightChild): ...@@ -87,47 +92,66 @@ class MentoringTableBlock(LightChild):
return self.student_view(context) return self.student_view(context)
class MentoringTableColumnBlock(LightChild): class MentoringTableColumnBlock(XBlock):
""" """
Individual column of a mentoring table Individual column of a mentoring table
""" """
header = String(help="Header of the column", scope=Scope.content, default=None) header = String(help="Header of the column", scope=Scope.content, default=None)
has_children = True has_children = True
def mentoring_table_view(self, context): def _render_table_view(self, view_name, id_filter, template, context):
""" fragment = Fragment()
The content of the column named_children = []
""" for child_id in self.children:
fragment, named_children = self.get_children_fragment( if id_filter(child_id):
context, view_name='mentoring_table_view', child = self.runtime.get_block(child_id)
not_instance_of=MentoringTableColumnHeaderBlock) child_frag = child.render(view_name, context)
fragment.add_content(loader.render_template('templates/html/mentoring-table-column.html', { 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, 'self': self,
'named_children': named_children, 'named_children': named_children,
})) }))
return fragment 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 issubclass(self._get_child_class(child_id), MentoringTableColumnHeaderBlock),
template='mentoring-table-column.html',
context=context
)
def mentoring_table_header_view(self, context): def mentoring_table_header_view(self, context):
""" """
The content of the column's header The content of the column's header
""" """
fragment, named_children = self.get_children_fragment( return self._render_table_view(
context, view_name='mentoring_table_header_view', view_name='mentoring_table_header_view',
instance_of=MentoringTableColumnHeaderBlock) id_filter=lambda child_id: issubclass(self._get_child_class(child_id), MentoringTableColumnHeaderBlock),
fragment.add_content(loader.render_template('templates/html/mentoring-table-header.html', { template='mentoring-table-header.html',
'self': self, context=context
'named_children': named_children, )
}))
return fragment def _get_child_class(self, child_id):
"""
Helper method to get a block type from a usage_id without loading the block.
Returns the XBlock subclass of the child block.
"""
type_name = self.runtime.id_reader.get_block_type(self.runtime.id_reader.get_definition_id(child_id))
return self.runtime.load_block_type(type_name)
class MentoringTableColumnHeaderBlock(LightChild):
class MentoringTableColumnHeaderBlock(XBlock):
""" """
Header content for a given column Header content for a given column
""" """
content = String(help="Body of the header", scope=Scope.content, default='') content = String(help="Body of the header", scope=Scope.content, default='')
def mentoring_table_header_view(self, context): def mentoring_table_header_view(self, context):
fragment = super(MentoringTableColumnHeaderBlock, self).children_view(context) return Fragment(unicode(self.content))
fragment.add_content(unicode(self.content))
return fragment
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
<div class="choice-result fa icon-2x"></div> <div class="choice-result fa icon-2x"></div>
<label class="choice-label"> <label class="choice-label">
<input class="choice-selector" type="radio" name="{{ self.name }}" value="{{ choice.value }}"{% if self.student_choice == choice.value %} checked{% endif %} /> <input class="choice-selector" type="radio" name="{{ self.name }}" value="{{ choice.value }}"{% if self.student_choice == choice.value %} checked{% endif %} />
{{ choice.render.body_html|safe }} {{ choice.content|safe }}
</label> </label>
<div class="choice-tips"></div> <div class="choice-tips"></div>
</div> </div>
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
<div class="choice"> <div class="choice">
<div class="choice-result fa icon-2x"></div> <div class="choice-result fa icon-2x"></div>
<label><input type="radio" name="{{ self.name }}" value="{{ choice.value }}"{% if self.student_choice == '{{ choice.value }}' %} checked{% endif %} /> <label><input type="radio" name="{{ self.name }}" value="{{ choice.value }}"{% if self.student_choice == '{{ choice.value }}' %} checked{% endif %} />
{{ choice.render.body_html|safe }} {{ choice.content|safe }}
</label> </label>
<div class="choice-tips"></div> <div class="choice-tips"></div>
</div> </div>
......
<div class="message {{ self.type }}"> <div class="message {{ self.type }}">
{% for name, c in named_children %}
{{c.body_html|safe}}
{% endfor %}
{% if self.content %} {% if self.content %}
<p>{{ self.content }} <p>{{ self.content }}</p>
{% endif %} {% endif %}
{{ child_content|safe }}
</div> </div>
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
<input class="choice-selector" type="checkbox" name="{{ self.name }}" <input class="choice-selector" type="checkbox" name="{{ self.name }}"
value="{{ choice.value }}" value="{{ choice.value }}"
{% if choice.value in self.student_choices %} checked{% endif %} /> {% if choice.value in self.student_choices %} checked{% endif %} />
{{ choice.render.body_html|safe }} {{ choice.content|safe }}
</label> </label>
<div class="choice-tips"></div> <div class="choice-tips"></div>
</div> </div>
......
<div
class="tip"
{% if width %}data-width="{{width}}"{% endif %}
{% if height %}data-height="{{height}}"{% endif %}
>
{% if self.content %}
<p>{{ self.content }}</p>
{% endif %}
{{ child_content|safe }}
</div>
...@@ -23,16 +23,11 @@ ...@@ -23,16 +23,11 @@
# Imports ########################################################### # Imports ###########################################################
import logging
from .light_children import LightChild, Scope, String
from .utils import loader
# Globals ###########################################################
log = logging.getLogger(__name__)
from xblock.core import XBlock
from xblock.fields import Scope, String
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
# Functions ######################################################### # Functions #########################################################
...@@ -48,7 +43,7 @@ def commas_to_set(commas_str): ...@@ -48,7 +43,7 @@ def commas_to_set(commas_str):
# Classes ########################################################### # Classes ###########################################################
class TipBlock(LightChild): class TipBlock(XBlock):
""" """
Each choice can define a tip depending on selection Each choice can define a tip depending on selection
""" """
...@@ -64,12 +59,18 @@ class TipBlock(LightChild): ...@@ -64,12 +59,18 @@ class TipBlock(LightChild):
""" """
Returns a fragment containing the formatted tip Returns a fragment containing the formatted tip
""" """
fragment, named_children = self.get_children_fragment({}) fragment = Fragment()
fragment.add_content(loader.render_template('templates/html/tip.html', { child_content = u""
for child_id in self.children:
child = self.runtime.get_block(child_id)
child_fragment = child.render('mentoring_view', {})
fragment.add_frag_resources(child_fragment)
child_content += child_fragment.content
fragment.add_content(ResourceLoader(__name__).render_template('templates/html/tip.html', {
'self': self, 'self': self,
'named_children': named_children, 'child_content': child_content,
})) }))
return self.xblock_container.fragment_text_rewriting(fragment) return fragment # TODO: fragment_text_rewriting
@property @property
def display_with_defaults(self): def display_with_defaults(self):
......
...@@ -23,18 +23,13 @@ ...@@ -23,18 +23,13 @@
# Imports ########################################################### # Imports ###########################################################
import logging from xblock.core import XBlock
from xblock.fields import Scope, String
from .light_children import LightChild, Scope, String
# Globals ###########################################################
log = logging.getLogger(__name__)
# Classes ########################################################### # Classes ###########################################################
class TitleBlock(LightChild): class TitleBlock(XBlock):
""" """
A simple html representation of a title, with the mentoring weight. A simple html representation of a title, with the mentoring weight.
""" """
......
...@@ -30,18 +30,30 @@ from webob import Response ...@@ -30,18 +30,30 @@ from webob import Response
from xblock.core import XBlock from xblock.core import XBlock
from xblock.fields import String, Scope from xblock.fields import String, Scope
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
from .models import Answer
from .utils import list2csv, loader
# Globals ########################################################### # Globals ###########################################################
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# Utils ###########################################################
def list2csv(row):
"""
Convert a list to a CSV string (single row)
"""
f = StringIO()
writer = unicodecsv.writer(f, encoding='utf-8')
writer.writerow(row)
f.seek(0)
return f.read()
# Classes ########################################################### # Classes ###########################################################
class MentoringDataExportBlock(XBlock): class MentoringDataExportBlock(XBlock):
""" """
An XBlock allowing the instructor team to export all the student answers as a CSV file An XBlock allowing the instructor team to export all the student answers as a CSV file
...@@ -50,7 +62,7 @@ class MentoringDataExportBlock(XBlock): ...@@ -50,7 +62,7 @@ class MentoringDataExportBlock(XBlock):
scope=Scope.settings) scope=Scope.settings)
def student_view(self, context): def student_view(self, context):
html = loader.render_template('templates/html/dataexport.html', { html = ResourceLoader(__name__).render_template('templates/html/dataexport.html', {
'self': self, 'self': self,
}) })
......
# -*- 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 logging
import json
from lazy import lazy
from weakref import WeakKeyDictionary
from StringIO import StringIO
from lxml import etree
from django.core.urlresolvers import reverse
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
try:
from xmodule_modifiers import replace_jump_to_id_urls
except:
# TODO-WORKBENCH-WORKAROUND: To allow to load from the workbench
replace_jump_to_id_urls = lambda a, b, c, d, frag, f: frag
from .utils import XBlockWithChildrenFragmentsMixin
# Globals ###########################################################
log = logging.getLogger(__name__)
# Classes ###########################################################
class LightChildrenMixin(XBlockWithChildrenFragmentsMixin):
"""
Allows to use lightweight children on a given XBlock, which will
have a similar behavior but will not be instanciated as full-fledged
XBlocks, which aren't correctly supported as children
TODO: Replace this once the support for XBlock children has matured
by a mixin implementing the following abstractions, used to keep
code reusable in the XBlocks:
* get_children_objects()
* Functionality of XBlockWithChildrenFragmentsMixin
* self.xblock_container for when we need a real XBlock reference
Other changes caused by LightChild use:
* overrides of `parse_xml()` have been replaced by overrides of
`init_block_from_node()` on LightChildren
* fields on LightChild don't have any persistence
"""
@classmethod
def parse_xml(cls, node, runtime, keys, id_generator):
log.debug('parse_xml called')
block = runtime.construct_xblock_from_class(cls, keys)
cls.init_block_from_node(block, node, node.items())
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
def init_block_from_node(cls, block, node, attr):
block.light_children = []
for child_id, xml_child in enumerate(node):
cls.add_node_as_child(block, xml_child, child_id)
for name, value in attr:
setattr(block, name, value)
return block
@classmethod
def add_node_as_child(cls, block, xml_child, child_id):
if xml_child.tag is etree.Comment:
return
# Instantiate child
child_class = cls.get_class_by_element(xml_child.tag)
child = child_class(block)
child.name = u'{}_{}'.format(block.name, child_id)
# Add any children the child may itself have
child_class.init_block_from_node(child, xml_child, xml_child.items())
text = xml_child.text
if text:
text = text.strip()
if text:
child.content = text
block.light_children.append(child)
@classmethod
def get_class_by_element(cls, xml_tag):
return LightChild.load_class(xml_tag)
def load_children_from_xml_content(self):
"""
Load light children from the `xml_content` attribute
"""
self.light_children = []
no_content = (not hasattr(self, 'xml_content') or not self.xml_content
or callable(self.xml_content))
if no_content:
return
parser = etree.XMLParser(remove_comments=True)
node = etree.parse(StringIO(self.xml_content), parser=parser).getroot()
LightChildrenMixin.init_block_from_node(self, node, node.items())
def get_children_objects(self):
"""
Replacement for ```[self.runtime.get_block(child_id) for child_id in self.children]```
"""
return self.light_children
def render_child(self, child, view_name, context):
"""
Replacement for ```self.runtime.render_child()```
"""
frag = getattr(child, view_name)(context)
frag.content = u'<div class="xblock-light-child" name="{}" data-type="{}">{}</div>'.format(
child.name, child.__class__.__name__, frag.content)
return frag
def get_children_fragment(self, context, view_name='student_view', instance_of=None,
not_instance_of=None):
fragment = Fragment()
named_child_frags = []
for child in self.get_children_objects():
if instance_of is not None and not isinstance(child, instance_of):
continue
if not_instance_of is not None and isinstance(child, not_instance_of):
continue
frag = self.render_child(child, view_name, context)
fragment.add_frag_resources(frag)
named_child_frags.append((child.name, frag))
return fragment, named_child_frags
class XBlockWithLightChildren(LightChildrenMixin, XBlock, PublishEventMixin):
"""
XBlock base class with support for LightChild
"""
def __init__(self, *args, **kwargs):
super(XBlockWithLightChildren, self).__init__(*args, **kwargs)
self.xblock_container = self
self.load_children_from_xml_content()
@XBlock.json_handler
def view(self, data, suffix=''):
"""
Current HTML view of the XBlock, for refresh by client
"""
frag = self.student_view({})
frag = self.fragment_text_rewriting(frag)
return {
'html': frag.content,
}
def fragment_text_rewriting(self, fragment):
"""
Do replacements like `/jump_to_id` URL rewriting in the provided text
"""
# TODO: Why do we need to use `xmodule_runtime` and not `runtime`?
try:
course_id = self.xmodule_runtime.course_id
except AttributeError:
# TODO-WORKBENCH-WORKAROUND: To allow to load from the workbench
course_id = 'sample-course'
try:
jump_to_url = reverse('jump_to_id', kwargs={'course_id': course_id, 'module_id': ''})
except:
# TODO-WORKBENCH-WORKAROUND: To allow to load from the workbench
jump_to_url = '/jump_to_id'
fragment = replace_jump_to_id_urls(course_id, jump_to_url, self, 'student_view', fragment, {})
return fragment
class LightChild(Plugin, LightChildrenMixin):
"""
Base class for the light children
"""
entry_point = 'xblock.light_children'
block_type = None
def __init__(self, parent):
self.parent = parent
try:
self.location = parent.location
except AttributeError:
self.location = None
self.scope_ids = parent.scope_ids
self.xblock_container = parent.xblock_container
self._student_data_loaded = False
@property
def runtime(self):
return self.parent.runtime
@property
def xmodule_runtime(self):
try:
xmodule_runtime = self.parent.xmodule_runtime
except AttributeError:
# TODO-WORKBENCH-WORKAROUND: To allow to load from the workbench
class xmodule_runtime(object):
course_id = 'sample-course'
anonymous_student_id = 'student1'
xmodule_runtime = xmodule_runtime()
return xmodule_runtime
@lazy
def student_data(self):
"""
Use lazy property instead of XBlock field, as __init__() doesn't support
overwriting field values
"""
if not self.name:
return ''
student_data = self.get_lightchild_model_object().student_data
return student_data
def load_student_data(self):
"""
Load the student data from the database.
"""
if self._student_data_loaded:
return
fields = self.get_fields_to_save()
if not fields or not self.student_data:
return
student_data = json.loads(self.student_data)
for field in fields:
if field in student_data:
setattr(self, field, student_data[field])
self._student_data_loaded = True
@classmethod
def get_fields_to_save(cls):
"""
Returns a list of all LightChildField of the class. Used for saving student data.
"""
return []
def save(self):
"""
Replicate data changes on the related Django model used for sharing of data accross XBlocks
"""
# Save all children
for child in self.get_children_objects():
child.save()
self.student_data = {}
# Get All LightChild fields to save
for field in self.get_fields_to_save():
self.student_data[field] = getattr(self, field)
if self.name:
lightchild_data = self.get_lightchild_model_object()
if lightchild_data.student_data != self.student_data:
lightchild_data.student_data = json.dumps(self.student_data)
lightchild_data.save()
def get_lightchild_model_object(self, name=None):
"""
Fetches the LightChild model object for the lightchild named `name`
"""
if not name:
name = self.name
if not name:
raise ValueError('LightChild.name field need to be set to a non-null/empty value')
student_id = self.xmodule_runtime.anonymous_student_id
course_id = self.xmodule_runtime.course_id
url_name = "%s-%s" % (self.xblock_container.url_name, name)
lightchild_data, created = LightChildModel.objects.get_or_create(
student_id=student_id,
course_id=course_id,
name=url_name,
)
return lightchild_data
def local_resource_url(self, block, uri):
return self.runtime.local_resource_url(block, uri, block_type=self.block_type)
class LightChildField(object):
"""
Fake field with no persistence - allows to keep XBlocks fields definitions on LightChild
"""
def __init__(self, *args, **kwargs):
self.default = kwargs.get('default', '')
self.data = WeakKeyDictionary()
def __get__(self, instance, name):
# A LightChildField can depend on student_data
instance.load_student_data()
return self.data.get(instance, self.default)
def __set__(self, instance, value):
self.data[instance] = value
class String(LightChildField):
def __init__(self, *args, **kwargs):
super(String, self).__init__(*args, **kwargs)
self.default = kwargs.get('default', '') or ''
# def split(self, *args, **kwargs):
# return self.value.split(*args, **kwargs)
class Integer(LightChildField):
def __init__(self, *args, **kwargs):
super(Integer, self).__init__(*args, **kwargs)
self.default = kwargs.get('default', 0)
def __set__(self, instance, value):
try:
self.data[instance] = int(value)
except (TypeError, ValueError): # not an integer
self.data[instance] = 0
class Boolean(LightChildField):
def __init__(self, *args, **kwargs):
super(Boolean, self).__init__(*args, **kwargs)
self.default = kwargs.get('default', False)
def __set__(self, instance, value):
if isinstance(value, basestring):
value = value.lower() == 'true'
self.data[instance] = value
class Float(LightChildField):
def __init__(self, *args, **kwargs):
super(Float, self).__init__(*args, **kwargs)
self.default = kwargs.get('default', 0)
def __set__(self, instance, value):
try:
self.data[instance] = float(value)
except (TypeError, ValueError): # not an integer
self.data[instance] = 0
class List(LightChildField):
def __init__(self, *args, **kwargs):
super(List, self).__init__(*args, **kwargs)
self.default = kwargs.get('default', [])
class Scope(object):
content = None
user_state = None
...@@ -24,8 +24,6 @@ ...@@ -24,8 +24,6 @@
# Imports ########################################################### # Imports ###########################################################
import logging import logging
import uuid
import re
from collections import namedtuple from collections import namedtuple
...@@ -36,42 +34,26 @@ from xblock.core import XBlock ...@@ -36,42 +34,26 @@ from xblock.core import XBlock
from xblock.fields import Boolean, Scope, String, Integer, Float, List from xblock.fields import Boolean, Scope, String, Integer, Float, List
from xblock.fragment import Fragment from xblock.fragment import Fragment
from .light_children import XBlockWithLightChildren from components.title import TitleBlock
from .title import TitleBlock from components.header import SharedHeaderBlock
from .header import SharedHeaderBlock from components.message import MentoringMessageBlock
from .message import MentoringMessageBlock from components.step import StepParentMixin
from .step import StepParentMixin
from .utils import loader
from xblockutils.resources import ResourceLoader
# Globals ########################################################### # Globals ###########################################################
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
loader = ResourceLoader(__name__)
default_xml_content = loader.render_template('templates/xml/mentoring_default.xml', {})
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 ########################################################### # Classes ###########################################################
Score = namedtuple("Score", ["raw", "percentage", "correct", "incorrect", "partially_correct"]) Score = namedtuple("Score", ["raw", "percentage", "correct", "incorrect", "partially_correct"])
class MentoringBlock(XBlockWithLightChildren, StepParentMixin):
class MentoringBlock(XBlock, StepParentMixin):
""" """
An XBlock providing mentoring capabilities An XBlock providing mentoring capabilities
...@@ -83,54 +65,105 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin): ...@@ -83,54 +65,105 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin):
@staticmethod @staticmethod
def is_default_xml_content(value): def is_default_xml_content(value):
return _is_default_xml_content(value) return value == default_xml_content
attempted = Boolean(help="Has the student attempted this mentoring step?", # Content
default=False, scope=Scope.user_state) MENTORING_MODES = ('standard', 'assessment')
completed = Boolean(help="Has the student completed this mentoring step?", mode = String(
default=False, scope=Scope.user_state) help="Mode of the mentoring. 'standard' or 'assessment'",
next_step = String(help="url_name of the next step the student must complete (global to all blocks)", default='standard',
default='mentoring_first', scope=Scope.preferences) scope=Scope.content,
followed_by = String(help="url_name of the step after the current mentoring block in workflow", values=MENTORING_MODES
default=None, scope=Scope.content) )
url_name = String(help="Name of the current step, used for URL building", followed_by = String(
default='mentoring-default', scope=Scope.content) help="url_name of the step after the current mentoring block in workflow.",
enforce_dependency = Boolean(help="Should the next step be the current block to complete?", default=None,
default=False, scope=Scope.content, enforce_type=True) scope=Scope.content
)
max_attempts = Integer(
help="Number of max attempts for this questions",
default=0,
scope=Scope.content,
enforce_type=True
)
url_name = String(
help="Name of the current step, used for URL building",
default='mentoring-default', # TODO in future: set this to xblock.fields.UNIQUE_ID and remove self.url_name_with_default
scope=Scope.content
)
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) 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) # Settings
num_attempts = Integer(help="Number of attempts a user has answered for this questions", weight = Float(
default=0, scope=Scope.user_state, enforce_type=True) help="Defines the maximum total grade of the block.",
max_attempts = Integer(help="Number of max attempts for this questions", default=0, default=1,
scope=Scope.content, enforce_type=True) scope=Scope.settings,
mode = String(help="Mode of the mentoring. 'standard' or 'assessment'", enforce_type=True
default='standard', scope=Scope.content) )
step = Integer(help="Keep track of the student assessment progress.", display_name = String(
default=0, scope=Scope.user_state, enforce_type=True) help="Display name of the component",
student_results = List(help="Store results of student choices.", default=[], default="Mentoring XBlock",
scope=Scope.user_state) scope=Scope.settings
)
display_name = String(help="Display name of the component", default="Mentoring XBlock",
scope=Scope.settings) # User state
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?",
default=False,
scope=Scope.user_state
)
num_attempts = Integer(
help="Number of attempts a user has answered for this questions",
default=0,
scope=Scope.user_state,
enforce_type=True
)
step = Integer(
help="Keep track of the student assessment progress.",
default=0,
scope=Scope.user_state,
enforce_type=True
)
student_results = List(
help="Store results of student choices.",
default=[],
scope=Scope.user_state
)
# Global user state
next_step = String(
help="url_name of the next step the student must complete (global to all blocks)",
default='mentoring_first',
scope=Scope.preferences
)
icon_class = 'problem' icon_class = 'problem'
has_score = True has_score = True
has_children = True
MENTORING_MODES = ('standard', 'assessment')
FLOATING_BLOCKS = (TitleBlock, MentoringMessageBlock, SharedHeaderBlock) FLOATING_BLOCKS = (TitleBlock, MentoringMessageBlock, SharedHeaderBlock)
FIELDS_TO_INIT = ('xml_content',)
@property @property
def is_assessment(self): def is_assessment(self):
return self.mode == 'assessment' return self.mode == 'assessment'
@property @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 weight of each step."""
total_child_weight = sum(float(step.weight) for step in self.steps) weights = [float(self.runtime.get_block(step_id).weight) for step_id in self.steps]
total_child_weight = sum(weights)
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'] for r in self.student_results) / total_child_weight score = sum(r[1]['score'] * r[1]['weight'] for r in self.student_results) / total_child_weight
...@@ -144,14 +177,31 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin): ...@@ -144,14 +177,31 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin):
# Migrate stored data if necessary # Migrate stored data if necessary
self.migrate_fields() self.migrate_fields()
fragment, named_children = self.get_children_fragment( title = ""
context, view_name='mentoring_view', header = ""
not_instance_of=self.FLOATING_BLOCKS,
) fragment = Fragment()
child_content = u""
for child_id in self.children:
child = self.runtime.get_block(child_id)
if isinstance(child, TitleBlock):
title = child.content
continue
elif isinstance(child, SharedHeaderBlock):
header = child.render('mentoring_view', context).content
continue
elif isinstance(child, MentoringMessageBlock):
# TODO
continue
child_fragment = child.render('mentoring_view', context)
fragment.add_frag_resources(child_fragment)
child_content += child_fragment.content
fragment.add_content(loader.render_template('templates/html/mentoring.html', { fragment.add_content(loader.render_template('templates/html/mentoring.html', {
'self': self, 'self': self,
'named_children': named_children, 'title': title,
'header': header,
'child_content': child_content,
'missing_dependency_url': self.has_missing_dependency and self.next_step_url, 'missing_dependency_url': self.has_missing_dependency and self.next_step_url,
})) }))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/mentoring.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/mentoring.css'))
...@@ -190,7 +240,7 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin): ...@@ -190,7 +240,7 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin):
def additional_publish_event_data(self): def additional_publish_event_data(self):
return { return {
'user_id': self.scope_ids.user_id, 'user_id': self.scope_ids.user_id,
'component_id': self.url_name, 'component_id': self.url_name_with_default,
} }
@property @property
...@@ -219,7 +269,7 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin): ...@@ -219,7 +269,7 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin):
Returns True if the student needs to complete another step before being able to complete Returns True if the student needs to complete another step before being able to complete
the current one, and False otherwise the current one, and False otherwise
""" """
return self.enforce_dependency and (not self.completed) and (self.next_step != self.url_name) return self.enforce_dependency and (not self.completed) and (self.next_step != self.url_name_with_default) # TODO: Fix
@property @property
def next_step_url(self): def next_step_url(self):
...@@ -229,6 +279,24 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin): ...@@ -229,6 +279,24 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin):
return '/jump_to_id/{}'.format(self.next_step) return '/jump_to_id/{}'.format(self.next_step)
@XBlock.json_handler @XBlock.json_handler
def view(self, data, suffix=''):
"""
Current HTML view of the XBlock, for refresh by client
"""
frag = self.student_view({})
return {'html': frag.content}
@XBlock.json_handler
def publish_event(self, data, suffix=''):
"""
Publish data for analytics purposes
"""
event_type = data.pop('event_type')
self.runtime.publish(self, event_type, data)
return {'result': 'ok'}
@XBlock.json_handler
def submit(self, submissions, suffix=''): def submit(self, submissions, suffix=''):
log.info(u'Received submissions: {}'.format(submissions)) log.info(u'Received submissions: {}'.format(submissions))
self.attempted = True self.attempted = True
...@@ -238,7 +306,8 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin): ...@@ -238,7 +306,8 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin):
submit_results = [] submit_results = []
completed = True completed = True
for child in self.get_children_objects(): for child_id in self.children:
child = self.runtime.get_block(child_id)
if child.name and child.name in submissions: if child.name and child.name in submissions:
submission = submissions[child.name] submission = submissions[child.name]
child_result = child.submit(submission) child_result = child.submit(submission)
...@@ -264,7 +333,7 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin): ...@@ -264,7 +333,7 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin):
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 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: elif completed and self.next_step == self.url_name_with_default:
self.next_step = self.followed_by self.next_step = self.followed_by
# Once it was completed, lock score # Once it was completed, lock score
...@@ -287,7 +356,7 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin): ...@@ -287,7 +356,7 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin):
raw_score = self.score.raw raw_score = self.score.raw
self.publish_event_from_dict('xblock.mentoring.submitted', { self.runtime.publish(self, 'xblock.mentoring.submitted', {
'num_attempts': self.num_attempts, 'num_attempts': self.num_attempts,
'submitted_answer': submissions, 'submitted_answer': submissions,
'grade': raw_score, 'grade': raw_score,
...@@ -390,18 +459,13 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin): ...@@ -390,18 +459,13 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin):
def max_attempts_reached(self): def max_attempts_reached(self):
return self.max_attempts > 0 and self.num_attempts >= self.max_attempts return self.max_attempts > 0 and self.num_attempts >= self.max_attempts
def get_message_fragment(self, message_type):
for child in self.get_children_objects():
if isinstance(child, MentoringMessageBlock) and child.type == message_type:
frag = self.render_child(child, 'mentoring_view', {})
return self.fragment_text_rewriting(frag)
def get_message_html(self, message_type): def get_message_html(self, message_type):
fragment = self.get_message_fragment(message_type) html = u""
if fragment: for child_id in self.children:
return fragment.body_html() child = self.runtime.get_block(child_id)
else: if isinstance(child, MentoringMessageBlock) and child.type == message_type:
return '' html += child.render('mentoring_view', {}).content # TODO: frament_text_rewriting ?
return html
def studio_view(self, context): def studio_view(self, context):
""" """
...@@ -461,13 +525,12 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin): ...@@ -461,13 +525,12 @@ class MentoringBlock(XBlockWithLightChildren, StepParentMixin):
def url_name_with_default(self): def url_name_with_default(self):
""" """
Ensure the `url_name` is set to a unique, non-empty value. Ensure the `url_name` is set to a unique, non-empty value.
This should ideally be handled by Studio, but we need to declare the attribute In future once hte pinned version of XBlock is updated,
to be able to use it from the workbench, and when this happen Studio doesn't set we can remove this and change the field to use the
a unique default value - this property gives either the set value, or if none is set xblock.fields.UNIQUE_ID flag instead.
a randomized default value
""" """
if self.url_name == 'mentoring-default': if self.url_name == 'mentoring-default':
return 'mentoring-{}'.format(uuid.uuid4()) return self.scope_ids.usage_id
else: else:
return self.url_name return self.url_name
......
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'Answer'
db.create_table('mentoring_answer', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('name', self.gf('django.db.models.fields.CharField')(max_length=50, db_index=True)),
('student_id', self.gf('django.db.models.fields.CharField')(max_length=20, db_index=True)),
('student_input', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
('created_on', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('modified_on', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
))
db.send_create_signal('mentoring', ['Answer'])
# Adding unique constraint on 'Answer', fields ['student_id', 'name']
db.create_unique('mentoring_answer', ['student_id', 'name'])
def backwards(self, orm):
# Removing unique constraint on 'Answer', fields ['student_id', 'name']
db.delete_unique('mentoring_answer', ['student_id', 'name'])
# Deleting model 'Answer'
db.delete_table('mentoring_answer')
models = {
'mentoring.answer': {
'Meta': {'unique_together': "(('student_id', 'name'),)", 'object_name': 'Answer'},
'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'student_id': ('django.db.models.fields.CharField', [], {'max_length': '20', 'db_index': 'True'}),
'student_input': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'})
}
}
complete_apps = ['mentoring']
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'Answer.course_id'
db.add_column('mentoring_answer', 'course_id',
self.gf('django.db.models.fields.CharField')(default='default', max_length=50, db_index=True),
keep_default=False)
# Changing field 'Answer.student_id'
db.alter_column('mentoring_answer', 'student_id', self.gf('django.db.models.fields.CharField')(max_length=32))
def backwards(self, orm):
# Deleting field 'Answer.course_id'
db.delete_column('mentoring_answer', 'course_id')
# Changing field 'Answer.student_id'
db.alter_column('mentoring_answer', 'student_id', self.gf('django.db.models.fields.CharField')(max_length=20))
models = {
'mentoring.answer': {
'Meta': {'unique_together': "(('student_id', 'name'),)", 'object_name': 'Answer'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'student_id': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'student_input': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'})
}
}
complete_apps = ['mentoring']
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Removing unique constraint on 'Answer', fields ['student_id', 'name']
db.delete_unique('mentoring_answer', ['student_id', 'name'])
# Adding unique constraint on 'Answer', fields ['course_id', 'student_id', 'name']
db.create_unique('mentoring_answer', ['course_id', 'student_id', 'name'])
def backwards(self, orm):
# Removing unique constraint on 'Answer', fields ['course_id', 'student_id', 'name']
db.delete_unique('mentoring_answer', ['course_id', 'student_id', 'name'])
# Adding unique constraint on 'Answer', fields ['student_id', 'name']
db.create_unique('mentoring_answer', ['student_id', 'name'])
models = {
'mentoring.answer': {
'Meta': {'unique_together': "(('student_id', 'course_id', 'name'),)", 'object_name': 'Answer'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'student_id': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'student_input': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'})
}
}
complete_apps = ['mentoring']
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'LightChild'
db.create_table('mentoring_lightchild', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('name', self.gf('django.db.models.fields.CharField')(max_length=50, db_index=True)),
('student_id', self.gf('django.db.models.fields.CharField')(max_length=32, db_index=True)),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=50, db_index=True)),
('student_data', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
('created_on', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('modified_on', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
))
db.send_create_signal('mentoring', ['LightChild'])
# Adding unique constraint on 'LightChild', fields ['student_id', 'course_id', 'name']
db.create_unique('mentoring_lightchild', ['student_id', 'course_id', 'name'])
def backwards(self, orm):
# Removing unique constraint on 'LightChild', fields ['student_id', 'course_id', 'name']
db.delete_unique('mentoring_lightchild', ['student_id', 'course_id', 'name'])
# Deleting model 'LightChild'
db.delete_table('mentoring_lightchild')
models = {
'mentoring.answer': {
'Meta': {'unique_together': "(('student_id', 'course_id', 'name'),)", 'object_name': 'Answer'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'student_id': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'student_input': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'})
},
'mentoring.lightchild': {
'Meta': {'unique_together': "(('student_id', 'course_id', 'name'),)", 'object_name': 'LightChild'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'student_data': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'student_id': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'})
}
}
complete_apps = ['mentoring']
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Changing field 'LightChild.name'
db.alter_column('mentoring_lightchild', 'name', self.gf('django.db.models.fields.CharField')(max_length=100))
def backwards(self, orm):
# Changing field 'LightChild.name'
db.alter_column('mentoring_lightchild', 'name', self.gf('django.db.models.fields.CharField')(max_length=50))
models = {
'mentoring.answer': {
'Meta': {'unique_together': "(('student_id', 'course_id', 'name'),)", 'object_name': 'Answer'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'student_id': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'student_input': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'})
},
'mentoring.lightchild': {
'Meta': {'unique_together': "(('student_id', 'course_id', 'name'),)", 'object_name': 'LightChild'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}),
'student_data': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
'student_id': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'})
}
}
complete_apps = ['mentoring']
# -*- 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 ###########################################################
from django.db import models
# Classes ###########################################################
class Answer(models.Model):
"""
Django model used to store AnswerBlock data that need to be shared
and queried accross XBlock instances (workaround).
"""
class Meta:
app_label = 'mentoring'
unique_together = (('student_id', 'course_id', 'name'),)
name = models.CharField(max_length=50, db_index=True)
student_id = models.CharField(max_length=32, db_index=True)
course_id = models.CharField(max_length=50, db_index=True)
student_input = models.TextField(blank=True, default='')
created_on = models.DateTimeField('created on', auto_now_add=True)
modified_on = models.DateTimeField('modified on', auto_now=True)
def save(self, *args, **kwargs):
# Force validation of max_length
self.full_clean()
super(Answer, self).save(*args, **kwargs)
class LightChild(models.Model):
"""
Django model used to store LightChild student data that need to be shared and queried accross
XBlock instances (workaround). Since this is temporary, `data` are stored in json.
"""
class Meta:
app_label = 'mentoring'
unique_together = (('student_id', 'course_id', 'name'),)
name = models.CharField(max_length=100, db_index=True)
student_id = models.CharField(max_length=32, db_index=True)
course_id = models.CharField(max_length=50, db_index=True)
student_data = models.TextField(blank=True, default='')
created_on = models.DateTimeField('created on', auto_now_add=True)
modified_on = models.DateTimeField('modified on', auto_now=True)
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
...@@ -4,17 +4,15 @@ ...@@ -4,17 +4,15 @@
attempting this step. attempting this step.
</div> </div>
{% if self.title or self.header %} {% if title or header %}
<div class="title"> <div class="title">
{% if self.title %} <h2 class="main">{{ self.title.content }}</h2> {% endif %} {% if title %} <h2 class="main">{{ title }}</h2> {% endif %}
{% if self.header %} <div class="shared-header">{{ self.header.content|safe }}</div> {% endif %} {% if header %} {{ header|safe }} {% endif %}
</div> </div>
{% endif %} {% endif %}
<div class="{{self.mode}}-question-block"> <div class="{{self.mode}}-question-block">
{% for name, c in named_children %} {{child_content|safe}}
{{c.body_html|safe}}
{% endfor %}
{% if self.display_submit %} {% if self.display_submit %}
......
<div
class="tip"
{% if self.width %}data-width="{{self.width}}"{% endif %}
{% if self.height %}data-height="{{self.height}}"{% endif %}
>
{% if self.content %}<p>{{ self.content }}</p>{% endif %}
{% for name, c in named_children %}
{{c.body_html|safe}}
{% endfor %}
</div>
<mentoring url_name="{{ url_name }}" display_name="Nav tooltip title" weight="1" mode="assessment" max_attempts="10"> <mentoring display_name="Nav tooltip title" weight="1" mode="assessment" max_attempts="10">
<title>Default Title</title> <title>Default Title</title>
<shared-header> <shared-header>
<p>This paragraph is shared between <strong>all</strong> questions.</p> <p>This paragraph is shared between <strong>all</strong> questions.</p>
......
<mentoring url_name="{{ url_name }}" display_name="Nav tooltip title" weight="1" mode="standard"> <mentoring display_name="Nav tooltip title" weight="1" mode="standard">
<title>Default Title</title> <title>Default Title</title>
<html> <html>
<p>Please answer the questions below.</p> <p>Please answer the questions below.</p>
......
<mentoring url_name="{{ url_name }}" display_name="Nav tooltip title" weight="1" mode="standard"> <mentoring display_name="Nav tooltip title" weight="1" mode="standard">
<title>Default Title</title> <title>Default Title</title>
<html> <html>
<p>Please answer the questions below.</p> <p>Please answer the questions below.</p>
......
# -*- 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/>.
#
import logging
import os
import pkg_resources
import unicodecsv
from cStringIO import StringIO
from django.template import Context, Template
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
log = logging.getLogger(__name__)
class MentoringResourceLoader(ResourceLoader):
def custom_render_js_template(self, template_path, context={}):
return self.render_js_template(template_path, 'light-child-template', context)
loader = MentoringResourceLoader(__name__)
def list2csv(row):
"""
Convert a list to a CSV string (single row)
"""
f = StringIO()
writer = unicodecsv.writer(f, encoding='utf-8')
writer.writerow(row)
f.seek(0)
return f.read()
class XBlockWithChildrenFragmentsMixin(object):
def get_children_fragment(self, context, view_name='student_view', instance_of=None,
not_instance_of=None):
"""
Returns a global fragment containing the resources used by the children views,
and a list of fragments, one per children
- `view_name` allows to select a specific view method on the children
- `instance_of` allows to only return fragments for children which are instances of
the provided class
- `not_instance_of` allows to only return fragments for children which are *NOT*
instances of the provided class
"""
fragment = Fragment()
named_child_frags = []
for child_id in self.children: # pylint: disable=E1101
child = self.runtime.get_block(child_id)
if instance_of is not None and not isinstance(child, instance_of):
continue
if not_instance_of is not None and isinstance(child, not_instance_of):
continue
frag = self.runtime.render_child(child, view_name, context)
fragment.add_frag_resources(frag)
named_child_frags.append((child.name, frag))
return fragment, named_child_frags
def children_view(self, context, view_name='children_view'):
"""
Returns a fragment with the content of all the children's content, concatenated
"""
fragment, named_children = self.get_children_fragment(context)
for name, child_fragment in named_children:
fragment.add_content(child_fragment.content)
return fragment
class ContextConstants(object):
AS_TEMPLATE = 'as_template'
\ No newline at end of file
...@@ -45,29 +45,27 @@ def package_data(pkg, root_list): ...@@ -45,29 +45,27 @@ def package_data(pkg, root_list):
BLOCKS = [ BLOCKS = [
'mentoring = mentoring:MentoringBlock', 'mentoring = mentoring:MentoringBlock',
'mentoring-dataexport = mentoring:MentoringDataExportBlock', 'mentoring-dataexport = mentoring:MentoringDataExportBlock',
]
BLOCKS_CHILDREN = [ 'mentoring-table = mentoring.components:MentoringTableBlock',
'mentoring-table = mentoring:MentoringTableBlock', 'column = mentoring.components:MentoringTableColumnBlock',
'column = mentoring:MentoringTableColumnBlock', 'header = mentoring.components:MentoringTableColumnHeaderBlock',
'header = mentoring:MentoringTableColumnHeaderBlock', 'answer = mentoring.components:AnswerBlock',
'answer = mentoring:AnswerBlock', 'quizz = mentoring.components:MCQBlock',
'quizz = mentoring:MCQBlock', 'mcq = mentoring.components:MCQBlock',
'mcq = mentoring:MCQBlock', 'mrq = mentoring.components:MRQBlock',
'mrq = mentoring:MRQBlock', 'message = mentoring.components:MentoringMessageBlock',
'message = mentoring:MentoringMessageBlock', 'tip = mentoring.components:TipBlock',
'tip = mentoring:TipBlock', 'choice = mentoring.components:ChoiceBlock',
'choice = mentoring:ChoiceBlock', 'html = mentoring.components:HTMLBlock',
'html = mentoring:HTMLBlock', 'title = mentoring.components:TitleBlock',
'title = mentoring:TitleBlock', 'shared-header = mentoring.components:SharedHeaderBlock',
'shared-header = mentoring:SharedHeaderBlock',
] ]
setup( setup(
name='xblock-mentoring', name='xblock-mentoring',
version='0.1', version='0.1',
description='XBlock - Mentoring', description='XBlock - Mentoring',
packages=['mentoring', 'mentoring.migrations'], packages=['mentoring'],
install_requires=[ install_requires=[
'XBlock', 'XBlock',
'xblock-utils', 'xblock-utils',
...@@ -75,7 +73,6 @@ setup( ...@@ -75,7 +73,6 @@ setup(
dependency_links = ['http://github.com/edx-solutions/xblock-utils/tarball/master#egg=xblock-utils'], dependency_links = ['http://github.com/edx-solutions/xblock-utils/tarball/master#egg=xblock-utils'],
entry_points={ entry_points={
'xblock.v1': BLOCKS, 'xblock.v1': BLOCKS,
'xblock.light_children': BLOCKS_CHILDREN,
}, },
package_data=package_data("mentoring", ["static", "templates", "public", "migrations"]), package_data=package_data("mentoring", ["static", "templates", "public"]),
) )
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