Finished level management

On assigning the block to a course, a skill needs to be selected in the config,
instead of creating a new skill by default
This commit is contained in:
pmk 2018-09-23 22:06:52 +02:00
parent 6b16559201
commit fa285134cb
19 changed files with 3560 additions and 193 deletions

52
amd/src/debugger.js Normal file
View File

@ -0,0 +1,52 @@
/*eslint no-var: "error"*/
/*eslint no-console: "off"*/
/*eslint-env es6*/
// Put this file in path/to/plugin/amd/src
// You can call it anything you like
define([], function () {
return (function (name) {
let handle = name;
let output_enabled = false;
return {
write: function debugger_write() {
if (output_enabled) {
let args = Array.prototype.slice.call(arguments);
args.unshift(handle + ": ");
console.info.apply(console, args);
}
},
info: function debugger_info() {
if (output_enabled) {
let args = Array.prototype.slice.call(arguments);
args.unshift(handle + ": ");
console.info.apply(console, args);
}
},
warn: function debugger_warn() {
if (output_enabled) {
let args = Array.prototype.slice.call(arguments);
args.unshift(handle + ": ");
console.warn.apply(console, args);
}
},
error: function debugger_error() {
if (output_enabled) {
let args = Array.prototype.slice.call(arguments);
args.unshift(handle + ": ");
console.error.apply(console, args);
}
},
enable: function debugger_enable() {
output_enabled = true;
},
disable: function debugger_disable() {
output_enabled = false;
}
};
});
});

22
amd/src/handlers.js Normal file
View File

@ -0,0 +1,22 @@
/*eslint no-var: "error"*/
/*eslint no-console: "off"*/
/*eslint-env es6*/
// Put this file in path/to/plugin/amd/src
// You can call it anything you like
define([], function () {
let debug_enabled = false;
let self = {
fail_report_exception: function fail_report_exception(ex) {
if (ex.error != undefined) {
console.error("Error from webservice: ", ex.error, "\n", ex.debuginfo, "\n---Stack trace---\n", ex.stacktrace, "\n", ex);
}
else {
console.error("Exception from webservice: ", ex.message, "\n", ex.debuginfo, "\n---Stack trace---\n", ex.backtrace, "\n", ex);
}
},
};
return self;
});

View File

@ -0,0 +1,235 @@
/**!
* @preserve Shadow animation 1.11
* http://www.bitstorm.org/jquery/shadow-animation/
* Copyright 2011, 2013 Edwin Martin
* Contributors: Mark Carver, Xavier Lepretre and Jason Redding
* Released under the MIT and GPL licenses.
*/
define(['jquery'], function (jQuery) {
return jQuery(function($, undefined) {
/**
* Check whether the browser supports RGBA color mode.
*
* Author Mehdi Kabab <http://pioupioum.fr>
* @return {boolean} True if the browser support RGBA. False otherwise.
*/
function isRGBACapable() {
var $script = $('script:first'),
color = $script.css('color'),
result = false;
if (/^rgba/.test(color)) {
result = true;
} else {
try {
result = (color !== $script.css('color', 'rgba(0, 0, 0, 0.5)').css('color'));
$script.css('color', color);
} catch (e) {
}
}
$script.removeAttr('style');
return result;
}
$.extend(true, $, {
support: {
'rgba': isRGBACapable()
}
});
/*************************************/
// First define which property to use
var styles = $('html').prop('style');
var boxShadowProperty;
$.each(['boxShadow', 'MozBoxShadow', 'WebkitBoxShadow'], function(i, property) {
var val = styles[property];
if (typeof val !== 'undefined') {
boxShadowProperty = property;
return false;
}
});
// Extend the animate-function
if (boxShadowProperty) {
$['Tween']['propHooks']['boxShadow'] = {
get: function(tween) {
return $(tween.elem).css(boxShadowProperty);
},
set: function(tween) {
var style = tween.elem.style;
var p_begin = parseShadows($(tween.elem)[0].style[boxShadowProperty] || $(tween.elem).css(boxShadowProperty));
var p_end = parseShadows(tween.end);
var maxShadowCount = Math.max(p_begin.length, p_end.length);
var i;
for(i = 0; i < maxShadowCount; i++) {
p_end[i] = $.extend({}, p_begin[i], p_end[i]);
if (p_begin[i]) {
if (!('color' in p_begin[i]) || $.isArray(p_begin[i].color) === false) {
p_begin[i].color = p_end[i].color || [0, 0, 0, 0];
}
} else {
p_begin[i] = parseShadows('0 0 0 0 rgba(0,0,0,0)')[0];
}
}
tween['run'] = function(progress) {
var rs = calculateShadows(p_begin, p_end, progress);
style[boxShadowProperty] = rs;
};
}
};
}
// Calculate an in-between shadow.
function calculateShadows(beginList, endList, pos) {
var shadows = [];
$.each(beginList, function(i) {
var parts = [], begin = beginList[i], end = endList[i];
if (begin.inset) {
parts.push('inset');
}
if (typeof end.left !== 'undefined') {
parts.push(parseFloat(begin.left + pos * (end.left - begin.left)) + 'px '
+ parseFloat(begin.top + pos * (end.top - begin.top)) + 'px');
}
if (typeof end.blur !== 'undefined') {
parts.push(parseFloat(begin.blur + pos * (end.blur - begin.blur)) + 'px');
}
if (typeof end.spread !== 'undefined') {
parts.push(parseFloat(begin.spread + pos * (end.spread - begin.spread)) + 'px');
}
if (typeof end.color !== 'undefined') {
var color = 'rgb' + ($.support['rgba'] ? 'a' : '') + '('
+ parseInt((begin.color[0] + pos * (end.color[0] - begin.color[0])), 10) + ','
+ parseInt((begin.color[1] + pos * (end.color[1] - begin.color[1])), 10) + ','
+ parseInt((begin.color[2] + pos * (end.color[2] - begin.color[2])), 10);
if ($.support['rgba']) {
color += ',' + parseFloat(begin.color[3] + pos * (end.color[3] - begin.color[3]));
}
color += ')';
parts.push(color);
}
shadows.push(parts.join(' '));
});
return shadows.join(', ');
}
// Parse the shadow value and extract the values.
function parseShadows(shadow) {
var parsedShadows = [];
var parsePosition = 0;
var parseLength = shadow.length;
function findInset() {
var m = /^inset\b/.exec(shadow.substring(parsePosition));
if (m !== null && m.length > 0) {
parsedShadow.inset = true;
parsePosition += m[0].length;
return true;
}
return false;
}
function findOffsets() {
var m = /^(-?[0-9\.]+)(?:px)?\s+(-?[0-9\.]+)(?:px)?(?:\s+(-?[0-9\.]+)(?:px)?)?(?:\s+(-?[0-9\.]+)(?:px)?)?/.exec(shadow.substring(parsePosition));
if (m !== null && m.length > 0) {
parsedShadow.left = parseInt(m[1], 10);
parsedShadow.top = parseInt(m[2], 10);
parsedShadow.blur = (m[3] ? parseInt(m[3], 10) : 0);
parsedShadow.spread = (m[4] ? parseInt(m[4], 10) : 0);
parsePosition += m[0].length;
return true;
}
return false;
}
function findColor() {
var m = /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})/.exec(shadow.substring(parsePosition));
if (m !== null && m.length > 0) {
parsedShadow.color = [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16), 1];
parsePosition += m[0].length;
return true;
}
m = /^#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])/.exec(shadow.substring(parsePosition));
if (m !== null && m.length > 0) {
parsedShadow.color = [parseInt(m[1], 16) * 17, parseInt(m[2], 16) * 17, parseInt(m[3], 16) * 17, 1];
parsePosition += m[0].length;
return true;
}
m = /^rgb\(\s*([0-9\.]+)\s*,\s*([0-9\.]+)\s*,\s*([0-9\.]+)\s*\)/.exec(shadow.substring(parsePosition));
if (m !== null && m.length > 0) {
parsedShadow.color = [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10), 1];
parsePosition += m[0].length;
return true;
}
m = /^rgba\(\s*([0-9\.]+)\s*,\s*([0-9\.]+)\s*,\s*([0-9\.]+)\s*,\s*([0-9\.]+)\s*\)/.exec(shadow.substring(parsePosition));
if (m !== null && m.length > 0) {
parsedShadow.color = [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10), parseFloat(m[4])];
parsePosition += m[0].length;
return true;
}
return false;
}
function findWhiteSpace() {
var m = /^\s+/.exec(shadow.substring(parsePosition));
if (m !== null && m.length > 0) {
parsePosition += m[0].length;
return true;
}
return false;
}
function findComma() {
var m = /^\s*,\s*/.exec(shadow.substring(parsePosition));
if (m !== null && m.length > 0) {
parsePosition += m[0].length;
return true;
}
return false;
}
function normalizeShadow(shadow) {
if ($.isPlainObject(shadow)) {
var i, sColor, cLength = 0, color = [];
if ($.isArray(shadow.color)) {
sColor = shadow.color;
cLength = sColor.length;
}
for(i = 0; i < 4; i++) {
if (i < cLength) {
color.push(sColor[i]);
} else if (i === 3) {
color.push(1);
} else {
color.push(0);
}
}
}
return $.extend({
'left': 0,
'top': 0,
'blur': 0,
'spread': 0
}, shadow);
}
var parsedShadow = normalizeShadow();
while (parsePosition < parseLength) {
if (findInset()) {
findWhiteSpace();
} else if (findOffsets()) {
findWhiteSpace();
} else if (findColor()) {
findWhiteSpace();
} else if (findComma()) {
parsedShadows.push(normalizeShadow(parsedShadow));
parsedShadow = {};
} else {
break;
}
}
parsedShadows.push(normalizeShadow(parsedShadow));
return parsedShadows;
}
});
});

