Commit bba22bba by Kelketek

Merge pull request #43 from open-craft/download-table

Allow students to download a copy of their answer table.
parents 6aaa3aba f5953e35
......@@ -203,7 +203,10 @@ class AnswerBlock(SubmittingXBlockMixin, AnswerMixin, StepMixin, StudioEditableX
if sub_api:
# Also send to the submissions API:
sub_api.create_submission(self.student_item_key, self.student_input)
item_key = self.student_item_key
# Need to do this by our own ID, since an answer can be referred to multiple times.
item_key['item_id'] = self.name
sub_api.create_submission(item_key, self.student_input)
log.info(u'Answer submitted for`{}`: "{}"'.format(self.name, self.student_input))
return self.get_results()
......@@ -272,6 +275,8 @@ class AnswerRecapBlock(AnswerMixin, StudioEditableXBlockMixin, XBlock):
)
editable_fields = ('name', 'display_name', 'description')
css_path = 'public/css/answer.css'
@property
def student_input(self):
if self.name:
......@@ -281,13 +286,28 @@ class AnswerRecapBlock(AnswerMixin, StudioEditableXBlockMixin, XBlock):
def mentoring_view(self, context=None):
""" Render this XBlock within a mentoring block. """
context = context.copy() if context else {}
student_submissions_key = context.get('student_submissions_key')
context['title'] = self.display_name
context['description'] = self.description
context['student_input'] = self.student_input
if student_submissions_key:
location = self.location.replace(branch=None, version=None) # Standardize the key in case it isn't already
target_key = {
'student_id': student_submissions_key,
'course_id': unicode(location.course_key),
'item_id': self.name,
'item_type': u'pb-answer',
}
submissions = sub_api.get_submissions(target_key, limit=1)
try:
context['student_input'] = submissions[0]['answer']
except IndexError:
context['student_input'] = None
else:
context['student_input'] = self.student_input
html = loader.render_template('templates/html/answer_read_only.html', context)
fragment = Fragment(html)
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/answer.css'))
fragment.add_css_url(self.runtime.local_resource_url(self, self.css_path))
return fragment
def student_view(self, context=None):
......
......@@ -58,6 +58,39 @@ def _(text):
# Classes ###########################################################
class ExportMixin(object):
"""
Used by blocks which need to provide a downloadable export.
"""
def _get_user_full_name(self):
"""
Get the full name of the current user, for the downloadable report.
"""
user_service = self.runtime.service(self, 'user')
if user_service:
return user_service.get_current_user().full_name
return ""
def _get_course_name(self):
"""
Get the name of the current course, for the downloadable report.
"""
try:
course_key = self.scope_ids.usage_id.course_key
except AttributeError:
return "" # We are not in an edX runtime
try:
course_root_key = course_key.make_usage_key('course', 'course')
return self.runtime.get_block(course_root_key).display_name
except Exception: # ItemNotFoundError most likely, but we can't import that exception in non-edX environments
# We may be on old mongo:
try:
course_root_key = course_key.make_usage_key('course', course_key.run)
return self.runtime.get_block(course_root_key).display_name
except Exception:
return ""
class ColorRule(object):
"""
A rule used to conditionally set colors
......@@ -155,7 +188,7 @@ class InvalidUrlName(ValueError):
@XBlock.needs("i18n")
@XBlock.wants("user")
class DashboardBlock(StudioEditableXBlockMixin, XBlock):
class DashboardBlock(StudioEditableXBlockMixin, ExportMixin, XBlock):
"""
A block to summarize self-assessment results.
"""
......@@ -260,7 +293,7 @@ class DashboardBlock(StudioEditableXBlockMixin, XBlock):
'color_rules', 'visual_rules', 'visual_title', 'visual_desc', 'header_html', 'footer_html',
)
css_path = 'public/css/dashboard.css'
js_path = 'public/js/dashboard.js'
js_path = 'public/js/review_blocks.js'
def get_mentoring_blocks(self, mentoring_ids, ignore_errors=True):
"""
......@@ -343,34 +376,6 @@ class DashboardBlock(StudioEditableXBlockMixin, XBlock):
return rule.color_str
return None
def _get_user_full_name(self):
"""
Get the full name of the current user, for the downloadable report.
"""
user_service = self.runtime.service(self, 'user')
if user_service:
return user_service.get_current_user().full_name
return ""
def _get_course_name(self):
"""
Get the name of the current course, for the downloadable report.
"""
try:
course_key = self.scope_ids.usage_id.course_key
except AttributeError:
return "" # We are not in an edX runtime
try:
course_root_key = course_key.make_usage_key('course', 'course')
return self.runtime.get_block(course_root_key).display_name
except Exception: # ItemNotFoundError most likely, but we can't import that exception in non-edX environments
# We may be on old mongo:
try:
course_root_key = course_key.make_usage_key('course', course_key.run)
return self.runtime.get_block(course_root_key).display_name
except Exception:
return ""
def _get_problem_questions(self, mentoring_block):
""" Generator returning only children of specified block that are MCQs """
for child_id in mentoring_block.children:
......@@ -469,7 +474,11 @@ class DashboardBlock(StudioEditableXBlockMixin, XBlock):
fragment = Fragment(html)
fragment.add_css_url(self.runtime.local_resource_url(self, self.css_path))
fragment.add_javascript_url(self.runtime.local_resource_url(self, self.js_path))
fragment.initialize_js('PBDashboardBlock', {'reportTemplate': report_template})
fragment.initialize_js(
'PBDashboardBlock', {
'reportTemplate': report_template,
'reportContentSelector': '.dashboard-report'
})
return fragment
def validate_field_data(self, validation, data):
......
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as 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 'Share'
db.create_table('problem_builder_share', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('shared_by', self.gf('django.db.models.fields.related.ForeignKey')(
related_name='problem_builder_shared_by', to=orm['auth.User']
)),
('submission_uid', self.gf('django.db.models.fields.CharField')(max_length=32)),
('block_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('shared_with', self.gf('django.db.models.fields.related.ForeignKey')(
related_name='problem_builder_shared_with', to=orm['auth.User']
)),
('notified', self.gf('django.db.models.fields.BooleanField')(default=False, db_index=True)),
))
db.send_create_signal('problem_builder', ['Share'])
# Adding unique constraint on 'Share', fields ['shared_by', 'shared_with', 'block_id']
db.create_unique('problem_builder_share', ['shared_by_id', 'shared_with_id', 'block_id'])
def backwards(self, orm):
# Removing unique constraint on 'Share', fields ['shared_by', 'shared_with', 'block_id']
db.delete_unique('problem_builder_share', ['shared_by_id', 'shared_with_id', 'block_id'])
# Deleting model 'Share'
db.delete_table('problem_builder_share')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {
'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'
})
},
'auth.permission': {
'Meta': {
'ordering': "('content_type__app_label', 'content_type__model', 'codename')",
'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'
},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {
'to': "orm['contenttypes.ContentType']"
}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {
'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'
}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {
'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'
}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {
'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)",
'object_name': 'ContentType', 'db_table': "'django_content_type'"
},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'problem_builder.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'})
},
'problem_builder.share': {
'Meta': {'unique_together': "(('shared_by', 'shared_with', 'block_id'),)", 'object_name': 'Share'},
'block_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'notified': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
'shared_by': ('django.db.models.fields.related.ForeignKey', [], {
'related_name': "'problem_builder_shared_by'", 'to': "orm['auth.User']"
}),
'shared_with': ('django.db.models.fields.related.ForeignKey', [], {
'related_name': "'problem_builder_shared_with'", 'to': "orm['auth.User']"
}),
'submission_uid': ('django.db.models.fields.CharField', [], {'max_length': '32'})
}
}
complete_apps = ['problem_builder']
......@@ -21,6 +21,7 @@
# Imports ###########################################################
from django.db import models
from django.contrib.auth.models import User
# Classes ###########################################################
......@@ -47,3 +48,19 @@ class Answer(models.Model):
# Force validation of max_length
self.full_clean()
super(Answer, self).save(*args, **kwargs)
class Share(models.Model):
"""
The XBlock User Service does not permit XBlocks instantiated with non-staff users
to query for arbitrary anonymous user IDs. In order to make sharing work, we have
to store them here.
"""
shared_by = models.ForeignKey(User, related_name='problem_builder_shared_by')
submission_uid = models.CharField(max_length=32)
block_id = models.CharField(max_length=255, db_index=True)
shared_with = models.ForeignKey(User, related_name='problem_builder_shared_with')
notified = models.BooleanField(default=False, db_index=True)
class Meta(object):
unique_together = (('shared_by', 'shared_with', 'block_id'),)
......@@ -52,3 +52,84 @@
position: absolute;
width: 1px;
}
.mentoring-table-container .share-with-container {
text-align: right;
}
.share-with-instructions {
max-width: 14.5em;
}
.mentoring-share-panel {
float: right;
}
.mentoring-share-panel .mentoring-share-with {
position: absolute;
right: 0;
background-color: rgb(255, 255, 255);
border: 1px solid rgb(221, 221, 221);
padding: 1em;
}
.mentoring-share-with .share-header {
text-align: left;
}
.mentoring-share-with .share-action-buttons {
text-align: center;
padding-top: .5em;
}
.mentoring-share-with .add-share-username {
margin-right: 1em;
}
.mentoring-share-with .share-errors {
color: darkred;
font-size: .75em;
text-align: center;
}
.new-share-container {
margin-top: .5em;
}
.shared-list li {
list-style-type: none;
display: block;
padding: .25em;
margin: 0;
}
.shared-list li .username {
display: inline-block;
float: left;
}
.share-panel-container {
text-align: right;
}
.share-notification {
border: 2px solid rgb(200, 200, 200);
max-width: 15em;
padding: 1em;
background-color: rgb(255, 255, 255);
position: absolute;
right: 0;
font-size: .8em;
}
.share-notification .notification-close {
float: right;
}
.report-download-container {
float: left;
}
.mentoring .identification {
padding-bottom: 1em;
}
\ No newline at end of file
// Client side code for the Problem Builder Dashboard XBlock
// So far, this code is only used to generate a downloadable report.
function PBDashboardBlock(runtime, element, initData) {
"use strict";
var reportTemplate = initData.reportTemplate;
var generateDataUriFromImageURL = function(imgURL) {
// Given the URL to an image, IF the image has already been cached by the browser,
// returns a data: URI with the contents of the image (image will be converted to PNG)
var img = new Image();
img.src = imgURL;
if (!img.complete)
return imgURL;
// Create an in-memory canvas from which we can extract a data URL:
var canvas = document.createElement("canvas");
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
// Draw the image onto our temporary canvas:
canvas.getContext('2d').drawImage(img, 0, 0);
return canvas.toDataURL("image/png");
};
var unicodeStringToBase64 = function(str) {
// Convert string to base64. A bit weird in order to support unicode, per
// https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/btoa
return window.btoa(unescape(encodeURIComponent(str)));
};
var downloadReport = function(ev) {
// Download Report:
// Change the URL to a data: URI before continuing with the click event.
if ($(this).attr('href').charAt(0) == '#') {
var $report = $('.dashboard-report', element).clone();
// Convert all images in $report to data URIs:
$report.find('image').each(function() {
var origURL = $(this).attr('xlink:href');
$(this).attr('xlink:href', generateDataUriFromImageURL(origURL));
});
// Take the resulting HTML and put it into the template we have:
var wrapperHTML = reportTemplate.replace('REPORT_GOES_HERE', $report.html());
//console.log(wrapperHTML);
var dataURI = "data:text/html;base64," + unicodeStringToBase64(wrapperHTML);
$(this).attr('href', dataURI);
}
};
var $downloadLink = $('.report-download-link', element);
$downloadLink.on('click', downloadReport);
}
function MentoringTableBlock(runtime, element) {
// Display an exceprt for long answers, with a "more" link to display the full text
$('.answer-table', element).shorten({
moreText: 'more',
lessText: 'less',
showChars: '500'
});
return {};
}
// Client side code for the Problem Builder Dashboard XBlock
// So far, this code is only used to generate a downloadable report.
function ExportBase(runtime, element, initData) {
"use strict";
var reportTemplate = initData.reportTemplate;
var generateDataUriFromImageURL = function(imgURL) {
// Given the URL to an image, IF the image has already been cached by the browser,
// returns a data: URI with the contents of the image (image will be converted to PNG)
var img = new Image();
img.src = imgURL;
if (!img.complete)
return imgURL;
// Create an in-memory canvas from which we can extract a data URL:
var canvas = document.createElement("canvas");
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
// Draw the image onto our temporary canvas:
canvas.getContext('2d').drawImage(img, 0, 0);
return canvas.toDataURL("image/png");
};
var unicodeStringToBase64 = function(str) {
// Convert string to base64. A bit weird in order to support unicode, per
// https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/btoa
return window.btoa(unescape(encodeURIComponent(str)));
};
var downloadReport = function(ev) {
// Download Report:
// Change the URL to a data: URI before continuing with the click event.
if ($(this).attr('href').charAt(0) == '#') {
var $report = $(initData.reportContentSelector, element).clone();
// Convert all images in $report to data URIs:
$report.find('image').each(function() {
var origURL = $(this).attr('xlink:href');
$(this).attr('xlink:href', generateDataUriFromImageURL(origURL));
});
// Take the resulting HTML and put it into the template we have:
var wrapperHTML = reportTemplate.replace('REPORT_GOES_HERE', $report.html());
//console.log(wrapperHTML);
var dataURI = "data:text/html;base64," + unicodeStringToBase64(wrapperHTML);
$(this).attr('href', dataURI);
}
};
var $downloadLink = $('.report-download-link', element);
$downloadLink.on('click', downloadReport);
}
function PBDashboardBlock(runtime, element, initData) {
new ExportBase(runtime, element, initData);
}
function MentoringTableBlock(runtime, element, initData) {
// Display an excerpt for long answers, with a "more" link to display the full text
var $element = $(element),
$shareButton = $element.find('.mentoring-share-button'),
$doShareButton = $element.find('.do-share-button'),
$shareMenu = $element.find('.mentoring-share-with'),
$displayDropdown = $element.find('.mentoring-display-dropdown'),
$errorHolder = $element.find('.share-errors'),
$deleteShareButton = $element.find('.remove-share'),
$newShareContainer = $($element.find('.new-share-container')[0]),
$addShareField = $($element.find('.add-share-field')[0]),
$notification = $($element.find('.share-notification')),
$closeNotification = $($element.find('.notification-close')),
tableLoadURL = runtime.handlerUrl(element, 'table_render'),
deleteShareUrl = runtime.handlerUrl(element, 'remove_share'),
sharedListLoadUrl = runtime.handlerUrl(element, 'get_shared_list'),
clearNotificationUrl = runtime.handlerUrl(element, 'clear_notification'),
shareResultsUrl = runtime.handlerUrl(element, 'share_results');
function loadTable(data) {
$element.find('.mentoring-table-target').html(data['content']);
$('.answer-table', element).shorten({
moreText: 'more',
lessText: 'less',
showChars: '500'
});
}
function errorMessage(event) {
$errorHolder.text(JSON.parse(event.responseText)['error'])
}
function sharedRefresh(data) {
$element.find('.shared-with-container').html(data['content']);
$deleteShareButton = $($deleteShareButton.selector);
$deleteShareButton.on('click', deleteShare);
}
function postShareRefresh(data) {
sharedRefresh(data);
$element.find(".new-share-container").each(function(index, container) {
if (index === 0) {
$(container).find('.add-share-username').val('');
return;
}
$(container).remove()
});
$errorHolder.html('');
}
function postShare() {
$.ajax({
type: "POST",
url: sharedListLoadUrl,
data: JSON.stringify({}),
success: postShareRefresh,
error: errorMessage
});
}
function updateShare() {
var usernames = [];
$element.find('.add-share-username').each(function(index, username) {
usernames.push($(username).val())
});
$.ajax({
type: "POST",
url: shareResultsUrl,
data: JSON.stringify({'usernames': usernames}),
success: postShare,
error: errorMessage
});
}
function menuHider(event) {
if (!$(event.target).closest($shareMenu).length) {
// We're clicking outside of the menu, so hide it.
$shareMenu.hide();
$(document).off('click.mentoring_share_menu_hide');
}
}
$shareButton.on('click', function (event) {
if (!$shareMenu.is(':visible')){
event.stopPropagation();
$(document).on('click.mentoring_share_menu_hide', menuHider);
$shareMenu.show();
}
});
$doShareButton.on('click', updateShare);
function postLoad(data) {
loadTable(data);
new ExportBase(runtime, element, initData);
}
$.ajax({
type: "POST",
url: tableLoadURL,
data: JSON.stringify({'target_username': $displayDropdown.val()}),
success: postLoad
});
$.ajax({
type: "POST",
url: sharedListLoadUrl,
data: JSON.stringify({}),
success: sharedRefresh
});
$displayDropdown.on('change', function () {
$.ajax({
type: "POST",
url: tableLoadURL,
data: JSON.stringify({'target_username': $displayDropdown.val()}),
success: loadTable
})
});
function addShare() {
var container = $newShareContainer.clone();
container.find('.add-share-username').val('');
container.insertAfter($element.find('.new-share-container').last());
container.find('.add-share-field').on('click', addShare)
}
function deleteShare(event) {
$.ajax({
type: "POST",
url: deleteShareUrl,
data: JSON.stringify({'username': $(event.target).prev()[0].innerHTML}),
success: function () {
$(event.target).parent().remove();
$errorHolder.html('');
},
error: errorMessage
});
}
$closeNotification.on('click', function () {
// Don't need server approval to hide it.
$notification.hide();
$.ajax({
type: "POST",
url: clearNotificationUrl,
data: JSON.stringify({'usernames': $notification.data('shared')})
})
});
$addShareField.on('click', addShare);
}
......@@ -21,16 +21,22 @@
# Imports ###########################################################
import errno
import json
from django.contrib.auth.models import User
from xblock.core import XBlock
from xblock.fields import Scope, String
from xblock.exceptions import JsonHandlerError
from xblock.fields import Scope, String, Boolean, Dict
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin, StudioContainerXBlockMixin
# Globals ###########################################################
from problem_builder import AnswerRecapBlock
from problem_builder.dashboard import ExportMixin
from problem_builder.models import Share
from problem_builder.sub_api import SubmittingXBlockMixin
loader = ResourceLoader(__name__)
......@@ -42,7 +48,11 @@ def _(text):
# Classes ###########################################################
class MentoringTableBlock(StudioEditableXBlockMixin, StudioContainerXBlockMixin, XBlock):
@XBlock.wants("user")
@XBlock.wants("submissions")
class MentoringTableBlock(
StudioEditableXBlockMixin, SubmittingXBlockMixin, StudioContainerXBlockMixin, ExportMixin, XBlock
):
"""
Table-type display of information from mentoring blocks
......@@ -66,23 +76,171 @@ class MentoringTableBlock(StudioEditableXBlockMixin, StudioContainerXBlockMixin,
{"display_name": "Immunity Map", "value": "immunity-map"},
],
)
editable_fields = ("type", )
editable_fields = ("type", "allow_download")
allow_download = Boolean(
display_name=_("Allow Download"),
help=_("Allow students to download a copy of the table for themselves."),
default=False,
scope=Scope.content
)
allow_sharing = Boolean(
display_name=_("Allow Sharing"),
help=_("Allow students to share their results with other students."),
default=True,
scope=Scope.content
)
has_children = True
def student_view(self, context):
context = context.copy() if context else {}
fragment = Fragment()
css_path = 'public/css/mentoring-table.css'
js_path = 'public/js/review_blocks.js'
@XBlock.json_handler
def table_render(self, data, suffix=''):
context = {}
header_values = []
content_values = []
target_username = data.get('target_username')
try:
if target_username and target_username != self.current_user_key:
share = Share.objects.get(
shared_by__username=target_username, shared_with__username=self.current_user_key,
block_id=self.block_id,
)
context['student_submissions_key'] = share.submission_uid
except Share.DoesNotExist:
raise JsonHandlerError(403, _("You are not permitted to view this student's table."))
for child_id in self.children:
child = self.runtime.get_block(child_id)
# Child should be an instance of MentoringTableColumn
header_values.append(child.header)
child_frag = child.render('mentoring_view', context)
content_values.append(child_frag.content)
fragment.add_frag_resources(child_frag)
context['header_values'] = header_values if any(header_values) else None
context['content_values'] = content_values
html = loader.render_template('templates/html/mentoring-table.html', context)
return {'content': html}
@property
def current_user_key(self):
user = self.runtime.service(self, 'user').get_current_user()
# We may be in the SDK, in which case the username may not really be available.
return user.opt_attrs.get('edx-platform.username', 'username')
@XBlock.json_handler
def get_shared_list(self, data, suffix=''):
context = {'shared_with': Share.objects.filter(
shared_by__username=self.current_user_key,
block_id=self.block_id,
).values_list('shared_with__username', flat=True)
}
return {
'content': loader.render_template('templates/html/mentoring-table-shared-list.html', context)
}
@XBlock.json_handler
def clear_notification(self, data, suffix=''):
"""
Clear out notifications for users who shared with this user on the last page load.
Since more users might share with them while they're viewing the page, only remove the ones
that they had at the time.
"""
usernames = data.get('usernames')
if not usernames:
raise JsonHandlerError(400, "No usernames sent.")
try:
isinstance(usernames, list)
except ValueError:
raise JsonHandlerError(400, "Usernames must be a list.")
Share.objects.filter(
shared_with__username=self.current_user_key,
shared_by__username__in=usernames,
block_id=self.block_id,
).update(
notified=True
)
@property
def block_id(self):
usage_id = self.scope_ids.usage_id
if isinstance(usage_id, basestring):
return usage_id
try:
return unicode(usage_id.replace(branch=None, version_guid=None))
except AttributeError:
pass
return unicode(usage_id)
@XBlock.json_handler
def share_results(self, data, suffix=''):
target_usernames = data.get('usernames')
target_usernames = [username.strip().lower() for username in target_usernames if username.strip()]
current_user = User.objects.get(username=self.current_user_key)
failed_users = []
if not target_usernames:
raise JsonHandlerError(400, _('Usernames not provided.'))
for target_username in target_usernames:
try:
target_user = User.objects.get(username=target_username)
except User.DoesNotExist:
failed_users.append(target_username)
continue
if current_user == target_user:
continue
try:
Share.objects.get(shared_by=current_user, shared_with=target_user, block_id=self.block_id)
except Share.DoesNotExist:
Share(
shared_by=current_user, submission_uid=self.runtime.anonymous_student_id, shared_with=target_user,
block_id=self.block_id,
).save()
if failed_users:
raise JsonHandlerError(
400,
_('Some users could not be shared with. Please check these usernames: {}').format(
','.join(failed_users)
)
)
return {}
@XBlock.json_handler
def remove_share(self, data, suffix=''):
target_username = data.get('username')
if not target_username:
raise JsonHandlerError(400, _('Username not provided.'))
Share.objects.filter(
shared_by__username=self.current_user_key,
shared_with__username=target_username,
block_id=self.block_id,
).delete()
return {'message': _('Removed successfully.')}
def student_view(self, context):
context = context.copy() if context else {}
fragment = Fragment()
for child_id in self.children:
child = self.runtime.get_block(child_id)
# Child should be an instance of MentoringTableColumn
child_frag = child.render('mentoring_view', context)
fragment.add_frag_resources(child_frag)
context['allow_sharing'] = self.allow_sharing
context['allow_download'] = self.allow_download
user_service = self.runtime.service(self, 'user')
if user_service:
context['view_options'] = Share.objects.filter(
shared_with__username=self.current_user_key,
block_id=self.block_id,
).values_list('shared_by__username', flat=True)
context['username'] = self.current_user_key
share_notifications = Share.objects.filter(
shared_with__username=self.current_user_key,
notified=False, block_id=self.block_id,
).values_list('shared_by__username', flat=True)
context['share_notifications'] = share_notifications and json.dumps(list(share_notifications))
if self.type:
# Load an optional background image:
......@@ -96,11 +254,23 @@ class MentoringTableBlock(StudioEditableXBlockMixin, StudioContainerXBlockMixin,
else:
raise
fragment.add_content(loader.render_template('templates/html/mentoring-table.html', context))
report_template = loader.render_template('templates/html/mentoring-table-report.html', {
'title': self.display_name,
'css': loader.load_unicode(AnswerRecapBlock.css_path) + loader.load_unicode(self.css_path),
'student_name': self._get_user_full_name(),
'course_name': self._get_course_name(),
})
fragment.add_content(loader.render_template('templates/html/mentoring-table-container.html', context))
fragment.add_css_url(self.runtime.local_resource_url(self, '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, 'public/js/mentoring-table.js'))
fragment.initialize_js('MentoringTableBlock')
fragment.add_javascript_url(self.runtime.local_resource_url(self, self.js_path))
fragment.initialize_js(
'MentoringTableBlock', {
'reportContentSelector': '.mentoring-table-container',
'reportTemplate': report_template,
}
)
return fragment
......
{% load i18n %}
{% if allow_sharing %}
<div class="share-panel-container">
{% if allow_download %}
<div class="report-download-container"><a class="report-download-link" href="#report_download" download="report.html">{% trans "Download report" %}</a></div>
{% endif %}
<div class="mentoring-share-panel">
{% if view_options %}
<span>{% trans "Display Map from:" %}</span>
<select class="mentoring-display-dropdown">
<option value="{{username}}">{% blocktrans %}You ({{username}}){% endblocktrans %}</option>
{% for option in view_options %}
<option value="{{option}}">{{option}}</option>
{% endfor %}
</select>
{% endif %}
<button class="mentoring-share-button">
<i class="fa fa-share"></i>Share
</button>
<div class="mentoring-share-with" style="display: none;">
<div class="share-with-instructions">
<p>{% trans "Enter the username of another student you'd like to share this with." %}</p>
</div>
<div class="shared-with-container"></div>
<div class="new-share-container"><input class="add-share-username"><button class="add-share-field">+</button></div>
<div class="share-errors"></div>
<div class="share-action-buttons">
<button class="do-share-button">Share</button>
</div>
</div>
{% if share_notifications %}
<div class="share-notification" data-shared="{{share_notifications}}">
<button class="notification-close"><i class="fa fa-close"></i></button>
<p><strong>{% trans "Map added!" %}</strong></p>
<p>
{% blocktrans %}
Another user has shared a map with you.
</p>
<p>
You can change the user you're currently displaying using the drop-down selector above.
{% endblocktrans %}
</p>
</div>
{% endif %}
</div>
</div>
<div class="clear"></div>
{% endif %}
<div class="mentoring-table-container">
<div class="mentoring-table {{ self.type }}" style="background-image: url({{ bg_image_url }})">
<div class="cont-text-sr">{{ bg_image_description }}</div>
<div class="mentoring-table-target"></div>
</div>
</div>
{% load i18n %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title }}</title>
<style>
body {
font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
{{css|safe}}
</style>
</head>
<body>
<div class="mentoring">
<div class="identification">
{% if student_name %}{% trans "Student" %}: {{student_name}}<br>{% endif %}
{% if course_name %}{% trans "Course" %}: {{course_name}}<br>{% endif %}
{% trans "Date" %}: {% now "DATE_FORMAT" %}<br>
</div>
REPORT_GOES_HERE
</div>
</body>
</html>
{% load i18n %}
{% if shared_with %}
<div class="share-header">{% trans "Shared with:" %}</div>
{% endif %}
<ul class="shared-list">
{% for username in shared_with %}
<li><span class="username">{{username}}</span><button class="remove-share"><i class="fa fa-trash"></i></button></li>
{% endfor %}
</ul>
\ No newline at end of file
<div class="mentoring-table {{ self.type }}" style="background-image: url({{ bg_image_url }})">
<div class="cont-text-sr">{{ bg_image_description }}</div>
<table>
<table>
{% if header_values %}
<thead>
{% for header in header_values %}
<th>{{ header|safe }}</th>
{% endfor %}
{% for header in header_values %}
<th>{{ header|safe }}</th>
{% endfor %}
</thead>
{% endif %}
<tbody>
<tr>
<tr>
{% for content in content_values %}
<td>
<div class="mentoring-column">
{{content|safe}}
</div>
<div class="mentoring-column">
{{content|safe}}
</div>
</td>
{% endfor %}
</tr>
</tr>
</tbody>
</table>
</div>
</table>
\ No newline at end of file
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