Commit 92585717 by Xavier Antoviaque

Merge pull request #4 from FiloSottile/port_studio

Port the Studio editor and remove the dataexport functionality
parents e2924962 21378208
*~ *~
*.pyc *.pyc
xblock_mentoring.egg-info /xblock_mentoring.egg-info
workbench.sqlite /workbench.sqlite
mentoring/templates/xml /dist
dist /templates
templates
...@@ -9,7 +9,6 @@ It supports: ...@@ -9,7 +9,6 @@ It supports:
* **Self-assessment quizzes** (multiple choice), to display predetermined feedback to a student based on his choices in the self-assessment. Supports rating scales and arbitrary answers. * **Self-assessment quizzes** (multiple choice), to display predetermined feedback to a student based on his choices in the self-assessment. Supports rating scales and arbitrary answers.
* **Progression tracking**, allowing to check that the student has completed the previous steps before allowing to complete a given XBlock instance. Provides a link to the next step to the student. * **Progression tracking**, allowing to check that the student has completed the previous steps before allowing to complete a given XBlock instance. Provides a link to the next step to the student.
* **Tables**, which allow to present answers from the student to free-form answers in a concise way. Supports custom headers. * **Tables**, which allow to present answers from the student to free-form answers in a concise way. Supports custom headers.
* **Data export**, to allow course authors to download a CSV file containing the free-form answers entered by the students
Examples Examples
-------- --------
...@@ -91,13 +90,6 @@ Second XBlock instance: ...@@ -91,13 +90,6 @@ Second XBlock instance:
</mentoring-table> </mentoring-table>
``` ```
### Data export
```xml
<vertical>
<mentoring-dataexport url_name="mentoring_dataexport"></mentoring-dataexport>
</vertical>
```
Installing dependencies Installing dependencies
----------------------- -----------------------
......
from .answer import AnswerBlock from .answer import AnswerBlock
from .dataexport import MentoringDataExportBlock
from .html import HTMLBlock from .html import HTMLBlock
from .quizz import QuizzBlock, QuizzChoiceBlock, QuizzTipBlock from .quizz import QuizzBlock, QuizzChoiceBlock, QuizzTipBlock
from .mentoring import MentoringBlock from .mentoring import MentoringBlock
......
# -*- 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
from itertools import groupby
from operator import itemgetter
from webob import Response
from xblock.core import XBlock
from xblock.fragment import Fragment
from .models import Answer
from .utils import list2csv, render_template, serialize_opaque_key
# Globals ###########################################################
log = logging.getLogger(__name__)
# Classes ###########################################################
class MentoringDataExportBlock(XBlock):
"""
An XBlock allowing the instructor team to export all the student answers as a CSV file
"""
def student_view(self, context):
html = render_template('templates/html/dataexport.html', {
'self': self,
})
fragment = Fragment(html)
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/dataexport.js'))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/dataexport.css'))
fragment.initialize_js('MentoringDataExportBlock')
return fragment
def studio_view(self, context):
return Fragment(u'Studio view body')
@XBlock.handler
def download_csv(self, request, suffix=''):
response = Response(content_type='text/csv')
response.app_iter = self.get_csv()
response.content_disposition = 'attachment; filename=course_data.csv'
return response
def get_csv(self):
course_id = serialize_opaque_key(self.xmodule_runtime.course_id)
answers = Answer.objects.filter(course_id=course_id).order_by('student_id', 'name')
answers_names = answers.values_list('name', flat=True).distinct().order_by('name')
# Header line
yield list2csv([u'student_id'] + list(answers_names))
answers_list = answers.values_list('name', 'student_id', 'student_input')
if answers_names:
for k, student_answers in groupby(answers_list, itemgetter(1)):
row = []
next_answer_idx = 0
for answer in student_answers:
if not row:
row = [answer[1]]
while answer[0] != answers_names[next_answer_idx]:
# Still add answer row to CSV when they don't exist in DB
row.append('')
next_answer_idx += 1
row.append(answer[2])
next_answer_idx += 1
if row:
yield list2csv(row)
...@@ -85,6 +85,8 @@ class LightChildrenMixin(XBlockWithChildrenFragmentsMixin): ...@@ -85,6 +85,8 @@ class LightChildrenMixin(XBlockWithChildrenFragmentsMixin):
cls.add_node_as_child(block, xml_child, child_id) cls.add_node_as_child(block, xml_child, child_id)
for name, value in attr: for name, value in attr:
if value == "false": value = False
if value == "true": value = True
setattr(block, name, value) setattr(block, name, value)
return block return block
...@@ -120,7 +122,7 @@ class LightChildrenMixin(XBlockWithChildrenFragmentsMixin): ...@@ -120,7 +122,7 @@ class LightChildrenMixin(XBlockWithChildrenFragmentsMixin):
return return
node = etree.parse(StringIO(self.xml_content)).getroot() node = etree.parse(StringIO(self.xml_content)).getroot()
LightChildrenMixin.init_block_from_node(self, node, {}) LightChildrenMixin.init_block_from_node(self, node, node.items())
def get_children_objects(self): def get_children_objects(self):
""" """
......
...@@ -24,9 +24,14 @@ ...@@ -24,9 +24,14 @@
# Imports ########################################################### # Imports ###########################################################
import logging import logging
import uuid
from lxml import etree
from StringIO import StringIO
from xblock.core import XBlock from xblock.core import XBlock
from xblock.fields import Boolean, Scope, String from xblock.fields import Boolean, Scope, String
from xblock.fragment import Fragment
from .light_children import XBlockWithLightChildren from .light_children import XBlockWithLightChildren
from .message import MentoringMessageBlock from .message import MentoringMessageBlock
...@@ -58,7 +63,7 @@ class MentoringBlock(XBlockWithLightChildren): ...@@ -58,7 +63,7 @@ class MentoringBlock(XBlockWithLightChildren):
followed_by = String(help="url_name of the step after the current mentoring block in workflow", followed_by = String(help="url_name of the step after the current mentoring block in workflow",
default=None, scope=Scope.content) default=None, scope=Scope.content)
url_name = String(help="Name of the current step, used for URL building", url_name = String(help="Name of the current step, used for URL building",
default='mentoring', scope=Scope.content) default='mentoring-default', scope=Scope.content)
enforce_dependency = Boolean(help="Should the next step be the current block to complete?", enforce_dependency = Boolean(help="Should the next step be the current block to complete?",
default=True, scope=Scope.content) default=True, scope=Scope.content)
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)
...@@ -156,6 +161,66 @@ class MentoringBlock(XBlockWithLightChildren): ...@@ -156,6 +161,66 @@ class MentoringBlock(XBlockWithLightChildren):
else: else:
return '' return ''
def studio_view(self, context):
"""
Editing view in Studio
"""
fragment = Fragment()
fragment.add_content(render_template('templates/html/mentoring_edit.html', {
'self': self,
'xml_content': self.xml_content or self.default_xml_content,
}))
fragment.add_javascript_url(
self.runtime.local_resource_url(self, 'public/js/mentoring_edit.js'))
fragment.add_css_url(
self.runtime.local_resource_url(self, 'public/css/mentoring_edit.css'))
fragment.initialize_js('MentoringEditBlock')
return fragment
@XBlock.json_handler
def studio_submit(self, submissions, suffix=''):
log.info(u'Received studio submissions: {}'.format(submissions))
xml_content = submissions['xml_content']
try:
content = etree.parse(StringIO(xml_content))
except etree.XMLSyntaxError as e:
response = {
'result': 'error',
'message': e.message
}
else:
response = {
'result': 'success',
}
self.xml_content = etree.tostring(content, pretty_print=True)
log.debug(u'Response from Studio: {}'.format(response))
return response
@property
def default_xml_content(self):
return render_template('templates/xml/mentoring_default.xml', {
'self': self,
'url_name': self.url_name_with_default,
})
@property
def url_name_with_default(self):
"""
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
to be able to use it from the workbench, and when this happen Studio doesn't set
a unique default value - this property gives either the set value, or if none is set
a randomized default value
"""
if self.url_name == 'mentoring-default':
return 'mentoring-{}'.format(uuid.uuid4())
else:
return self.url_name
@staticmethod @staticmethod
def workbench_scenarios(): def workbench_scenarios():
""" """
......
.mentoring-dataexport {
margin: 10px;
}
.mentoring-edit .module-actions .error-message {
color: red;
}
function MentoringDataExportBlock(runtime, element) {
var downloadUrl = runtime.handlerUrl(element, 'download_csv');
$('button.download', element).click(function(ev) {
ev.preventDefault();
window.location = downloadUrl;
});
}
function MentoringEditBlock(runtime, element) {
var xmlEditorTextarea = $('.block-xml-editor', element),
xmlEditor = CodeMirror.fromTextArea(xmlEditorTextarea[0], { mode: 'xml' });
$('.save-button', element).bind('click', function() {
var handlerUrl = runtime.handlerUrl(element, 'studio_submit'),
data = {
'xml_content': xmlEditor.getValue(),
};
$('.error-message', element).html();
$.post(handlerUrl, JSON.stringify(data)).done(function(response) {
if (response.result === 'success') {
window.location.reload(false);
} else {
$('.error-message', element).html('Error: '+response.message);
}
});
});
$('.cancel-button', element).bind('click', function() {
runtime.notify('cancel', {});
});
}
<div class="mentoring-dataexport">
<h3>Answers data dump</h3>
<button class="download">Download CSV</button>
</div>
{% load i18n %}
<!-- TODO: Replace by default edit view once available in Studio -->
<div class="mentoring-edit editor-with-buttons">
<div class="wrapper-comp-settings is-active" id="settings-tab">
<textarea class="block-xml-editor">{{ xml_content }}</textarea>
</div>
<div class="xblock-actions">
<span class="error-message"></span>
<ul>
<li class="action-item">
<a href="#" class="button action-primary save-button">{% trans "Save" %}</a>
</li>
<li class="action-item">
<a href="#" class="button cancel-button">{% trans "Cancel" %}</a>
</li>
</ul>
</div>
</div>
<mentoring url_name="{{ url_name }}" enforce_dependency="false">
<html>
<p>What is your goal?</p>
</html>
<answer name="goal" />
<quizz name="quizz_1_1" type="choices">
<question>Do you like this quizz?</question>
<choice value="yes">Yes</choice>
<choice value="maybenot">Maybe not</choice>
<choice value="understand">I don't understand</choice>
<tip display="yes">Great!</tip>
<tip reject="maybenot">Ah, damn.</tip>
<tip reject="understand"><html><div id="test-custom-html">Really?</div></html></tip>
</quizz>
<quizz name="quizz_1_2" type="rating" low="Not good at all" high="Extremely good">
<question>How much do you rate this quizz?</question>
<choice value="notwant">I don't want to rate it</choice>
<tip display="4,5">I love good grades.</tip>
<tip reject="1,2,3">Will do better next time...</tip>
<tip reject="notwant">Your loss!</tip>
</quizz>
<message type="completed">
All is good now...
<html><p>Congratulations!</p></html>
</message>
</mentoring>
...@@ -26,9 +26,7 @@ ...@@ -26,9 +26,7 @@
import logging import logging
import os import os
import pkg_resources import pkg_resources
import unicodecsv
from cStringIO import StringIO
from django.template import Context, Template from django.template import Context, Template
from xblock.fragment import Fragment from xblock.fragment import Fragment
...@@ -55,16 +53,6 @@ def render_template(template_path, context={}): ...@@ -55,16 +53,6 @@ def render_template(template_path, context={}):
template = Template(template_str) template = Template(template_str)
return template.render(Context(context)) return template.render(Context(context))
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()
def get_scenarios_from_path(scenarios_path, include_identifier=False): def get_scenarios_from_path(scenarios_path, include_identifier=False):
""" """
Returns an array of (title, xmlcontent) from files contained in a specified directory, Returns an array of (title, xmlcontent) from files contained in a specified directory,
......
-e . -e .
unicodecsv==0.9.4
-e git://github.com/nosedjango/nosedjango.git@ed7d7f9aa969252ff799ec159f828eaa8c1cbc5a#egg=nosedjango-dev -e git://github.com/nosedjango/nosedjango.git@ed7d7f9aa969252ff799ec159f828eaa8c1cbc5a#egg=nosedjango-dev
...@@ -44,7 +44,6 @@ def package_data(pkg, root_list): ...@@ -44,7 +44,6 @@ def package_data(pkg, root_list):
BLOCKS = [ BLOCKS = [
'mentoring = mentoring:MentoringBlock', 'mentoring = mentoring:MentoringBlock',
'mentoring-dataexport = mentoring:MentoringDataExportBlock',
] ]
BLOCKS_CHILDREN = [ BLOCKS_CHILDREN = [
......
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