View File

@ -18,7 +18,7 @@ define([], function () {
//if (!window.jscolor) { window.jscolor = (function () {
return (function () {
console.info("Initializing JSColor");
//console.info("Initializing JSColor");
var jsc = {

View File

@ -1,7 +1,14 @@
/*eslint no-var: "error"*/
/*eslint-env es6*/
// Put this file in path/to/plugin/amd/src
// You can call it anything you like
define(['jquery', 'core/str', 'core/ajax', 'block_gradelevel/jscolor'], function ($, str, ajax, jscolor) {
define(['jquery', 'block_gradelevel/jquery.animate-shadow', 'core/str', 'core/ajax', 'block_gradelevel/jscolor',
'block_gradelevel/handlers', 'block_gradelevel/debugger'], function ($, $shadow, str, ajax, jscolor, handlers, Debugger) {
let debug = new Debugger("leveleditor");
debug.enable();
let skill_id = null;
let $skill_table = null;
@ -13,15 +20,12 @@ define(['jquery', 'core/str', 'core/ajax', 'block_gradelevel/jscolor'], function
skill_id = Number($skill_table.attr('data-skill'));
if (!skill_id && skill_id !== 0 && skill_id !== '0') {
console.error("Cannot find configured skill id");
debug.error("Cannot find configured skill id");
return;
}
// load and fill the level table
self.refresh();
// make sure all uint type numbers are not able to go below 0
$(document).on('change', "input[type=number].uint", function (evt) {
$(document).on('input', "input[type=number].uint", function (evt) {
let $this = $(this);
if ($this.val() < 0) {
$this.val(0);
@ -35,6 +39,8 @@ define(['jquery', 'core/str', 'core/ajax', 'block_gradelevel/jscolor'], function
// On click handler for save changes button
$("button[data-action=savechanges]").on('click', self.submit);
// On click handler for save changes button
$("button[data-action=resetlevels]").on('click', self.resetlevels);
},
@ -53,7 +59,7 @@ define(['jquery', 'core/str', 'core/ajax', 'block_gradelevel/jscolor'], function
$skill_table.find("tbody").append($newtr);
},
refresh: function refresh() {
console.info("Attempting to refresh");
debug.info("Attempting to refresh");
// perform refresh call
let promises = ajax.call([{
@ -82,7 +88,7 @@ define(['jquery', 'core/str', 'core/ajax', 'block_gradelevel/jscolor'], function
level_data.push(row);
});
//console.info("Attempting to submit", level_data);
//debug.info("Attempting to submit", level_data);
// perform ajax call
let promises = ajax.call([{
@ -91,27 +97,54 @@ define(['jquery', 'core/str', 'core/ajax', 'block_gradelevel/jscolor'], function
}]);
// and link promise returns to callbacks
promises[0].done(self.success_refill_table).fail(self.fail_report_exception);
promises[0].done(self.success_refill_table).fail(handlers.fail_report_exception);
},
resetlevels: function resetlevels(evt) {
let $table = $("table#level_config");
// Send a request to delete all rows
let level_data = [];
$table.find("tbody tr[data-rowid]").each(function () {
let $this = $(this);
let row = {
id: Number($this.attr('data-rowid')),
points: -255, // means: delete row
badgecolor: "", // totally irrelevent when requesting a delete
}
level_data.push(row);
});
//debug.info("Attempting to submit", level_data);
// perform ajax call
let promises = ajax.call([{
methodname: 'block_gradelevel_submit_levels',
args: { skill_id: skill_id, levels: level_data },
}]);
// and link promise returns to callbacks
promises[0].done(self.success_refill_table).fail(handlers.fail_report_exception);
},
success_refill_table: function success_refill_table(response) {
//console.info("Response from webservice: ", response);
//debug.info("Response from webservice: ", response);
let $tbody = $skill_table.find('tbody');
$tbody.empty();
for (let idx in response) {
let lvl = response[idx];
//console.info("Level:", lvl);
//debug.info("Level:", lvl);
self.add_levelrow(lvl.id, lvl.points, lvl.badgecolor);
}
let $inputs = $tbody.find("input[data-name='points']");
let orig_border = $inputs.css('border-left-color');
$inputs
.animate({ boxShadow: '0px 0px 5px 3px #3FDFCF'}, 400)
.delay(200)
.animate({ boxShadow: 'none'}, 800);
},
fail_report_exception: function fail_report_exception(ex) {
if (ex.error != undefined) {
console.error("Error from webservice: ", ex.error, "\n" , ex.debuginfo, "\n---Stack trace---\n", ex.stacktrace, "\n", ex);
}
else {
console.error("Exception from webservice: ", ex.message, "\n", ex.debuginfo, "\n---Stack trace---\n", ex.backtrace, "\n", ex);
}
},
};
return self;

View File

@ -1,23 +1,39 @@
/*eslint no-var: "error"*/
/*eslint-env es6*/
// Put this file in path/to/plugin/amd/src
// You can call it anything you like
define(['jquery', 'core/str', 'core/ajax'], function ($, str, ajax) {
define(['jquery', 'core/str', 'core/ajax','block_gradelevel/debugger' ], function ($, str, ajax, Debugger) {
let debug = Debugger("renderbadge");
//debug.enable();
let self = {
init: function init() {
// Put whatever you like here. $ is available
// to you as normal.
$("figure.levelbadge").each(function () {
let props = self.fetchProperties(this);
let $this = $(this);
let $canvas = $("<canvas height= '"+props.height +"' width = '"+props.width+"'/>");
//console.info("$canvas", $canvas, $canvas[0]);
$('canvas', this).remove();// clear out any existing canvas
$this.append($canvas); // Put the canvas in there
self.render($canvas[0], props);
self.setup_badge(this,true);
});
},
setup_badge: function setup_Badge(figure, allowskip) {
let $figure = $(figure);
let props = $(figure).data('badge-props');
debug.info("Setting up skill badge on ", figure, props);
if (props && allowskip) {
debug.info(" skill badge was already configured. Skipping process...");
}
else {
props = self.fetchProperties(figure);
let $canvas = $("<canvas height= '" + props.height + "' width = '" + props.width + "'/>");
$('canvas', figure).remove();// clear out any existing canvas
$figure.append($canvas); // Put the new canvas in there
self.render($canvas[0], props);
// Store properties
$figure.data("badge-props", props);
}
},
render: function render(canvas, props) {
let ctx = canvas.getContext("2d");
// Color configuration
@ -61,9 +77,9 @@ define(['jquery', 'core/str', 'core/ajax'], function ($, str, ajax) {
scale: 0.7, // scale to this fraction of image size
}
}
//console.info("Config", config);
//console.info("Colors", colors);
//console.info("Props", props);
debug.info(" Config", config);
debug.info(" Colors", colors);
debug.info(" Props", props);
// draw main circle
let baseGradient = ctx.createRadialGradient(
@ -72,7 +88,7 @@ define(['jquery', 'core/str', 'core/ajax'], function ($, str, ajax) {
(config.size ) * colors.radialGradient.r0,
config.size * colors.radialGradient.x1,
config.size * colors.radialGradient.y1,
(config.size ) * colors.radialGradient.r1,
(config.size ) * colors.radialGradient.r1
);
baseGradient.addColorStop(0, colors.lightPoint);
baseGradient.addColorStop(1, colors.base);
@ -157,9 +173,9 @@ define(['jquery', 'core/str', 'core/ajax'], function ($, str, ajax) {
imPos.x = (config.size * config.icon.x) - (imPos.w / 2);
imPos.y = (config.size * config.icon.y) - (imPos.h / 2);
ctx.drawImage(this, 15, 15, 120, 120);//imPos.x, imPos.y, imPos.w, imPos.h);
ctx.drawImage(this, imPos.x, imPos.y, imPos.w, imPos.h);
}
//console.info("Image: ",props.image);
//debug.info("Image: ",props.image);
iconImg.src = props.image;
}

196
amd/src/skilleditor.js Normal file
View File

@ -0,0 +1,196 @@
/*eslint no-var: "error"*/
/*eslint-env es6*/
// Put this file in path/to/plugin/amd/src
// You can call it anything you like
define(['jquery', 'core/str', 'core/ajax', 'core/config', 'block_gradelevel/renderbadge', 'block_gradelevel/handlers',
'block_gradelevel/debugger', 'core/modal_factory','core/modal_events'], function ($, str, ajax, mdlcfg, renderbadge, handlers, Debugger, ModalFactory, ModalEvents) {
let debug = Debugger("skilleditor");
debug.enable();
let $skillset_div = null;
const uploadFilter = ".png,.svg";
let self = {
init: function init() {
// Find skill table and skill id
$skillset_div = $("div#skill_set");
// Run initialization code on all items.
debug.info("Initializing editors");
$skillset_div.find("li.skill_info").each(function () {
self.initialize_item(this);
});
debug.info("Attaching function to addskill link");
$skillset_div.find("a[data-action='addskill']").on('click', function (ev) {
let promises = ajax.call([{
methodname: 'block_gradelevel_add_skill',
args: { name: null, icon: null },
}]);
// and link promise returns to callbacks
promises[0].fail(handlers.fail_report_exception).done(function (data) {
let $newskill = $(data.html);
$skillset_div.find('ul').append($newskill);
$newskill.each(function () {
self.initialize_item(this);
});
$newskill.find('figure.levelbadge').each(function () {
renderbadge.setup_badge(this);
});
});
});
},
initialize_item: function initialize_item(li) {
let $li = $(li);
let skill_id = $li.data('id');
// Attach edit name pencil
$li.find("a[data-action='editname']").on('click', function (evt) {
let $span = $li.find("figcaption span[data-type='label']");
if ($span.length > 0) {
let name = $span.text();
let $input = $("<input type='text'/>").val(name);// value='" + name + "' />");
$span.replaceWith($input);
$input.on("keyup", function input_keypress(e) {
let keycode = (e.keyCode ? e.keyCode : e.which);
if (keycode == '13') {
debug.info("Saving name");
// save the new name
let new_name = $input.val();
let promises = ajax.call([{
methodname: 'block_gradelevel_update_skill',
args: { id: skill_id, name: new_name, icon: null },
}]);
// and link promise returns to callbacks
promises[0].fail(handlers.fail_report_exception).done(function (data) {
$input.replaceWith($("<span data-type='label'>" + data.name + "</span>"));
});
}
if (keycode == '27') {
debug.info("Cancelled editing");
$input.replaceWith($("<span data-type='label'>" + name + "</span>"));
}
}).on("focusout", function input_cancel(e) {
debug.info("Cancelled editing");
$input.replaceWith($("<span data-type='label'>" + name + "</span>"));
});
$input.focus();
}
else {
let $textinput = $li.find("figurecaption input[type=text]");
}
});
// Attach edit icon pencil
$li.find("a[data-action='editicon']").on('click', function (ev) {
let $uploader = $("<input type='file' style='display: none;' accept='" + uploadFilter + "'/>");
$uploader.on('change', function (ev) {
ev.stopPropagation();
ev.preventDefault();
let file = ev.originalEvent.target.files[0];
let fr = new FileReader();
$(fr).on('load', function (ev) {
let dataURI = ev.target.result;
let promises = ajax.call([{
methodname: 'block_gradelevel_update_skill',
args: { id: skill_id, name: null, icon: dataURI },
}]);
// and link promise returns to callbacks
promises[0].fail(handlers.fail_report_exception).done(function (data) {
let $figure = $li.find("figure.levelbadge");
$figure.empty();
$figure.append($("<img style='display:none;' src='"+data.icon+"'/>"));
$figure.each(function () {
renderbadge.setup_badge(this);
});
});
$uploader.remove(); // delete the uploader after it's done
});
fr.readAsDataURL(file);
});
$li.append($uploader);
$uploader.trigger('click');
});
// Attach edit levels link
$li.find("a[data-action='editlevels']").attr('href', mdlcfg.wwwroot + "/blocks/gradelevel/cfg_skilllevels.php?skill_id=" + skill_id);
// Attach delete function to any existing functional delete link
$li.find("a[data-action='deleteskill']").each(function () {
let $trigger = $(this);
// Attach delete button code
ModalFactory.create({
type: ModalFactory.types.SAVE_CANCEL,
title: str.get_string('title_confirmdelete', 'block_gradelevel'),
body: str.get_string('dialog_confirmdeleteskill', 'block_gradelevel'),
}, $trigger).done(function (modal) {
// Do what you want with your new modal.
debug.info("Modal done", modal);
modal.setSaveButtonText(str.get_string('delete', 'core'));
modal.getRoot().on(ModalEvents.save, function (e) {
debug.info("Delete Confirmed");
//e.preventDefault(); if you don't want to close the window
// call the delete function
let promises = ajax.call([{
methodname: 'block_gradelevel_delete_skill',
args: { id: skill_id},
}]);
// and link promise returns to callbacks
promises[0].fail(handlers.fail_report_exception).done(function (data) {
if (data.deleted) {
$li.remove();
}
});
});
});
});
},
success_refill_table: function success_refill_table(response) {
//console.info("Response from webservice: ", response);
let $tbody = $skill_table.find('tbody');
$tbody.empty();
for (let idx in response) {
let lvl = response[idx];
//console.info("Level:", lvl);
self.add_levelrow(lvl.id, lvl.points, lvl.badgecolor);
}
},
fail_report_exception: function fail_report_exception(ex) {
if (ex.error != undefined) {
console.error("Error from webservice: ", ex.error, "\n" , ex.debuginfo, "\n---Stack trace---\n", ex.stacktrace, "\n", ex);
}
else {
console.error("Exception from webservice: ", ex.message, "\n", ex.debuginfo, "\n---Stack trace---\n", ex.backtrace, "\n", ex);
}
},
};
return self;
});

View File

@ -24,13 +24,6 @@ class block_gradelevel extends block_base {
// find or create the levelset for this course
$this->levelset = block_gradelevel_levelset::find_by_course($COURSE->id);
if(empty($this->levelset))
{
// create a new levelset with the name of this course and attach
$this->levelset = block_gradelevel_levelset::create_new($COURSE->shortname);
$this->levelset->attach_course($COURSE->id);
}
}
// The PHP tag and the curly bracket for the class definition
@ -43,6 +36,7 @@ class block_gradelevel extends block_base {
}
public function get_content() {
global $CFG;
global $USER;
global $COURSE;
@ -52,28 +46,34 @@ class block_gradelevel extends block_base {
$this->content = new stdClass;
// below can be a single call to $this->levelset->get_user_leveldata() once the need for debugging (fixed point total) is gone
$pointstotal = $this->levelset->get_levelset_grade($USER->id);
$level_info = $this->levelset->calculate_level($pointstotal);
if(empty($this->levelset))
{
$this->content->text = "<figure class='dummybadge'><img height='150px' width='150px' src='{$CFG->wwwroot}/blocks/gradelevel/pix/brokenbadge.svg' /></figure>";
$this->content->footer = get_string("unattached_course",'block_gradelevel');
}
else {
// below can be a single call to $this->levelset->get_user_leveldata() once the need for debugging (fixed point total) is gone
$pointstotal = $this->levelset->get_levelset_grade($USER->id);
$level_info = $this->levelset->calculate_level($pointstotal);
$this->content->text = $this->levelset->render_badge($pointstotal);
$this->content->text = $this->levelset->render_badge($pointstotal);
if($level_info->levelup_total > 0)
{
$this->content->footer = "<div class='pointinfo'>".get_string('levelup_at','block_gradelevel')." <span class='currentpoints'>{$level_info->points_in_level}</span>/<span class='leveluppoints'>{$level_info->levelup_total}</span></div>";
}
else
{
$this->content->footer = "<div class='pointinfo complete'>".get_string('levelup_done','block_gradelevel')."</div>";
}
if($level_info->levelup_total > 0)
{
$this->content->footer = "<div class='pointinfo'>".get_string('levelup_at','block_gradelevel')." <span class='currentpoints'>{$level_info->points_in_level}</span>/<span class='leveluppoints'>{$level_info->levelup_total}</span></div>";
}
else
{
$this->content->footer = "<div class='pointinfo complete'>".get_string('levelup_done','block_gradelevel')."</div>";
}
$coursecontext = context_course::instance($COURSE->id);
$coursecontext = context_course::instance($COURSE->id);
if(has_capability('block/gradelevel:viewresults', $coursecontext))
{
$this->content->footer .= "\n<div class='teachermode'><a href='#'>".get_string('teacher_view_results','block_gradelevel')."</a></div>";
}
if(has_capability('block/gradelevel:viewresults', $coursecontext))
{
$this->content->footer .= "\n<div class='teachermode'><a href='#'>".get_string('teacher_view_results','block_gradelevel')."</a></div>";
}
}
return $this->content;
}

48
cfg_skilllevels.php Normal file
View File

@ -0,0 +1,48 @@
<?php
if(isset($_SERVER['SCRIPT_FILENAME']))
{
// If SCRIPT_FILENAME is set, use that so the symlinked directories the developmen environment uses are handled correctly
$root = dirname(dirname(dirname($_SERVER['SCRIPT_FILENAME'])));
error_log("Using {$root}/config.php");
require_once($root."/config.php");
}
else
{
// If not, assume the cwd is not symlinked and proceed as we are used to
require_once("../../config.php");
}
// HOW DID WE ENSURE ONLY ADMINS CAN VIEW THIS PAGE?
require_once($CFG->libdir.'/adminlib.php');
admin_externalpage_setup("block_gradelevel_config_skills");
$skill_id = required_param('skill_id', PARAM_INT);
$systemcontext = context_system::instance();
// Check if user has capability to manage skills
require_capability('block/gradelevel:skillmanager', $systemcontext);
$skill = block_gradelevel_levelset::find_by_id($skill_id);
$PAGE->requires->js_call_amd('block_gradelevel/renderbadge', 'init');
$PAGE->requires->js_call_amd('block_gradelevel/skilleditor', 'init');
$PAGE->requires->js_call_amd('block_gradelevel/leveleditor', 'init');
print $OUTPUT->header();
// render skill editor
print $OUTPUT->heading(get_string('cfgpage_editskill','block_gradelevel')." ".$skill->getName());
print block_gradelevel_skillmgmtservice::render_skill_editor($skill_id);
// render level editor
print $OUTPUT->heading(get_string('cfgpage_skilllevels','block_gradelevel'));
print block_gradelevel_skillmgmtservice::render_leveltable($skill_id);
// add back button to return to skill management page
$cfg_skills_url = $CFG->wwwroot."/blocks/gradelevel/cfg_skills.php";
print "<button onclick='window.location=\"{$cfg_skills_url}\";'>".get_string('back','core')."</button>";
print $OUTPUT->footer();

35
cfg_skills.php Normal file
View File

@ -0,0 +1,35 @@
<?php
if(isset($_SERVER['SCRIPT_FILENAME']))
{
// If SCRIPT_FILENAME is set, use that so the symlinked directories the developmen environment uses are handled correctly
$root = dirname(dirname(dirname($_SERVER['SCRIPT_FILENAME'])));
error_log("Using {$root}/config.php");
require_once($root."/config.php");
}
else
{
// If not, assume the cwd is not symlinked and proceed as we are used to
require_once("../../config.php");
}
// HOW DID WE ENSURE ONLY ADMINS CAN VIEW THIS PAGE?
require_once($CFG->libdir.'/adminlib.php');
admin_externalpage_setup("block_gradelevel_config_skills");
$systemcontext = context_system::instance();
// Check if user has capability to manage skills
require_capability('block/gradelevel:skillmanager', $systemcontext);
$PAGE->requires->js_call_amd('block_gradelevel/skilleditor', 'init');
$PAGE->requires->js_call_amd('block_gradelevel/renderbadge', 'init');
print $OUTPUT->header();
print $OUTPUT->heading(get_string('cfgpage_skills', 'block_gradelevel'));
// render page for skill level 0 (global)
print block_gradelevel_skillmgmtservice::render_skill_list(0);
print $OUTPUT->footer();

View File

@ -141,9 +141,11 @@ class block_gradelevel_levelset {
return null; // return null if no current levelset linked
}
static public function find_by_id($id)
static public function find_by_id(int $id)
{
$levelset = $DB->get_record('block_gradelevel_levelset', array('id' => array_values($records)[0]->levelset_id));
global $DB;
$levelset = $DB->get_record('block_gradelevel_levelset', array('id' => $id));
if($levelset)
{
return new static($levelset->id,$levelset);
@ -397,7 +399,6 @@ class block_gradelevel_levelset {
// If we have levels defined, use those, otherwise use the global levels
if(!empty($this->levels))
{
print ("EXSTING_LEVELS");
if(array_values($this->levels)[0]->points > 0)
{
// insert level 0
@ -411,16 +412,13 @@ class block_gradelevel_levelset {
if(!empty($lvl->badgecolor)) {
$color = $lvl->badgecolor;
}
elseif(isset($this->global_levels[$i]))
{
$color = $this->global_levels[$i]->badgecolor;
}
else
{
$color = static::UNDEFINED_BADGE_COLOR;
}
$level_info[$lvl->points];
$level_info[$lvl->points] = $color;
$i++;
}
}
@ -458,7 +456,7 @@ class block_gradelevel_levelset {
$info = $this->calculate_level($points);
$image = $this->getIcon();
if(strncmp($image,"data: ",6) != 0)
if(strncmp($image,"data:",5) != 0)
{
$image_url = $CFG->wwwroot.$image;
}
@ -487,7 +485,7 @@ class block_gradelevel_levelset {
$levels = $this->badgelevels();
$maxpoints = array_pop(array_keys($levels));
return $this->render_badge($maxpoints,$size);
return $this->render_badge($maxpoints, $size);
}

View File

@ -8,8 +8,8 @@ require_once($CFG->dirroot.'/grade/querylib.php');
class block_gradelevel_skillmgmtservice extends external_api
{
const DEBUG = false; // enable debug logging
const DEMOBADGE_SIZE = 100; // size of demo badge
const DEBUG = true; // enable debug logging
const DEMOBADGE_SIZE = 150; // size of demo badge
private static function log($message)
{
@ -199,28 +199,6 @@ class block_gradelevel_skillmgmtservice extends external_api
$systemcontext = context_system::instance();
self::validate_context($systemcontext);
if(skill_id == 0)
{
// Check if user has capability to manage global levels
require_capability('block/gradelevel:managelevels', $systemcontext);
}
else
{
// Check if user has teacher or manager capability in any of the attached courses
$capability = false;
$courses = self::list_courses($skill_id);
foreach($courses as $course_id)
{
$coursecontext = context_course::instance($course_id);
$capability |= has_capability('block/gradelevel:changelevels', $coursecontext);
}
if(!$capability)
{
throw new Exception("Access denied - you do not have editing capabilities for one of the attached courses");
}
}
foreach($levels as $lvl_raw)
{
@ -273,13 +251,35 @@ class block_gradelevel_skillmgmtservice extends external_api
$levels = $DB->get_records('block_gradelevel_levels', array('levelset_id' => $skill_id));
// Sort by points
usort( $levels, function( $a, $b) {
return ( $a->points < $b->points ) ? -1 : 1;
} );
if($skill_id == 0 || count($levels) > 0)
{
// If global level, or skills are defined, return those
// Sort by points
usort( $levels, function( $a, $b) {
return ( $a->points < $b->points ) ? -1 : 1;
} );
return $levels;
return $levels;
}
else
{
// Else, return a nameless clone of the default levels).
$levels = $DB->get_records('block_gradelevel_levels', array('levelset_id' => 0));
// Sort by points
usort( $levels, function( $a, $b) {
return ( $a->points < $b->points ) ? -1 : 1;
} );
foreach($levels as $lvl)
{
$lvl->id = -255; // replace level id with -255, which is code for "new level" when returned
}
return $levels;
}
}
@ -316,35 +316,51 @@ class block_gradelevel_skillmgmtservice extends external_api
}
public static function update_skill(int $id, string $name, string $icon )
public static function update_skill(int $id, $name, $icon )
{
global $CFG, $DB;
$skill = $DB->get_record('block_gradelevel_levelset', array('id' => $id));
$skill->name = $name;
$skill->icon = $icon;
if($name != null){
$skill->name = $name;
}
if($icon != null){
$skill->icon = $icon;
}
$DB->update_record('block_gradelevel_levelset',$skill);
$skill->html = block_gradelevel_levelset::find_by_id($skill->id)->render_demo_badge(static::DEMOBADGE_SIZE);
return $skill;
}
public static function add_skill(string $name, string $icon)
public static function add_skill($name, $icon)
{
global $CFG, $DB;
$skill = stdObj;
$skill = new stdClass;
$skill->name = $name;
$skill->icon = $icon;
if(empty($name)){
$skill->name = get_string('defaults_name','block_gradelevel');
}
else {
$skill->name = $name;
}
if(empty($icon)){
$skill->icon = "/blocks/gradelevel/pix/undefinedskill.svg";
}
else {
$skill->icon = $icon;
}
$id = $DB->insert_record('block_gradelevel_levelset',$skill, true);
$skill->id = $id;
$skill->html = block_gradelevel_levelset::find_by_id($skill->id)->render_demo_badge(static::DEMOBADGE_SIZE);
$skill->html = static::single_skill_editor_item(block_gradelevel_levelset::find_by_id($skill->id));
return $skill;
}
@ -353,10 +369,14 @@ class block_gradelevel_skillmgmtservice extends external_api
{
global $CFG, $DB;
$result = stdObj;
$result->id = $id;
$skill = block_gradelevel_levelset::find_by_id($id);
if(count($skill->list_courses()) > 0)
{
throw new Exception("Cannot delete skills that have courses attached");
}
$result->id = ($DB->delete_record('block_gradelevel_levelset',array('id' => $id)))?true:false;
$result = array('id' => $id);
$result['deleted'] = ($DB->delete_records('block_gradelevel_levelset',array('id' => $id)))?true:false;
return $result;
}
@ -368,7 +388,7 @@ class block_gradelevel_skillmgmtservice extends external_api
$levels = static::list_levels($skill_id);
$s = "<p>".get_string('levelcfg_description','block_gradelevel')."</p>";
$s .= "<table id='level_config' data-skill='{$skill_id}'>";
$s .= "<table id='level_config' class='level_config' data-skill='{$skill_id}'>";
$s .= "<thead><tr><th>".get_string('levelcfg_head_points','block_gradelevel')."</th><th>".get_string('levelcfg_head_color','block_gradelevel')."</th></tr></thead>";
$s .= "<tbody>";
foreach($levels as $lvl)
@ -380,7 +400,12 @@ class block_gradelevel_skillmgmtservice extends external_api
$s .= "</tbody>";
$s .= "<tfoot><tr><td class='block_gradelevel_addlevel' colspan='2'><a data-action='addlevel' href='#' onclick='return false;'>".get_string("levelcfg_addlevel",'block_gradelevel')."</a></td></td></tfoot>";
$s .= "</table>";
$s .= "<p><button data-action='savechanges'>".get_string('savechanges','core')."</button></p>";
$s .= "<p><button data-action='savechanges'>".get_string('savechanges','core')."</button>";
if($skill_id > 0)
{
$s .= " <button data-action='resetlevels'>".get_string('cfg_resetlevels','block_gradelevel')."</button>";
}
$s .= "</p>";
return $s;
@ -388,19 +413,61 @@ class block_gradelevel_skillmgmtservice extends external_api
public static function render_skill_list()
{
global $CFG;
$skills = block_gradelevel_levelset::list_all();
$s = "<div class='skill_set'><ul>";
// Sort by points
usort( $skills, function( $a, $b) {
return ( $a->getName() < $b->getName()) ? -1 : 1;
} );
$s = "<div id='skill_set' class='skill_set'><ul>";
foreach($skills as $skill)
{
$skill_id = $skill->getId();
$s .= "<li class='skill_info' data-id='{$skill_id}'>".$skill->render_demo_badge(DEMOBADGE_SIZE);
$s .= "<figcaption data-for='name'>".$skill->getName()."</figcaption>";
$s .= "</li>";
$s .= static::single_skill_editor_item($skill);
}
$s .= "</ul>";
$s .= "<a href='#' onclick='return false;' data-action='addskill'>".get_string('cfg_addskill','block_gradelevel')."</a>";
$s .= "</div>";
return $s;
}
public static function render_skill_editor(int $skill_id)
{
$skill = block_gradelevel_levelset::find_by_id($skill_id);
$s = "<div id='skill_set' class='skill_set'><ul>";
$s .= static::single_skill_editor_item($skill,true);
$s .= "</ul></div>";
return $s;
}
private static function single_skill_editor_item($skill,$single=false)
{
global $OUTPUT;
$skill_id = $skill->getId();
$s = "<li class='skill_info' data-id='{$skill_id}'>";
if(!$single){
$s .= "<a data-action='editlevels' href='#'>";
}
$s .= $skill->render_demo_badge(static::DEMOBADGE_SIZE)."</a>";
if(!$single){
$s .= "<a data-action='editlevels' href='#'>";
if(count($skill->list_courses()) ){
$s .= "<a class='deleteskill' data-action='deleteskill' onclick='return false;' href='#'><i class='icon fa fa-minus-circle fa-fw' title='".get_string('tooltip_deleteskill','block_gradelevel')."'></i></a>";
}
}
$s .= "<a class='editicon' data-action='editicon' onclick='return false;' href='#'><i class='icon fa fa-pencil fa-fw' title='".get_string('tooltip_editicon','block_gradelevel')."'></i></a>";
$s .= "<figcaption data-for='name'><span data-type='label'>".$skill->getName()."</span>";
$s .= "<a class='editname' data-action='editname' onclick='return false;' href='#'><i class='icon fa fa-pencil fa-fw' title='".get_string('tooltip_editname','block_gradelevel')."'></i></a>";
$s .= "</figcaption></li>";
return $s;
}
}

View File

@ -13,35 +13,35 @@ class block_gradelevel_edit_form extends block_edit_form {
// Section header title according to language file.
$mform->addElement('header', 'config_header', get_string('blocksettings', 'block'));
// A sample string variable with a default value.
$mform->addElement('text', 'levelset_name', get_string('levelset_name', 'block_gradelevel'));
$icon = $levelset->getIcon();
if(strncmp($icon,"data: ",6) != 0)
$skills = block_gradelevel_levelset::list_all();
// sort by name
usort( $skills, function( $a, $b) {
return ( $a->getName() < $b->getName()) ? -1 : 1;
} );
$options = array( '-1' => get_string('edit_noneskill', 'block_gradelevel'));
foreach($skills as $skill)
{
$icon = $CFG->wwwroot.$icon;
$options[$skill->getId()] = $skill->getName();
}
$icon_title = get_string('levelset_icon_cur', 'block_gradelevel');
$mform->addElement('html',"<div class='form-group row fitem'><div class='col-md-3'>{$icon_title}</div><div class='col-md-9'><figure class='levelset_icon'><img src='{$icon}' /></figure></div></div>");
// A sample string variable with a default value.
$mform->addElement('filepicker', 'levelset_icon', get_string('levelset_icon_new', 'block_gradelevel'), null,
array('maxbytes' => $maxbytes, 'accepted_types' => array('.png', '.svg')));
$mform->addElement('header', 'config_header', get_string('levelset_levels', 'block_gradelevel'));
$mform->addElement('html',print block_gradelevel_managelevelservice::render_leveltable(0));
$mform->addElement('select', 'attached_skill', get_string('edit_pickskill', 'block_gradelevel'), $options);
}
public function set_data($defaults)
{
global $COURSE;
$defaults->attached_skill = -1;
// retrieve levelset
$levelset = $this->block->levelset;
$skill = block_gradelevel_levelset::find_by_course($COURSE->id);
if(isset($skill)){
$defaults->attached_skill = $skill->getId();
}
$defaults->levelset_name = $levelset->getName();
$defaults->levelset_icon = $levelset->getIcon();
parent::set_data($defaults);
}
@ -61,50 +61,26 @@ class block_gradelevel_edit_form extends block_edit_form {
protected function save_data($data)
{
global $COURSE;
// retrieve levelset
$levelset = $this->block->levelset;
$this->debug("data: ".print_r($data,true));
if(isset($data->levelset_name)){
$levelset->setName($data->levelset_name);
}
if(isset($data->levelset_icon))
if(isset($data->attached_skill))
{
$filename = $this->get_new_filename('levelset_icon');
$contents = $this->get_file_content('levelset_icon');
$imageData = base64_encode($contents);
$pi = pathinfo($filename);
if($pi['extension'] == 'svg')
$currentskill = block_gradelevel_levelset::find_by_course($COURSE->id);
if(isset($currentskill))
{
$mime = "image/svg+xml";
}
else if($pi['extension'] == 'png')
{
$mime = "image/png";
}
else
{
$mime = "binary/octet-stream"; // no idea what it is
$this->debug("deetaching...");
$currentskill->detach_course($COURSE->id);
}
$dataSrc = 'data: '.$mime.';base64,'.$imageData;
$levelset->setIcon($dataSrc);
$skill = block_gradelevel_levelset::find_by_id($data->attached_skill);
if(isset($skill)){
$this->debug("attaching...");
$skill->attach_course($COURSE->id);
}
}
$this->debug("levelset: ".print_r($levelset,true));
$levelset->save_data();
}
private function debug($msg)
{
error_log($msg, 3, '/tmp/moodledevlog.log');

View File

@ -7,16 +7,18 @@ $string['gradelevel:myaddinstance'] = 'Add a new Grade Level block to the My Moo
$string['blockstring'] = 'Text in the block';
$string['levelup_at'] = 'Next level: ';
$string['levelup_done'] = 'Complete';
$string['unattached_course'] = 'Please attach this course to a skill in the block settings';
$string['teacher_view_results'] = 'View student results';
$string['levelset_name'] = "Skill name";
$string['levelset_icon_cur'] = "Current skill icon (for level badge)";
$string['levelset_icon_new'] = "New skill icon (for level badge)";
$string['edit_pickskill'] = "Attach to skill";
$string['edit_noneskill'] = "None";
$string['cfgpage_generic'] = "Grade Level settings";
$string['cfgpage_globallevels'] = "Default levels";
$string['cfgpage_skilllevels'] = "Edit levels";
$string['cfgpage_skills'] = "Manage skills";
$string['cfgpage_editskill'] = "Edit skill";
$string['descconfig'] = "Configuration for Grade Level block";
@ -34,3 +36,16 @@ $string['levelcfg_description'] = "Levels will be sorted based on points. Make s
$string['levelcfg_head_points'] = "Points";
$string['levelcfg_head_color'] = "Medal color";
$string['levelcfg_addlevel'] = "Add level";
$string['tooltip_editicon'] = "Edit icon";
$string['tooltip_editname'] = "Edit name";
$string['tooltip_deleteskill'] = "Delete skill";
$string['tooltip_deleteskill_disabled'] = "Cannot delete skill, because there are attached courses";
$string['defaults_name'] = "New skill";
$string['cfg_resetlevels'] = "Reset levels to default";
$string['cfg_addskill'] = "Add skill";
$string['title_confirmdelete'] = "Confirm delete";
$string['dialog_confirmdeleteskill'] = "Are you sure you want to delete this skill?";
//$string[''] = "";

2601
pix/brokenbadge.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 92 KiB

View File

@ -18,16 +18,23 @@ $settings->add(new admin_setting_configtext(
));
$ADMIN->add("block_gradelevel", $settings);
// Add the default levels page
$external = new admin_externalpage(
'block_gradelevel_default_levels',
get_string('cfgpage_globallevels', 'block_gradelevel'),
$CFG->wwwroot . '/blocks/gradelevel/globallevels.php'
);
$ADMIN->add("block_gradelevel", new admin_externalpage(
'block_gradelevel_default_levels',
get_string('cfgpage_globallevels', 'block_gradelevel'),
$CFG->wwwroot . '/blocks/gradelevel/cfg_globallevels.php'
)
);
$ADMIN->add("block_gradelevel", $settings);
$ADMIN->add("block_gradelevel", $external);
// Add the skill configpage
$ADMIN->add("block_gradelevel", new admin_externalpage(
'block_gradelevel_config_skills',
get_string('cfgpage_skills', 'block_gradelevel'),
$CFG->wwwroot . '/blocks/gradelevel/cfg_skills.php'
)
);
$settings = null;

View File

@ -1,4 +1,4 @@
figure.levelbadge {
figure.levelbadge, figure.dummybadge {
text-align: center;
}
@ -6,17 +6,19 @@ div.pointinfo {
text-align: center;
font-family: "Open Sans", Arial, helvetica, sans-serif;
}
div.pointinfo.complete {
font-weight: bold;
}
div.pointinfo span.currentpoints {
font-weight: bold;
}
div.pointinfo.complete {
font-weight: bold;
}
div.pointinfo span.currentpoints {
font-weight: bold;
}
div.pointinfo span.leveluppoints {
font-weight: bold;
}
div.pointinfo span.leveluppoints {
font-weight: bold;
}
div.teachermode {
text-align: center;
}
@ -29,6 +31,70 @@ input.jscolor[type=text] {
width: 5em;
}
div.skill_set ul {
padding-left: 0px;
margin-left: 0px;
}
div.skill_set li.skill_info {
position: relative; /* needed to have absolute positions be relative to this li */
list-style: none;
display: inline-block;
width: 220px;
height: 220px;
border-color: #ccc;
border-width: 1px;
border-style: solid;
margin: 5px;
padding: 5px;
padding-top: 10px;
}
div.skill_set li.skill_info figcaption {
text-align: center;
}
div.skill_set li.skill_info input[type=text] {
width: 150px;
}
div.skill_set li.skill_info a.editicon {
position: absolute;
top: 5px;
right: 10px;
}
div.skill_set li.skill_info a.deleteskill {
position: absolute;
top: 5px;
left: 7px;
}
div.skill_set li.skill_info a.deleteskill i.icon {
color: red;
}
div.skill_set li.skill_info a.deleteskill i.icon.disabled {
color: #ccc;
}
div.skill_set li.skill_info a.editname {
margin-left: 5px;
}
table.level_config tr[data-rowid='-255'] {
}
table.level_config tr[data-rowid='-255'] td {
}
table.level_config tr[data-rowid='-255'] input[data-name='points'] {
background-color: #F0F0F0;
}
figure.levelset_icon {
background-color: #ccc;
background-image: -moz-linear-gradient(45deg, #aaa 25%, transparent 25%), -moz-linear-gradient(-45deg, #aaa 25%, transparent 25%), -moz-linear-gradient(45deg, transparent 75%, #aaa 75%), -moz-linear-gradient(-45deg, transparent 75%, #aaa 75%);

View File

@ -1,4 +1,4 @@
<?php
$plugin->component = 'block_gradelevel'; // Recommended since 2.0.2 (MDL-26035). Required since 3.0 (MDL-48494)
$plugin->version = 2018092101; // YYYYMMDDHH (year, month, day, 24-hr time)
$plugin->version = 2018092300; // YYYYMMDDHH (year, month, day, 24-hr time)
$plugin->requires = 2018050800; // YYYYMMDDHH (This is the release version for Moodle 3.5)