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
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);
}
{% 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 %}
......@@ -19,5 +17,4 @@
{% endfor %}
</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