mirror of
https://github.com/moodle/moodle.git
synced 2025-04-21 00:12:56 +02:00
MDL-14163 YUI implementation complete, new grader_report preference and admin setting for enabling ajax.
This commit is contained in:
parent
d0e422c45a
commit
b244b9b776
119
grade/lib.php
119
grade/lib.php
@ -987,7 +987,7 @@ class grade_structure {
|
||||
}
|
||||
|
||||
if ($url) {
|
||||
return '<a href="'.$url.'"><img '.$overlib.' src="'.$CFG->pixpath.'/t/edit.gif" class="iconsmall" alt="'.$stredit.'" title="'.$stredit.'"/></a>';
|
||||
return '<a href="'.$url.'" class="edit"><img '.$overlib.' src="'.$CFG->pixpath.'/t/edit.gif" class="iconsmall" alt="'.$stredit.'" title="'.$stredit.'"/></a>';
|
||||
|
||||
} else {
|
||||
return '';
|
||||
@ -1022,13 +1022,13 @@ class grade_structure {
|
||||
$url = $CFG->wwwroot.'/grade/edit/tree/action.php?id='.$this->courseid.'&action=show&sesskey='.sesskey()
|
||||
. '&eid='.$element['eid'];
|
||||
$url = $gpr->add_url_params($url);
|
||||
$action = '<a href="'.$url.'"><img alt="'.$strshow.'" src="'.$CFG->pixpath.'/t/'.$icon.'.gif" class="iconsmall" title="'.$tooltip.'"/></a>';
|
||||
$action = '<a href="'.$url.'" class="hide"><img alt="'.$strshow.'" src="'.$CFG->pixpath.'/t/'.$icon.'.gif" class="iconsmall" title="'.$tooltip.'"/></a>';
|
||||
|
||||
} else {
|
||||
$url = $CFG->wwwroot.'/grade/edit/tree/action.php?id='.$this->courseid.'&action=hide&sesskey='.sesskey()
|
||||
. '&eid='.$element['eid'];
|
||||
$url = $gpr->add_url_params($url);
|
||||
$action = '<a href="'.$url.'"><img src="'.$CFG->pixpath.'/t/hide.gif" class="iconsmall" alt="'.$strhide.'" title="'.$strhide.'"/></a>';
|
||||
$action = '<a href="'.$url.'" class="hide"><img src="'.$CFG->pixpath.'/t/hide.gif" class="iconsmall" alt="'.$strhide.'" title="'.$strhide.'"/></a>';
|
||||
}
|
||||
return $action;
|
||||
}
|
||||
@ -1066,7 +1066,7 @@ class grade_structure {
|
||||
$url = $CFG->wwwroot.'/grade/edit/tree/action.php?id='.$this->courseid.'&action=unlock&sesskey='.sesskey()
|
||||
. '&eid='.$element['eid'];
|
||||
$url = $gpr->add_url_params($url);
|
||||
$action = '<a href="'.$url.'"><img src="'.$CFG->pixpath.'/t/'.$icon.'.gif" alt="'.$strunlock.'" class="iconsmall" title="'.$tooltip.'"/></a>';
|
||||
$action = '<a href="'.$url.'" class="lock"><img src="'.$CFG->pixpath.'/t/'.$icon.'.gif" alt="'.$strunlock.'" class="iconsmall" title="'.$tooltip.'"/></a>';
|
||||
|
||||
} else {
|
||||
if (!has_capability('moodle/grade:manage', $this->context) and !has_capability('moodle/grade:lock', $this->context)) {
|
||||
@ -1075,7 +1075,7 @@ class grade_structure {
|
||||
$url = $CFG->wwwroot.'/grade/edit/tree/action.php?id='.$this->courseid.'&action=lock&sesskey='.sesskey()
|
||||
. '&eid='.$element['eid'];
|
||||
$url = $gpr->add_url_params($url);
|
||||
$action = '<a href="'.$url.'"><img src="'.$CFG->pixpath.'/t/lock.gif" class="iconsmall" alt="'.$strlock.'" title="'
|
||||
$action = '<a href="'.$url.'" class="lock"><img src="'.$CFG->pixpath.'/t/lock.gif" class="iconsmall" alt="'.$strlock.'" title="'
|
||||
. $strlock.'"/></a>';
|
||||
}
|
||||
return $action;
|
||||
@ -1111,7 +1111,7 @@ class grade_structure {
|
||||
}
|
||||
$url = $CFG->wwwroot.'/grade/edit/tree/calculation.php?courseid='.$this->courseid.'&id='.$object->id;
|
||||
$url = $gpr->add_url_params($url);
|
||||
$calculation_icon = '<a href="'. $url.'"><img src="'.$CFG->pixpath.'/t/'.$icon.'" class="iconsmall" alt="'
|
||||
$calculation_icon = '<a href="'. $url.'" class="calculation"><img src="'.$CFG->pixpath.'/t/'.$icon.'" class="iconsmall" alt="'
|
||||
. $streditcalculation.'" title="'.$streditcalculation.'" /></a>'. "\n";
|
||||
}
|
||||
}
|
||||
@ -1548,6 +1548,113 @@ class grade_tree extends grade_structure {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a well-formed XML representation of the grade-tree using recursion.
|
||||
* @param array $root The current element in the recursion. If null, starts at the top of the tree.
|
||||
* @return string $xml
|
||||
*/
|
||||
function exportToXML($root=null, $tabs="\t") {
|
||||
$xml = null;
|
||||
$first = false;
|
||||
if (is_null($root)) {
|
||||
$root = $this->top_element;
|
||||
$xml = '<?xml version="1.0" encoding="UTF-8" ?>' . "\n";
|
||||
$xml .= "<gradetree>\n";
|
||||
$first = true;
|
||||
}
|
||||
|
||||
$type = 'undefined';
|
||||
if (strpos($root['object']->table, 'grade_categories') !== false) {
|
||||
$type = 'category';
|
||||
} elseif (strpos($root['object']->table, 'grade_items') !== false) {
|
||||
$type = 'item';
|
||||
} elseif (strpos($root['object']->table, 'grade_outcomes') !== false) {
|
||||
$type = 'outcome';
|
||||
}
|
||||
|
||||
$xml .= "$tabs<element type=\"$type\">\n";
|
||||
foreach ($root['object'] as $var => $value) {
|
||||
if (!is_object($value) && !is_array($value) && !empty($value)) {
|
||||
$xml .= "$tabs\t<$var>$value</$var>\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($root['children'])) {
|
||||
$xml .= "$tabs\t<children>\n";
|
||||
foreach ($root['children'] as $sortorder => $child) {
|
||||
$xml .= $this->exportToXML($child, $tabs."\t\t");
|
||||
}
|
||||
$xml .= "$tabs\t</children>\n";
|
||||
}
|
||||
|
||||
$xml .= "$tabs</element>\n";
|
||||
|
||||
if ($first) {
|
||||
$xml .= "</gradetree>";
|
||||
}
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a JSON representation of the grade-tree using recursion.
|
||||
* @param array $root The current element in the recursion. If null, starts at the top of the tree.
|
||||
* @param string $tabs Tab characters used to indent the string nicely for humans to enjoy
|
||||
* @param int $switch The position (first or last) of the aggregations
|
||||
* @return string $xml
|
||||
*/
|
||||
function exportToJSON($root=null, $tabs="\t") {
|
||||
$json = null;
|
||||
$first = false;
|
||||
if (is_null($root)) {
|
||||
$root = $this->top_element;
|
||||
$first = true;
|
||||
}
|
||||
|
||||
$name = '';
|
||||
|
||||
|
||||
if (strpos($root['object']->table, 'grade_categories') !== false) {
|
||||
$name = $root['object']->fullname;
|
||||
if ($name == '?') {
|
||||
$name = $root['object']->get_name();
|
||||
}
|
||||
} elseif (strpos($root['object']->table, 'grade_items') !== false) {
|
||||
$name = $root['object']->itemname;
|
||||
} elseif (strpos($root['object']->table, 'grade_outcomes') !== false) {
|
||||
$name = $root['object']->itemname;
|
||||
}
|
||||
|
||||
$json .= "$tabs {\n";
|
||||
$json .= "$tabs\t \"type\": \"{$root['type']}\",\n";
|
||||
$json .= "$tabs\t \"name\": \"$name\",\n";
|
||||
|
||||
foreach ($root['object'] as $var => $value) {
|
||||
if (!is_object($value) && !is_array($value) && !empty($value)) {
|
||||
$json .= "$tabs\t \"$var\": \"$value\",\n";
|
||||
}
|
||||
}
|
||||
|
||||
$json = substr($json, 0, strrpos($json, ','));
|
||||
|
||||
if (!empty($root['children'])) {
|
||||
$json .= ",\n$tabs\t\"children\": [\n";
|
||||
foreach ($root['children'] as $sortorder => $child) {
|
||||
$json .= $this->exportToJSON($child, $tabs."\t\t");
|
||||
}
|
||||
$json = substr($json, 0, strrpos($json, ','));
|
||||
$json .= "\n$tabs\t]\n";
|
||||
}
|
||||
|
||||
if ($first) {
|
||||
$json .= "\n}";
|
||||
} else {
|
||||
$json .= "\n$tabs},\n";
|
||||
}
|
||||
|
||||
return $json;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
582
grade/report/grader/ajax.php
Normal file
582
grade/report/grader/ajax.php
Normal file
@ -0,0 +1,582 @@
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
|
||||
// If the mouse is clicked outside this element, the edit is CANCELLED (even if the mouse clicks another grade/feedback cell)
|
||||
// If ctrl-arrow is used, or if [tab] or [enter] are pressed, the edit is RECORDED and the row is updated. The previous element returns to normal
|
||||
|
||||
YAHOO.namespace("grader_report");
|
||||
YAHOO.grader_report.el_being_edited = null;
|
||||
YAHOO.grader_report.courseid = <?php echo $COURSE->id; ?>;
|
||||
YAHOO.grader_report.wwwroot = '<?php echo $CFG->wwwroot; ?>';
|
||||
YAHOO.grader_report.straddfeedback = '<?php echo get_string("addfeedback", "grades"); ?>';
|
||||
YAHOO.grader_report.strfeedback = '<?php echo get_string("feedback", "grades"); ?>';
|
||||
YAHOO.grader_report.feedback_trunc_length = <?php echo $report->feedback_trunc_length ?>;
|
||||
YAHOO.grader_report.decimalpoints = <?php echo $report->getItemsDecimalPoints() ?>;
|
||||
YAHOO.grader_report.studentsperpage = <?php echo $report->get_pref('studentsperpage'); ?>;
|
||||
YAHOO.grader_report.showquickfeedback = <?php echo $report->get_pref('showquickfeedback'); ?>;
|
||||
|
||||
// Feedback data is cached in a JS array: we don't want to show complete feedback strings in the report, but
|
||||
// neither do we want to fetch the feedback from PHP each time we click one to edit it
|
||||
YAHOO.grader_report.feedbacks = <?php echo $report->getFeedbackJsArray(); ?>
|
||||
|
||||
|
||||
/**
|
||||
* Given an elementId formatted as grade[cell|value|feedback]_u[$userId]-i[$itemId], returns an object with key-value pairs
|
||||
* @param string elId
|
||||
* @return object
|
||||
*/
|
||||
YAHOO.grader_report.getIdData = function(elId) {
|
||||
var re = /grade(value|feedback|cell|scale)_([0-9]*)-i([0-9]*)/;
|
||||
var matches = re.exec(elId);
|
||||
if (undefined != matches && matches.length > 0) {
|
||||
return {type: matches[1], userId: matches[2], itemId: matches[3]};
|
||||
} else {
|
||||
YAHOO.log("getIdData: Invalid elementId: " + elId, "warn");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reverse-engineering of getIdData: returns a string based on an object
|
||||
* @param object idData
|
||||
* @return string
|
||||
*/
|
||||
YAHOO.grader_report.getElementId = function(idData) {
|
||||
if (undefined != idData.userId && undefined != idData.type && undefined != idData.itemId) {
|
||||
return "grade" + idData.type + "_" + idData.userId + "-i" + idData.itemId;
|
||||
} else {
|
||||
YAHOO.log("getElementId: Invalid elementId: " + idData, "warn");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface to the overlib js library. DEPENDENCY ALERT!
|
||||
*/
|
||||
YAHOO.grader_report.tooltip = function(e, dataObj) {
|
||||
var gr = YAHOO.grader_report;
|
||||
if (undefined != dataObj.text) {
|
||||
return overlib(dataObj.text, BORDER, 0, FGCLASS, 'feedback', CAPTIONFONTCLASS, 'caption', CAPTION, gr.strfeedback);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends the record request and un-edits the element
|
||||
* @param object editedEl The DOM element being edited, whose value we are trying to save in DB
|
||||
* @return array An array of values used to update the row
|
||||
*/
|
||||
YAHOO.grader_report.saveField = function(editedEl) {
|
||||
var gr = YAHOO.grader_report;
|
||||
var idData = gr.getIdData(editedEl.id);
|
||||
|
||||
if (idData.type == 'value') { // Text input
|
||||
var newVal = editedEl.firstChild.value;
|
||||
} else if (idData.type == 'feedback') { // Textarea
|
||||
var newVal = editedEl.firstChild.innerHTML;
|
||||
} else if (idData.type == 'scale') { // Select
|
||||
var newVal = editedEl.options[editedEl.selectedIndex].value;
|
||||
}
|
||||
|
||||
// Don't save if the new value is the same as the old
|
||||
if (gr.el_being_edited.value == newVal || (gr.el_being_edited.value == gr.straddfeedback && newVal == '')) {
|
||||
YAHOO.log("saveField: Field unchanged, not saving. (" + newVal + ")", "info");
|
||||
return false;
|
||||
}
|
||||
|
||||
YAHOO.log("saveField: Old value: " + gr.el_being_edited.value + ", new value: " + newVal + ". Saving field...", "info");
|
||||
|
||||
var postData = "id=" + gr.courseid + "&userid=" + idData.userId + "&itemid=" + idData.itemId +
|
||||
"&action=update&newvalue=" + newVal + "&type=" + idData.type;
|
||||
|
||||
var handleSuccess = function(o) {
|
||||
try {
|
||||
var queryResult = YAHOO.lang.JSON.parse(o.responseText);
|
||||
} catch (e) {
|
||||
YAHOO.log("saveField: JSON syntax error! " + o.responseText, "error");
|
||||
}
|
||||
|
||||
if (queryResult.result == "success") {
|
||||
// For a textarea, truncate the feedback to 40 chars and provide a tooltip with the full text
|
||||
if (queryResult.gradevalue == null) {
|
||||
if (idData.type == 'scale') {
|
||||
editedEl.selectedIndex = 0;
|
||||
} else {
|
||||
editedEl.innerHTML = '';
|
||||
}
|
||||
} else if (idData.type == 'feedback') {
|
||||
editedEl.innerHTML = gr.truncateText(queryResult.gradevalue);
|
||||
|
||||
YAHOO.util.Event.addListener(editedEl.id, 'mouseover', gr.tooltip, {text: queryResult.gradevalue}, true);
|
||||
YAHOO.util.Event.addListener(editedEl.id, 'mouseout', nd); // See overlib doc for reference
|
||||
gr.feedbacks[idData.userId][idData.itemId] = queryResult.gradevalue;
|
||||
} else if (idData.type == 'value') {
|
||||
editedEl.innerHTML = gr.roundValue(queryResult.gradevalue, idData.itemId);
|
||||
}
|
||||
|
||||
YAHOO.util.Dom.addClass(editedEl, "editable");
|
||||
|
||||
// TODO "highlight" the updated element using animation of color (yellow fade-out)
|
||||
|
||||
// Update the row's final grade values
|
||||
gr.updateRow(gr.getIdData(editedEl.id).userId, queryResult.row);
|
||||
} else {
|
||||
|
||||
}
|
||||
|
||||
// Display message
|
||||
gr.displayMessage(queryResult.result, queryResult.message, editedEl);
|
||||
}
|
||||
|
||||
var handleFailure = function(o) {
|
||||
YAHOO.log("saveField: Failure to call the ajax callbacks page!", "error");
|
||||
}
|
||||
|
||||
var uri = gr.wwwroot + '/grade/report/grader/ajax_callbacks.php';
|
||||
var callback = {success: handleSuccess, failure: handleFailure};
|
||||
var conn = YAHOO.util.Connect.asyncRequest("post", uri, callback, postData);
|
||||
}; // End of saveField function
|
||||
|
||||
/**
|
||||
* Displays a message in the message bar above the report, Google-style
|
||||
* @param string result "success", "notice" or "error"
|
||||
* @param string message
|
||||
* @param object An element to highlight
|
||||
*/
|
||||
YAHOO.grader_report.displayMessage = function(result, message, elToHighlight) {
|
||||
var messageDiv = document.getElementById('grader_report_message');
|
||||
// Remove previous message
|
||||
// TODO log messages in DB?
|
||||
messageDiv.innerHTML = '';
|
||||
|
||||
if (message.length < 1 || !message) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove all state classes first
|
||||
YAHOO.util.Dom.removeClass(messageDiv, 'success');
|
||||
YAHOO.util.Dom.removeClass(messageDiv, 'error');
|
||||
YAHOO.util.Dom.removeClass(messageDiv, 'notice');
|
||||
|
||||
var attributes = {backgroundColor: { to: '#00F0F0'} };
|
||||
|
||||
// Add result class
|
||||
YAHOO.util.Dom.addClass(messageDiv, result);
|
||||
|
||||
messageDiv.innerHTML = message;
|
||||
|
||||
// Highlight given element
|
||||
if (result == 'error' && elToHighlight != null) {
|
||||
YAHOO.util.Dom.addClass(elToHighlight, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a userId and an array of string values, updates the innerHTML of each grade cell in the row
|
||||
* @param string userId Identifies the correct row
|
||||
* @param array An array of values
|
||||
*
|
||||
*/
|
||||
YAHOO.grader_report.updateRow = function(userId, row) {
|
||||
var gr = YAHOO.grader_report;
|
||||
|
||||
// Send update request and update the row
|
||||
YAHOO.log("updateRow: Updating row..." + row, "info");
|
||||
|
||||
for (var i in row) {
|
||||
if (row[i].finalgrade != null) {
|
||||
// Build id string
|
||||
var gradevalue = gr.roundValue(row[i].finalgrade, row[i].itemid);
|
||||
|
||||
if (row[i].scale) {
|
||||
var idString = "gradescale_";
|
||||
} else {
|
||||
var idString = "gradevalue_";
|
||||
}
|
||||
|
||||
idString += row[i].userid + "-i" + row[i].itemid;
|
||||
|
||||
var elementToUpdate = document.getElementById(idString);
|
||||
|
||||
if (undefined == elementToUpdate) {
|
||||
YAHOO.log("updateRow: Element with id " + idString + " does not exist!", "error");
|
||||
} else {
|
||||
if (row[i].scale) {
|
||||
elementToUpdate.selectedIndex = gradevalue;
|
||||
} else {
|
||||
elementToUpdate.innerHTML = gradevalue;
|
||||
}
|
||||
|
||||
// Add overridden class where it applies, and remove it where it does not
|
||||
// TODO fix the code below, it doesn't currently work. See ajax_callbacks.php for the code building the JSON data
|
||||
if (row[i].overridden > 0) {
|
||||
YAHOO.util.Dom.addClass(elementToUpdate.parentNode, "overridden");
|
||||
} else if (YAHOO.util.Dom.hasClass(elementToUpdate.parentNode, "overridden")) {
|
||||
YAHOO.util.Dom.removeClass(elementToUpdate.parentNode, "overridden");
|
||||
}
|
||||
|
||||
YAHOO.log("updateRow: Updated finalgrade (" + gradevalue + ") of user " + row[i].userid + " for grade item " + row[i].itemid, "info");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a gradevalue or gradefeedback <a> element,
|
||||
* @param object element A DOM element
|
||||
*/
|
||||
YAHOO.grader_report.openField = function(element) {
|
||||
YAHOO.log("openField: Moving to next item: " + element.id, "info");
|
||||
var gr = YAHOO.grader_report;
|
||||
var idData = gr.getIdData(element.id);
|
||||
element.inputId = element.id + "_input";
|
||||
|
||||
// If field is in error, empty it before editing
|
||||
if (YAHOO.util.Dom.hasClass(element, 'error')) {
|
||||
if (idData.type == 'feedback') {
|
||||
element.innerHTML = '';
|
||||
} else if (idData.type == 'value') {
|
||||
element.value = '';
|
||||
}
|
||||
|
||||
YAHOO.util.Dom.removeClass(element, 'error');
|
||||
}
|
||||
|
||||
// Show a textarea for feedback, input for grade and leave scale as it is
|
||||
if (undefined == idData.type) {
|
||||
YAHOO.log("openField: Could not get info from elementId: " + element.id, "warn");
|
||||
} else if (idData.type == 'feedback') {
|
||||
var original = gr.feedbacks[idData.userId][idData.itemId].toString();
|
||||
var tabIndex = element.tabIndex.toString();
|
||||
var displayValue = null;
|
||||
|
||||
// If empty feedback, show empty textarea
|
||||
if (original == gr.straddfeedback) {
|
||||
displayValue = '';
|
||||
} else {
|
||||
displayValue = original;
|
||||
}
|
||||
|
||||
element.innerHTML = '<textarea id="' + element.inputId + '" name="' + idData.type + '">' + displayValue + '</textarea>';
|
||||
setTimeout(function() {element.firstChild.focus(); }, 0);
|
||||
} else if (idData.type == 'value') {
|
||||
var original = element.innerHTML.toString(); // Removes reference to original
|
||||
element.innerHTML = '<input onfocus="this.select()" id="' + element.inputId + '" type="text" name="' +
|
||||
idData.type + '" value="' + gr.roundValue(original, idData.itemId) + '" />';
|
||||
setTimeout(function() {element.firstChild.focus(); }, 0);
|
||||
} else if (idData.type == 'scale') {
|
||||
var original = element.options[element.selectedIndex].value;
|
||||
setTimeout(function() {element.focus(); }, 0);
|
||||
}
|
||||
|
||||
YAHOO.util.Dom.removeClass(element, "editable");
|
||||
|
||||
// Save the element and its original value
|
||||
gr.el_being_edited = {elementId: element.id, value: original, tabIndex: tabIndex};
|
||||
YAHOO.log("openField: el_being_edited saved as: " + gr.el_being_edited.value, "info");
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the input, textarea or select inside a gradecell with the value currently held in that
|
||||
* input, textarea or select. If the second argument (cancel) is true, replaces the innerHTML with
|
||||
* the value held in YAHOO.grader_report.el_being_edited
|
||||
*
|
||||
* @param object element DOM element being closed
|
||||
* @param boolean forcecancel If true, current value held in input element is dropped in favour of original value in memory
|
||||
*/
|
||||
YAHOO.grader_report.closeField = function(element, forcecancel) {
|
||||
YAHOO.log("closeField: Closing field: " + element.id, "info");
|
||||
|
||||
var gr = YAHOO.grader_report;
|
||||
var idData = gr.getIdData(element.id);
|
||||
|
||||
if (idData.type == 'feedback') {
|
||||
var newValue = element.firstChild.value;
|
||||
var originalValue = gr.feedbacks[idData.userId][idData.itemId];
|
||||
|
||||
if (!forcecancel && originalValue == newValue) {
|
||||
YAHOO.log("closeField: originalValue == newvalue, forcing a cancel", "info");
|
||||
forcecancel = true;
|
||||
originalValue = gr.truncateText(originalValue);
|
||||
}
|
||||
} else if (idData.type == 'value') {
|
||||
var originalValue = gr.el_being_edited.value;
|
||||
var newValue = element.firstChild.value;
|
||||
}
|
||||
|
||||
YAHOO.util.Dom.addClass(element, "editable");
|
||||
|
||||
// No need to change HTML for select element
|
||||
if (idData.type == 'scale') {
|
||||
gr.el_being_edited = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (forcecancel || ((newValue == '' && idData.type == 'feedback') && idData.type != 'scale')) {
|
||||
// Replace the input by the original value as plain text
|
||||
element.innerHTML = gr.truncateText(originalValue);
|
||||
YAHOO.log("closeField: Cancelling : Replacing field by original value: " + originalValue, "info");
|
||||
} else {
|
||||
// For numeric grades, round off to item decimal points
|
||||
if (idData.type == 'value') {
|
||||
element.innerHTML = gr.roundValue(newValue, idData.itemId);
|
||||
} else if (idData.type == 'feedback') {
|
||||
element.innerHTML = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Erase the element in memory
|
||||
gr.el_being_edited = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a string, truncates it if over the limit defined in the report's preferences, adding an ellipsis at the end.
|
||||
* TODO improve the regex so that it doesn't truncate halfway through a word
|
||||
* @param string text
|
||||
* @return string
|
||||
*/
|
||||
YAHOO.grader_report.truncateText = function(text) {
|
||||
var gr = YAHOO.grader_report;
|
||||
var returnString = '';
|
||||
|
||||
returnString = text.substring(0, gr.feedback_trunc_length);
|
||||
|
||||
// Add ... if the string was truncated
|
||||
if (returnString.length < text.length) {
|
||||
returnString += '...';
|
||||
}
|
||||
|
||||
return returnString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a float value and an itemId, uses that grade_item's decimalpoints preference to round off the value and return it.
|
||||
* @param float value
|
||||
* @param int itemId
|
||||
* @return string
|
||||
*/
|
||||
YAHOO.grader_report.roundValue = function(value, itemId) {
|
||||
return value;
|
||||
|
||||
// I am ignoring the rest for now, because I am not sure we should round values while in editing mode
|
||||
if (value.length == 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var gr = YAHOO.grader_report;
|
||||
var decimalpoints = gr.decimalpoints[itemId];
|
||||
var gradevalue = Math.round(Number(value) * Math.pow(10, decimalpoints)) / Math.pow(10, decimalpoints);
|
||||
|
||||
// If the value is an integer, add appropriate zeros after the decimal point
|
||||
if (gradevalue % 1 == 0 && decimalpoints > 0) {
|
||||
gradevalue += '.';
|
||||
for (var i = 0; i < decimalpoints; i++) {
|
||||
gradevalue += '0';
|
||||
}
|
||||
}
|
||||
|
||||
return gradevalue;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given an element of origin, returns the field to be edited in the given direction.
|
||||
* @param object origin_element
|
||||
* @param string direction previous|next|left|right|up|down
|
||||
* @return object element
|
||||
*/
|
||||
YAHOO.grader_report.getField = function(origin_element, direction) {
|
||||
var gr = YAHOO.grader_report;
|
||||
// get all 'editable' elements
|
||||
var haystack = YAHOO.util.Dom.getElementsByClassName('editable');
|
||||
var wrapElement = null;
|
||||
|
||||
var feedbackModifier = 1;
|
||||
var upDownOffset = 1;
|
||||
var idData = gr.getIdData(origin_element.id);
|
||||
|
||||
if (gr.showquickfeedback) {
|
||||
feedbackModifier = 2;
|
||||
|
||||
if (idData.type == 'value' || idData.type == 'scale') {
|
||||
upDownOffset = gr.studentsperpage;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (direction == 'next') {
|
||||
var wrapValue = 1;
|
||||
var needle = origin_element.tabIndex + 1;
|
||||
|
||||
} else if (direction == 'previous') {
|
||||
var wrapValue = haystack.length;
|
||||
var needle = origin_element.tabIndex - 1;
|
||||
|
||||
} else if (direction == 'right') {
|
||||
var needle = origin_element.tabIndex + gr.studentsperpage * feedbackModifier;
|
||||
var wrapValue = null; // TODO implement wrapping when moving right
|
||||
|
||||
} else if (direction == 'left') {
|
||||
var needle = origin_element.tabIndex - gr.studentsperpage * feedbackModifier;
|
||||
var wrapValue = null; // TODO implement wrapping when moving left
|
||||
|
||||
} else if (direction == 'up') {
|
||||
|
||||
// Jump up from value to feedback: origin + (studentsperpage - 1)
|
||||
if (idData.type == 'value' || idData.type == 'scale') {
|
||||
var upDownOffset = gr.studentsperpage - 1;
|
||||
|
||||
// Jump up from feedback to value: origin - studentsperpage
|
||||
} else if (idData.type == 'feedback') {
|
||||
var upDownOffset = -(gr.studentsperpage);
|
||||
}
|
||||
|
||||
var needle = origin_element.tabIndex + upDownOffset;
|
||||
var wrapValue = haystack.length;
|
||||
|
||||
} else if (direction == 'down') {
|
||||
// Jump down from value to feedback: origin + studentsperpage
|
||||
if (idData.type == 'value' || idData.type == 'scale') {
|
||||
var upDownOffset = gr.studentsperpage;
|
||||
|
||||
// Jump down from feedback to value: origin - (studentsperpage - 1)
|
||||
} else if (idData.type == 'feedback') {
|
||||
var upDownOffset = -(gr.studentsperpage - 1);
|
||||
}
|
||||
var needle = origin_element.tabIndex + upDownOffset;
|
||||
var wrapValue = 1;
|
||||
}
|
||||
|
||||
for (var i = 0; i < haystack.length; i++) {
|
||||
if (haystack[i].tabIndex == wrapValue) {
|
||||
wrapElement = haystack[i];
|
||||
}
|
||||
|
||||
if (haystack[i].tabIndex == needle) {
|
||||
return haystack[i];
|
||||
}
|
||||
}
|
||||
|
||||
// If we haven't returned yet, it means we have reached the end of tabindices: return the wrap element
|
||||
if (wrapElement != null) {
|
||||
return wrapElement;
|
||||
} else { // If no wrap element, just return the element of origin: we are stuck!
|
||||
return origin_element;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
YAHOO.grader_report.init = function() {
|
||||
var gr = YAHOO.grader_report;
|
||||
|
||||
// Handle Key presses: Tab and Enter
|
||||
this.keyHandler = function(e) {
|
||||
var charCode = YAHOO.util.Event.getCharCode(e);
|
||||
|
||||
YAHOO.log("init: Key pressed (" + charCode + "). el_being_edited = " + gr.el_being_edited, "info");
|
||||
// Handle keys if editing
|
||||
if (gr.el_being_edited !== null) {
|
||||
var editedEl = document.getElementById(gr.el_being_edited.elementId);
|
||||
var idData = gr.getIdData(editedEl.id);
|
||||
|
||||
// Handle Tab and Shift-Tab for navigation forward/backward
|
||||
if (charCode == 9) {
|
||||
gr.saveField(editedEl);
|
||||
gr.closeField(editedEl, false);
|
||||
|
||||
if (e.shiftKey) {
|
||||
var fieldToOpen = gr.getField(editedEl, 'previous');
|
||||
} else {
|
||||
var fieldToOpen = gr.getField(editedEl, 'next');
|
||||
}
|
||||
|
||||
gr.openField(fieldToOpen);
|
||||
}
|
||||
|
||||
// Handle Enter key press
|
||||
if (charCode == 13 && idData.type != 'feedback') { // textareas need [enter]
|
||||
// Locate element being edited
|
||||
var editedEl = document.getElementById(gr.el_being_edited.elementId);
|
||||
gr.saveField(editedEl);
|
||||
gr.closeField(editedEl, false);
|
||||
}
|
||||
|
||||
// Handle ctrl-arrows
|
||||
var arrows = { 37: "left", 38: "up", 39: "right", 40: "down" };
|
||||
|
||||
if (e.ctrlKey && (charCode == 37 || charCode == 38 || charCode == 39 || charCode == 40)) {
|
||||
gr.saveField(editedEl);
|
||||
gr.closeField(editedEl, false);
|
||||
|
||||
var fieldToOpen = gr.getField(editedEl, arrows[charCode]);
|
||||
gr.openField(fieldToOpen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle mouse clicks
|
||||
this.clickHandler = function(e) {
|
||||
var clickTargetElement = YAHOO.util.Event.getTarget(e);
|
||||
|
||||
|
||||
// Handle a click while a grade value or feedback is being edited
|
||||
if (gr.el_being_edited !== null) {
|
||||
// idData represents the element being edited, not the clicked element
|
||||
var idData = gr.getIdData(gr.el_being_edited.elementId);
|
||||
|
||||
if (idData.type != 'scale') {
|
||||
// If clicking in a cell being edited, no action
|
||||
// parentNode is the original a to which the element was added as a Child node
|
||||
if (gr.el_being_edited.elementId == clickTargetElement.parentNode.id) {
|
||||
YAHOO.log("init: Clicked within the edited element, no action.", "info");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise, we CANCEL the current edit
|
||||
var originalTarget = document.getElementById(gr.el_being_edited.elementId);
|
||||
YAHOO.log("init: Clicked out of the edited element, cancelling edit.", "info");
|
||||
gr.closeField(originalTarget, true);
|
||||
|
||||
} else if (idData.type == 'scale' && gr.el_being_edited.elementId == clickTargetElement.id) {
|
||||
|
||||
// An option has been selected, update the element
|
||||
gr.saveField(clickTargetElement);
|
||||
// Then open the element to save the new value in el_being_edited
|
||||
gr.openField(clickTargetElement);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
while (clickTargetElement.id != 'grade-report-grader-index') {
|
||||
anchor_re = /(gradevalue|gradefeedback|gradescale)(.*) editable/;
|
||||
anchor_matches = anchor_re.exec(clickTargetElement.className);
|
||||
|
||||
// YAHOO.log("className = " + clickTargetElement.className, "info");
|
||||
var nodeName = clickTargetElement.nodeName.toLowerCase();
|
||||
|
||||
if ((nodeName == 'a' || nodeName == 'select') && anchor_matches) {
|
||||
gr.openField(clickTargetElement);
|
||||
break;
|
||||
|
||||
// If clicked anywhere else in the cell, default to editing the grade, not the feedback
|
||||
} else if (clickTargetElement.nodeName.toLowerCase() == "td" && clickTargetElement.id.match(/gradecell/)) {
|
||||
anchors = YAHOO.util.Dom.getElementsByClassName("editable", "a", clickTargetElement);
|
||||
if (anchors.length > 0) {
|
||||
clickTargetElement = anchors[0];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
clickTargetElement = clickTargetElement.parentNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
YAHOO.util.Event.on("grade-report-grader-index", "click", this.clickHandler);
|
||||
YAHOO.util.Event.on(document, "keydown", this.keyHandler, this, true);
|
||||
};
|
||||
|
||||
YAHOO.util.Event.onDOMReady(YAHOO.grader_report.init);
|
||||
|
||||
// ]]>
|
||||
</script>
|
139
grade/report/grader/ajax_callbacks.php
Normal file
139
grade/report/grader/ajax_callbacks.php
Normal file
@ -0,0 +1,139 @@
|
||||
<?php // $Id$
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// NOTICE OF COPYRIGHT //
|
||||
// //
|
||||
// Moodle - Modular Object-Oriented Dynamic Learning Environment //
|
||||
// http://moodle.org //
|
||||
// //
|
||||
// Copyright (C) 1999 onwards Martin Dougiamas http://moodle.com //
|
||||
// //
|
||||
// This program is free software; you can redistribute it and/or modify //
|
||||
// it under the terms of the GNU General Public License as published by //
|
||||
// the Free Software Foundation; either version 2 of the License, or //
|
||||
// (at your option) any later version. //
|
||||
// //
|
||||
// This program is distributed in the hope that it will be useful, //
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
||||
// GNU General Public License for more details: //
|
||||
// //
|
||||
// http://www.gnu.org/copyleft/gpl.html //
|
||||
// //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
require_once '../../../config.php';
|
||||
require_once $CFG->libdir.'/gradelib.php';
|
||||
require_once $CFG->dirroot.'/grade/lib.php';
|
||||
// require_once $CFG->dirroot.'/grade/report/grader/ajaxlib.php';
|
||||
// require_once $CFG->dirroot.'/grade/report/grader/lib.php';
|
||||
|
||||
$courseid = required_param('id'); // course id
|
||||
$userid = optional_param('userid', false, PARAM_INT);
|
||||
$itemid = optional_param('itemid', false, PARAM_INT);
|
||||
$type = optional_param('type', false, PARAM_ALPHA);
|
||||
$action = optional_param('action', false, PARAM_ALPHA);
|
||||
$newvalue = optional_param('newvalue', false, PARAM_MULTILANG);
|
||||
|
||||
switch ($action) {
|
||||
case 'update':
|
||||
if (!empty($userid) && !empty($itemid) && $newvalue !== false && !empty($type)) {
|
||||
// Save the grade or feedback
|
||||
if (!$grade_item = grade_item::fetch(array('id'=>$itemid, 'courseid'=>$courseid))) { // we must verify course id here!
|
||||
print_error('Incorrect grade item id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Code copied from grade/report/grader/lib.php line 187+
|
||||
*/
|
||||
$warnings = array();
|
||||
$finalvalue = null;
|
||||
$finalgrade = null;
|
||||
$feedback = null;
|
||||
$json_object = new stdClass();
|
||||
// Pre-process grade
|
||||
if ($type == 'value' || $type == 'scale') {
|
||||
$feedback = false;
|
||||
$feedbackformat = false;
|
||||
if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
|
||||
if ($newvalue == -1) { // -1 means no grade
|
||||
$finalgrade = null;
|
||||
} else {
|
||||
$finalgrade = $newvalue;
|
||||
}
|
||||
} else {
|
||||
$finalgrade = unformat_float($newvalue);
|
||||
}
|
||||
|
||||
$errorstr = '';
|
||||
// Warn if the grade is out of bounds.
|
||||
if (is_null($finalgrade)) {
|
||||
// ok
|
||||
} else if ($finalgrade < $grade_item->grademin) {
|
||||
$errorstr = 'lessthanmin';
|
||||
} else if ($finalgrade > $grade_item->grademax) {
|
||||
$errorstr = 'morethanmax';
|
||||
}
|
||||
|
||||
if ($errorstr) {
|
||||
$user = get_record('user', 'id', $userid, '', '', '', '', 'id, firstname, lastname');
|
||||
$gradestr = new object();
|
||||
$gradestr->username = fullname($user);
|
||||
$gradestr->itemname = $grade_item->get_name();
|
||||
$json_object->message = get_string($errorstr, 'grades', $gradestr);
|
||||
$json_object->result = "error";
|
||||
|
||||
}
|
||||
|
||||
$finalvalue = $finalgrade;
|
||||
|
||||
} else if ($type == 'feedback') {
|
||||
$finalgrade = false;
|
||||
$trimmed = trim($newvalue);
|
||||
if (empty($trimmed)) {
|
||||
$feedback = NULL;
|
||||
} else {
|
||||
$feedback = stripslashes($newvalue);
|
||||
}
|
||||
|
||||
$finalvalue = $feedback;
|
||||
}
|
||||
|
||||
if (!empty($json_object->result) && $json_object->result == 'error') {
|
||||
echo json_encode($json_object);
|
||||
die();
|
||||
} else {
|
||||
$json_object->gradevalue = $finalvalue;
|
||||
|
||||
if ($grade_item->update_final_grade($userid, $finalgrade, 'gradebook', $feedback, FORMAT_MOODLE)) {
|
||||
$json_object->result = 'success';
|
||||
$json_object->message = false;
|
||||
} else {
|
||||
$json_object->result = 'error';
|
||||
$json_object->message = "TO BE LOCALISED: Failure to update final grade!";
|
||||
echo json_encode();
|
||||
die();
|
||||
}
|
||||
|
||||
// Get row data
|
||||
$sql = "SELECT gg.id, gi.id AS itemid, gi.scaleid AS scale, gg.userid AS userid, finalgrade, gg.overridden AS overridden "
|
||||
. "FROM {$CFG->prefix}grade_grades gg, {$CFG->prefix}grade_items gi WHERE "
|
||||
. "gi.courseid = $courseid AND gg.itemid = gi.id AND gg.userid = $userid";
|
||||
$records = get_records_sql($sql);
|
||||
$json_object->row = $records;
|
||||
echo json_encode($json_object);
|
||||
die();
|
||||
}
|
||||
} else {
|
||||
$json_object = new stdClass();
|
||||
$json_object->result = "error";
|
||||
$json_object->message = "Missing parameter to ajax UPDATE callback: \n" .
|
||||
" userid: $userid,\n itemid: $itemid\n, type: $type\n, newvalue: $newvalue";
|
||||
echo json_encode($json_object);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
?>
|
527
grade/report/grader/ajaxlib.php
Normal file
527
grade/report/grader/ajaxlib.php
Normal file
@ -0,0 +1,527 @@
|
||||
<?php // $Id$
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// //
|
||||
// NOTICE OF COPYRIGHT //
|
||||
// //
|
||||
// Moodle - Modular Object-Oriented Dynamic Learning Environment //
|
||||
// http://moodle.com //
|
||||
// //
|
||||
// Copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com //
|
||||
// //
|
||||
// This program is free software; you can redistribute it and/or modify //
|
||||
// it under the terms of the GNU General Public License as published by //
|
||||
// the Free Software Foundation; either version 2 of the License, or //
|
||||
// (at your option) any later version. //
|
||||
// //
|
||||
// This program is distributed in the hope that it will be useful, //
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
||||
// GNU General Public License for more details: //
|
||||
// //
|
||||
// http://www.gnu.org/copyleft/gpl.html //
|
||||
// //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
/**
|
||||
* File in which the grade_report_grader class is defined.
|
||||
* @package gradebook
|
||||
*/
|
||||
|
||||
require_once($CFG->dirroot . '/grade/report/grader/lib.php');
|
||||
|
||||
/**
|
||||
* Class providing an API for the grader report building and displaying.
|
||||
* @uses grade_report
|
||||
* @package gradebook
|
||||
*/
|
||||
class grade_report_grader_ajax extends grade_report_grader {
|
||||
|
||||
/**
|
||||
* An array of feedbacks, indexed by userid_itemid, used for JS caching
|
||||
* @var array $feedbacks
|
||||
*/
|
||||
var $feedbacks = array();
|
||||
|
||||
/**
|
||||
* Length at which feedback will be truncated (to the nearest word) and an ellipsis be added.
|
||||
* TODO replace this by a report preference
|
||||
* @var int $feedback_trunc_length
|
||||
*/
|
||||
var $feedback_trunc_length = 50;
|
||||
|
||||
/**
|
||||
* Self-incrementing variable, tracking the tabindex. Depending on the tabindex option ("all values, then feedbacks" is default)
|
||||
* Increments by one between each user for the gradevalues, and by 1 + usercount for the gradefeedback
|
||||
* @var int $tabindex
|
||||
*/
|
||||
var $tabindex = 0;
|
||||
|
||||
/**
|
||||
* Constructor. Sets local copies of user preferences and initialises grade_tree.
|
||||
* @param int $courseid
|
||||
* @param object $gpr grade plugin return tracking object
|
||||
* @param string $context
|
||||
* @param int $page The current page being viewed (when report is paged)
|
||||
* @param int $sortitemid The id of the grade_item by which to sort the table
|
||||
*/
|
||||
function grade_report_grader_ajax($courseid, $gpr, $context, $page=null, $sortitemid=null) {
|
||||
parent::grade_report_grader($courseid, $gpr, $context, $page, $sortitemid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads, stores and returns the array of scales used in this course.
|
||||
* @return array
|
||||
*/
|
||||
function get_scales_array() {
|
||||
if (empty($this->gtree->items)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!empty($this->scales_array)) {
|
||||
return $this->scales_array;
|
||||
}
|
||||
|
||||
$scales_list = '';
|
||||
$scales_array = array();
|
||||
|
||||
foreach ($this->gtree->items as $item) {
|
||||
if (!empty($item->scaleid)) {
|
||||
$scales_list .= "$item->scaleid,";
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($scales_list)) {
|
||||
$scales_list = substr($scales_list, 0, -1);
|
||||
$scales_array = get_records_list('scale', 'id', $scales_list);
|
||||
$this->scales_array = $scales_array;
|
||||
return $scales_array;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the data sent by the form (grades and feedbacks).
|
||||
* Caller is responsible for all access control checks
|
||||
* @param array $data form submission (with magic quotes)
|
||||
* @return array empty array if success, array of warnings if something fails.
|
||||
*/
|
||||
function process_data($data) {
|
||||
return parent::process_data($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns a div with on/off toggles.
|
||||
* @return string HTML code
|
||||
*/
|
||||
function get_toggles_html() {
|
||||
return parent::get_toggles_html();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut function for printing the grader report toggles.
|
||||
* @param string $type The type of toggle
|
||||
* @param bool $return Whether to return the HTML string rather than printing it
|
||||
* @return void
|
||||
*/
|
||||
function print_toggle($type, $return=false) {
|
||||
return parent::print_toggle($type, $return);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns the HTML code for the headers.
|
||||
* @return string $headerhtml
|
||||
*/
|
||||
function get_headerhtml() {
|
||||
return parent::get_headerhtml();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and return the HTML rows of the table (grades headed by student).
|
||||
* @return string HTML
|
||||
*/
|
||||
function get_studentshtml() {
|
||||
if (empty($this->users)) {
|
||||
print_error('nousersloaded', 'grades');
|
||||
}
|
||||
|
||||
$this->numusers = count($this->users);
|
||||
|
||||
$studentshtml = '';
|
||||
|
||||
foreach ($this->users as $userid => $user) {
|
||||
$this->tabindex++;
|
||||
$studentshtml .= $this->get_studentrowhtml($user);
|
||||
}
|
||||
|
||||
return $studentshtml;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a userid, and provided the gtree is correctly loaded, returns a complete HTML row for this user.
|
||||
*
|
||||
* @param object $user
|
||||
* @return string
|
||||
*/
|
||||
function get_studentrowhtml($user) {
|
||||
global $CFG;
|
||||
$showuserimage = $this->get_pref('showuserimage');
|
||||
$showuseridnumber = $this->get_pref('showuseridnumber');
|
||||
$studentrowhtml = '';
|
||||
$row_classes = array(' even ', ' odd ');
|
||||
|
||||
if ($this->canviewhidden) {
|
||||
$altered = array();
|
||||
$unknown = array();
|
||||
} else {
|
||||
$hiding_affected = grade_grade::get_hiding_affected($this->grades[$userid], $this->gtree->items);
|
||||
$altered = $hiding_affected['altered'];
|
||||
$unknown = $hiding_affected['unknown'];
|
||||
unset($hiding_affected);
|
||||
}
|
||||
|
||||
$columncount = 0;
|
||||
// Student name and link
|
||||
$user_pic = null;
|
||||
if ($showuserimage) {
|
||||
$user_pic = '<div class="userpic">' . print_user_picture($user, $this->courseid, true, 0, true) . '</div>';
|
||||
}
|
||||
|
||||
$studentrowhtml .= '<tr class="r'.$this->rowcount++ . $row_classes[$this->rowcount % 2] . '">'
|
||||
.'<th class="header c'.$columncount++.' user" scope="row" onclick="set_row(this.parentNode.rowIndex);">'.$user_pic
|
||||
.'<a href="'.$CFG->wwwroot.'/user/view.php?id='.$user->id.'&course='.$this->course->id.'">'
|
||||
.fullname($user).'</a></th>';
|
||||
|
||||
if ($showuseridnumber) {
|
||||
$studentrowhtml .= '<th class="header c'.$columncount++.' useridnumber" onclick="set_row(this.parentNode.rowIndex);">'. $user->idnumber.'</th>';
|
||||
}
|
||||
|
||||
$columntabcount = 0;
|
||||
$feedback_tabindex_modifier = 1; // Used to offset the grade value at the beginning of each new column
|
||||
|
||||
if ($this->get_pref('showquickfeedback')) {
|
||||
$feedback_tabindex_modifier = 2;
|
||||
}
|
||||
|
||||
foreach ($this->gtree->items as $itemid=>$unused) {
|
||||
|
||||
$nexttabindex = $this->tabindex + $columntabcount * $feedback_tabindex_modifier * $this->numusers;
|
||||
$studentrowhtml .= $this->get_gradecellhtml($user, $itemid, $columncount, $nexttabindex, $altered, $unknown);
|
||||
$columntabcount++;
|
||||
}
|
||||
|
||||
$studentrowhtml .= '</tr>';
|
||||
return $studentrowhtml;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Retuns the HTML table cell for a user's grade for a grade_item
|
||||
*
|
||||
* @param object $user
|
||||
* @param int $itemid
|
||||
* @param int $columncount
|
||||
* @param int $nexttabindex
|
||||
* @param array $altered
|
||||
* @param array $unknown
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function get_gradecellhtml($user, $itemid, $columncount, $nexttabindex, $altered=array(), $unknown=array()) {
|
||||
global $CFG, $USER;
|
||||
|
||||
$strfeedback = $this->get_lang_string("feedback");
|
||||
$strgrade = $this->get_lang_string('grade');
|
||||
|
||||
// Preload scale objects for items with a scaleid
|
||||
$scales_array = $this->get_scales_array();
|
||||
|
||||
$userid = $user->id;
|
||||
$item =& $this->gtree->items[$itemid];
|
||||
$grade = $this->grades[$userid][$item->id];
|
||||
|
||||
// Get the decimal points preference for this item
|
||||
$decimalpoints = $item->get_decimals();
|
||||
|
||||
if (in_array($itemid, $unknown)) {
|
||||
$gradeval = null;
|
||||
} else if (array_key_exists($itemid, $altered)) {
|
||||
$gradeval = $altered[$itemid];
|
||||
} else {
|
||||
$gradeval = $grade->finalgrade;
|
||||
}
|
||||
|
||||
$gradecellhtml = '';
|
||||
|
||||
// MDL-11274
|
||||
// Hide grades in the grader report if the current grader doesn't have 'moodle/grade:viewhidden'
|
||||
if (!$this->canviewhidden and $grade->is_hidden()) {
|
||||
if (!empty($CFG->grade_hiddenasdate) and $grade->get_datesubmitted() and !$item->is_category_item() and !$item->is_course_item()) {
|
||||
// the problem here is that we do not have the time when grade value was modified, 'timemodified' is general modification date for grade_grades records
|
||||
$gradecellhtml .= '<td class="cell c'.$columncount++.'"><span class="datesubmitted">'.userdate($grade->get_datesubmitted(),get_string('strftimedatetimeshort')).'</span></td>';
|
||||
} else {
|
||||
$gradecellhtml .= '<td class="cell c'.$columncount++.'">-</td>';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// emulate grade element
|
||||
$eid = $this->gtree->get_grade_eid($grade);
|
||||
$element = array('eid'=>$eid, 'object'=>$grade, 'type'=>'grade');
|
||||
|
||||
$cellclasses = 'cell c'.$columncount++;
|
||||
if ($item->is_category_item()) {
|
||||
$cellclasses .= ' cat';
|
||||
}
|
||||
if ($item->is_course_item()) {
|
||||
$cellclasses .= ' course';
|
||||
}
|
||||
if ($grade->is_overridden()) {
|
||||
$cellclasses .= ' overridden';
|
||||
}
|
||||
|
||||
if ($grade->is_excluded()) {
|
||||
$cellclasses .= ' excluded';
|
||||
}
|
||||
|
||||
$gradecellhtml .= "<td id=\"gradecell_u$userid-i$itemid\" class=\"$cellclasses\">";
|
||||
|
||||
if ($grade->is_excluded()) {
|
||||
$gradecellhtml .= get_string('excluded', 'grades') . ' ';
|
||||
}
|
||||
|
||||
// Do not show any icons if no grade (no record in DB to match)
|
||||
if (!$item->needsupdate and $USER->gradeediting[$this->courseid]) {
|
||||
$gradecellhtml .= $this->get_icons($element);
|
||||
}
|
||||
|
||||
$hidden = '';
|
||||
if ($grade->is_hidden()) {
|
||||
$hidden = ' hidden ';
|
||||
}
|
||||
|
||||
$gradepass = ' gradefail ';
|
||||
if ($grade->is_passed($item)) {
|
||||
$gradepass = ' gradepass ';
|
||||
} elseif (is_null($grade->is_passed($item))) {
|
||||
$gradepass = '';
|
||||
}
|
||||
|
||||
// if in editting mode, we need to print either a text box
|
||||
// or a drop down (for scales)
|
||||
// grades in item of type grade category or course are not directly editable
|
||||
if ($item->needsupdate) {
|
||||
$gradecellhtml .= '<span class="gradingerror'.$hidden.'">'.get_string('error').'</span>';
|
||||
|
||||
} else if ($USER->gradeediting[$this->courseid]) {
|
||||
$anchor_id = "gradevalue_$userid-i$itemid";
|
||||
|
||||
if ($item->scaleid && !empty($scales_array[$item->scaleid])) {
|
||||
$scale = $scales_array[$item->scaleid];
|
||||
$gradeval = (int)$gradeval; // scales use only integers
|
||||
$scales = explode(",", $scale->scale);
|
||||
// reindex because scale is off 1
|
||||
|
||||
// MDL-12104 some previous scales might have taken up part of the array
|
||||
// so this needs to be reset
|
||||
$scaleopt = array();
|
||||
$i = 0;
|
||||
foreach ($scales as $scaleoption) {
|
||||
$i++;
|
||||
$scaleopt[$i] = $scaleoption;
|
||||
}
|
||||
|
||||
if ($this->get_pref('quickgrading') and $grade->is_editable()) {
|
||||
$oldval = empty($gradeval) ? -1 : $gradeval;
|
||||
if (empty($item->outcomeid)) {
|
||||
$nogradestr = $this->get_lang_string('nograde');
|
||||
} else {
|
||||
$nogradestr = $this->get_lang_string('nooutcome', 'grades');
|
||||
}
|
||||
|
||||
$gradecellhtml .= '<select name="grade_'.$userid.'_'.$item->id.'" class="gradescale editable" '
|
||||
. 'id="gradescale_'.$userid.'-i'.$item->id.'" tabindex="'.$nexttabindex.'">' . "\n";
|
||||
$gradecellhtml .= '<option value="-1">' . $nogradestr . "</option>\n";
|
||||
|
||||
foreach ($scaleopt as $val => $label) {
|
||||
$selected = '';
|
||||
|
||||
if ($val == $oldval) {
|
||||
$selected = 'selected="selected"';
|
||||
}
|
||||
|
||||
$gradecellhtml .= "<option value=\"$val\" $selected>$label</option>\n";
|
||||
}
|
||||
|
||||
$gradecellhtml .= "</select>\n";
|
||||
|
||||
} elseif(!empty($scale)) {
|
||||
$scales = explode(",", $scale->scale);
|
||||
|
||||
// invalid grade if gradeval < 1
|
||||
if ($gradeval < 1) {
|
||||
$gradecellhtml .= '<a tabindex="'.$nexttabindex .'" id="' . $anchor_id
|
||||
. '" class="gradevalue'.$hidden.$gradepass.'">-</a>';
|
||||
} else {
|
||||
//just in case somebody changes scale
|
||||
$gradeval = (int)bounded_number($grade->grade_item->grademin, $gradeval, $grade->grade_item->grademax);
|
||||
$gradecellhtml .= '<a tabindex="'.$nexttabindex .'" id="' . $anchor_id
|
||||
. '" class="gradevalue'.$hidden.$gradepass.'">'.$scales[$gradeval-1].'</a>';
|
||||
}
|
||||
} else {
|
||||
// no such scale, throw error?
|
||||
}
|
||||
|
||||
} else if ($item->gradetype != GRADE_TYPE_TEXT) { // Value type
|
||||
$value = $gradeval;
|
||||
if ($this->get_pref('quickgrading') and $grade->is_editable()) {
|
||||
$gradecellhtml .= '<a tabindex="'.$nexttabindex .'" id="' . $anchor_id
|
||||
. '" class="gradevalue'.$hidden.$gradepass.' editable">' .$value.'</a>';
|
||||
} else {
|
||||
$gradecellhtml .= '<a tabindex="'.$nexttabindex .'" id="' . $anchor_id . '" class="gradevalue'
|
||||
.$hidden.$gradepass.'">'.$value.'</a>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If quickfeedback is on, print an input element
|
||||
if ($this->get_pref('showquickfeedback') and $grade->is_editable()) {
|
||||
if ($this->get_pref('quickgrading')) {
|
||||
$gradecellhtml .= '<br />';
|
||||
}
|
||||
$feedback = s($grade->feedback);
|
||||
$anchor_id = "gradefeedback_$userid-i$itemid";
|
||||
|
||||
if (empty($feedback)) {
|
||||
$feedback = get_string('addfeedback', 'grades');
|
||||
$gradecellhtml .= '<a ';
|
||||
} else {
|
||||
$overlib = '';
|
||||
$full_feedback = addslashes_js(trim(format_string($grade->feedback, $grade->feedbackformat)));
|
||||
$overlib = "return overlib('$full_feedback', BORDER, 0, FGCLASS, 'feedback', "
|
||||
."CAPTIONFONTCLASS, 'caption', CAPTION, '$strfeedback');";
|
||||
$gradecellhtml .= '<a onmouseover="'.s($overlib).'" onmouseout="return nd();" ';
|
||||
}
|
||||
|
||||
$feedback_tabindex = $nexttabindex + $this->numusers;
|
||||
|
||||
$short_feedback = shorten_text($feedback, $this->feedback_trunc_length);
|
||||
$gradecellhtml .= ' tabindex="'.$feedback_tabindex .'" id="'
|
||||
. $anchor_id . '" class="gradefeedback editable">' . $short_feedback . '</a>';
|
||||
$this->feedbacks[$userid][$item->id] = $feedback;
|
||||
}
|
||||
|
||||
} else { // Not editing
|
||||
$gradedisplaytype = $item->get_displaytype();
|
||||
|
||||
// If feedback present, surround grade with feedback tooltip: Open span here
|
||||
if (!empty($grade->feedback)) {
|
||||
$overlib = '';
|
||||
$feedback = addslashes_js(trim(format_string($grade->feedback, $grade->feedbackformat)));
|
||||
$overlib = "return overlib('$feedback', BORDER, 0, FGCLASS, 'feedback', "
|
||||
."CAPTIONFONTCLASS, 'caption', CAPTION, '$strfeedback');";
|
||||
$gradecellhtml .= '<span onmouseover="'.s($overlib).'" onmouseout="return nd();">';
|
||||
}
|
||||
|
||||
if ($item->needsupdate) {
|
||||
$gradecellhtml .= '<span class="gradingerror'.$hidden.$gradepass.'">'.get_string('error').'</span>';
|
||||
|
||||
} else {
|
||||
$gradecellhtml .= '<span class="gradevalue'.$hidden.$gradepass.'">'.grade_format_gradevalue($gradeval, $item, true, $gradedisplaytype, null).'</span>';
|
||||
}
|
||||
|
||||
// Close feedback span
|
||||
if (!empty($grade->feedback)) {
|
||||
$gradecellhtml .= '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($this->gradeserror[$item->id][$userid])) {
|
||||
$gradecellhtml .= $this->gradeserror[$item->id][$userid];
|
||||
}
|
||||
|
||||
$gradecellhtml .= '</td>' . "\n";
|
||||
return $gradecellhtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and return the HTML row of column totals.
|
||||
* @param bool $grouponly Whether to return only group averages or all averages.
|
||||
* @return string HTML
|
||||
*/
|
||||
function get_avghtml($grouponly=false) {
|
||||
return parent::get_avghtml();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and return the HTML row of ranges for each column (i.e. range).
|
||||
* @return string HTML
|
||||
*/
|
||||
function get_rangehtml() {
|
||||
return parent::get_rangehtml();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and return the HTML row of ranges for each column (i.e. range).
|
||||
* @return string HTML
|
||||
*/
|
||||
function get_iconshtml() {
|
||||
return parent::get_iconshtml();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a grade_category, grade_item or grade_grade, this function
|
||||
* figures out the state of the object and builds then returns a div
|
||||
* with the icons needed for the grader report.
|
||||
*
|
||||
* @param object $object
|
||||
* @return string HTML
|
||||
*/
|
||||
function get_icons($element) {
|
||||
return parent::get_icons($element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a category element returns collapsing +/- icon if available
|
||||
* @param object $object
|
||||
* @return string HTML
|
||||
*/
|
||||
function get_collapsing_icon($element) {
|
||||
return parent::get_collapsing_icon($element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a single action against a category, grade_item or grade.
|
||||
* @param string $target eid ({type}{id}, e.g. c4 for category4)
|
||||
* @param string $action Which action to take (edit, delete etc...)
|
||||
* @return
|
||||
*/
|
||||
function process_action($target, $action) {
|
||||
return parent::process_action($target, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a valid JSON object with feedbacks indexed by userid and itemid.
|
||||
* Paging is taken into account: this needs to be reloaded at each new page (not page load, just page of data);
|
||||
*/
|
||||
function getFeedbackJsArray() {
|
||||
if (!empty($this->feedbacks)) {
|
||||
return json_encode($this->feedbacks);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a json_encoded hash of itemid => decimalpoints preferences
|
||||
*/
|
||||
function getItemsDecimalPoints() {
|
||||
$decimals = array();
|
||||
foreach ($this->gtree->items as $itemid=>$item) {
|
||||
$decimals[$itemid] = $item->get_decimals();
|
||||
}
|
||||
return json_encode($decimals);
|
||||
}
|
||||
}
|
||||
?>
|
@ -113,14 +113,40 @@ if (!is_null($toggle) && !empty($toggle_type)) {
|
||||
//first make sure we have proper final grades - this must be done before constructing of the grade tree
|
||||
grade_regrade_final_grades($courseid);
|
||||
|
||||
// Perform actions
|
||||
// Perform actions.
|
||||
if (!empty($target) && !empty($action) && confirm_sesskey()) {
|
||||
grade_report_grader::process_action($target, $action);
|
||||
}
|
||||
|
||||
// Initialise the grader report object
|
||||
$bodytags = '';
|
||||
$report = new grade_report_grader($courseid, $gpr, $context, $page, $sortitemid);
|
||||
|
||||
// Initialise the grader report object
|
||||
if (ajaxenabled() && $report->get_pref('enableajax')) {
|
||||
require_once $CFG->dirroot.'/grade/report/grader/ajaxlib.php';
|
||||
|
||||
require_js(array('yui_yahoo',
|
||||
'yui_dom',
|
||||
'yui_event',
|
||||
'yui_json',
|
||||
'yui_connection',
|
||||
'yui_dragdrop',
|
||||
'yui_animation'));
|
||||
|
||||
if (debugging('', DEBUG_DEVELOPER)) {
|
||||
require_js(array('yui_logger'));
|
||||
|
||||
$bodytags = 'onload = "javascript:
|
||||
show_logger = function() {
|
||||
var logreader = new YAHOO.widget.LogReader();
|
||||
logreader.newestOnTop = false;
|
||||
logreader.setTitle(\'Moodle Debug: YUI Log Console\');
|
||||
};
|
||||
show_logger();
|
||||
"';
|
||||
}
|
||||
$report = new grade_report_grader_ajax($courseid, $gpr, $context, $page, $sortitemid);
|
||||
}
|
||||
|
||||
/// processing posted grades & feedback here
|
||||
if ($data = data_submitted() and confirm_sesskey() and has_capability('moodle/grade:edit', $context)) {
|
||||
@ -141,8 +167,7 @@ $numusers = $report->get_numusers();
|
||||
$report->load_final_grades();
|
||||
|
||||
/// Print header
|
||||
print_header_simple($strgrades.': '.$reportname, ': '.$strgrades, $navigation,
|
||||
'', '', true, $buttons, navmenu($course));
|
||||
print_header_simple($strgrades.': '.$reportname, ': '.$strgrades, $navigation, '', '', true, $buttons, navmenu($course), false, $bodytags);
|
||||
|
||||
/// Print the plugin selector at the top
|
||||
print_grade_plugin_selector($courseid, 'report', 'grader');
|
||||
@ -168,17 +193,21 @@ if (!empty($studentsperpage)) {
|
||||
|
||||
$reporthtml = '<script src="functions.js" type="text/javascript"></script>';
|
||||
|
||||
$reporthtml .= '<div id="grader_report_message"></div>' . "\n";
|
||||
$reporthtml .= '<table id="user-grades" class="gradestable flexible boxaligncenter generaltable">';
|
||||
$reporthtml .= '<thead>';
|
||||
$reporthtml .= $report->get_headerhtml();
|
||||
$reporthtml .= $report->get_iconshtml();
|
||||
$reporthtml .= $report->get_rangehtml();
|
||||
$reporthtml .= $report->get_studentshtml();
|
||||
$reporthtml .= "</thead>\n<tfoot>";
|
||||
$reporthtml .= $report->get_avghtml(true);
|
||||
$reporthtml .= $report->get_avghtml();
|
||||
$reporthtml .= "</table>";
|
||||
$reporthtml .= "</tfoot>\n<tbody>";
|
||||
$reporthtml .= $report->get_studentshtml();
|
||||
$reporthtml .= "</tbody>\n</table>";
|
||||
|
||||
// print submit button
|
||||
if ($USER->gradeediting[$course->id]) {
|
||||
if ($USER->gradeediting[$course->id] and !$report->get_pref('enableajax')) {
|
||||
echo '<form action="index.php" method="post">';
|
||||
echo '<div>';
|
||||
echo '<input type="hidden" value="'.$courseid.'" name="id" />';
|
||||
@ -189,7 +218,9 @@ if ($USER->gradeediting[$course->id]) {
|
||||
echo $reporthtml;
|
||||
|
||||
// print submit button
|
||||
if ($USER->gradeediting[$course->id] && ($report->get_pref('showquickfeedback') || $report->get_pref('quickgrading'))) {
|
||||
if ($USER->gradeediting[$course->id] && ($report->get_pref('showquickfeedback')
|
||||
||
|
||||
$report->get_pref('quickgrading')) && !$report->get_pref('enableajax')) {
|
||||
echo '<div class="submit"><input type="submit" value="'.get_string('update').'" /></div>';
|
||||
echo '</div></form>';
|
||||
}
|
||||
@ -199,6 +230,11 @@ if (!empty($studentsperpage) && $studentsperpage >= 20) {
|
||||
print_paging_bar($numusers, $report->page, $studentsperpage, $report->pbarurl);
|
||||
}
|
||||
|
||||
// Print AJAX code
|
||||
if ($report->get_pref('enableajax')) {
|
||||
require_once 'ajax.php';
|
||||
}
|
||||
|
||||
print_footer($course);
|
||||
|
||||
?>
|
||||
|
@ -115,7 +115,7 @@ class grader_report_preferences_form extends moodleform {
|
||||
$preferences['prefgeneral']['aggregationposition'] = array(GRADE_REPORT_PREFERENCE_DEFAULT => '*default*',
|
||||
GRADE_REPORT_AGGREGATION_POSITION_FIRST => get_string('positionfirst', 'grades'),
|
||||
GRADE_REPORT_AGGREGATION_POSITION_LAST => get_string('positionlast', 'grades'));
|
||||
// $preferences['prefgeneral']['enableajax'] = $checkbox_default;
|
||||
$preferences['prefgeneral']['enableajax'] = $checkbox_default;
|
||||
|
||||
$preferences['prefshow']['showuserimage'] = $checkbox_default;
|
||||
$preferences['prefshow']['showuseridnumber'] = $checkbox_default;
|
||||
|
@ -50,8 +50,8 @@ $settings->add(new admin_setting_configselect('grade_report_meanselection', get_
|
||||
array(GRADE_REPORT_MEAN_ALL => get_string('meanall', 'grades'),
|
||||
GRADE_REPORT_MEAN_GRADED => get_string('meangraded', 'grades'))));
|
||||
|
||||
// $settings->add(new admin_setting_configcheckbox('grade_report_enableajax', get_string('enableajax', 'grades'),
|
||||
// get_string('configenableajax', 'grades'), 0));
|
||||
$settings->add(new admin_setting_configcheckbox('grade_report_enableajax', get_string('enableajax', 'grades'),
|
||||
get_string('configenableajax', 'grades'), 0));
|
||||
|
||||
$settings->add(new admin_setting_configcheckbox('grade_report_showcalculations', get_string('showcalculations', 'grades'),
|
||||
get_string('configshowcalculations', 'grades'), 0));
|
||||
|
@ -345,6 +345,7 @@ $string['nopublish'] = 'Do not publish';
|
||||
$string['noselectedcategories'] = 'no categories were selected.';
|
||||
$string['noselecteditems'] = 'no items were selected.';
|
||||
$string['notteachererror'] = 'You must be a teacher to use this feature.';
|
||||
$string['nousersloaded'] = 'No users loaded';
|
||||
$string['numberofgrades'] = 'Number of grades';
|
||||
$string['onascaleof'] = ' on a scale of $a->grademin to $a->grademax';
|
||||
$string['operations'] = 'Operations';
|
||||
|
@ -20,10 +20,13 @@ function ajax_get_lib($libname) {
|
||||
'yui_calendar' => '/lib/yui/calendar/calendar-min.js',
|
||||
'yui_connection' => '/lib/yui/connection/connection-min.js',
|
||||
'yui_container' => '/lib/yui/container/container-min.js',
|
||||
'yui_datasource' => '/lib/yui/datasource/datasource-beta-min.js',
|
||||
'yui_dom' => '/lib/yui/dom/dom-min.js',
|
||||
'yui_dom-event' => '/lib/yui/yahoo-dom-event/yahoo-dom-event.js',
|
||||
'yui_dragdrop' => '/lib/yui/dragdrop/dragdrop-min.js',
|
||||
'yui_ddsend' => '/lib/yui/dragdrop/ddsend.js',
|
||||
'yui_event' => '/lib/yui/event/event-min.js',
|
||||
'yui_json' => '/lib/yui/json/json-min.js',
|
||||
'yui_logger' => '/lib/yui/logger/logger-min.js',
|
||||
'yui_menu' => '/lib/yui/menu/menu-min.js',
|
||||
'yui_tabview' => '/lib/yui/tabview/tabview-min.js',
|
||||
|
Loading…
x
Reference in New Issue
Block a user