),
'idnumber' => array(
'type' => PARAM_RAW, // ID numbers are plain text.
- 'default' => ''
+ 'default' => '',
+ 'null' => NULL_ALLOWED
),
'visible' => array(
'type' => PARAM_BOOL,
echo '<div class="clearer"><!-- --></div>';
echo $newsettingshtml;
echo '</fieldset>';
-echo '<div class="form-buttons"><input class="form-submit" type="submit" value="'.get_string('savechanges','admin').'" /></div>';
+echo '<div class="form-buttons"><input class="form-submit btn btn-primary" type="submit" value="'.get_string('savechanges','admin').'" /></div>';
echo '</div>';
echo '</form>';
// The first category in the list.
$radionode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-new-course ')]" .
- "/descendant::div[@class='restore-course-search']" .
+ "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' restore-course-search ')]" .
"/descendant::input[@type='radio']");
$radionode->click();
"require-dev": {
"phpunit/phpunit": "5.5.*",
"phpunit/dbUnit": "1.4.*",
- "moodlehq/behat-extension": "3.32.3"
+ "moodlehq/behat-extension": "3.32.4"
}
}
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
- "hash": "ec5f1e9d8b8134cf6a4490ba5dfe0082",
- "content-hash": "583f9a915721de799118a396dc81f177",
+ "hash": "6042e8e9600da0881e3f63b2e79ad742",
+ "content-hash": "59c508468d7f10d1a16b4d24228e7eab",
"packages": [],
"packages-dev": [
{
},
{
"name": "moodlehq/behat-extension",
- "version": "v3.32.3",
+ "version": "v3.32.4",
"source": {
"type": "git",
"url": "https://github.com/moodlehq/moodle-behat-extension.git",
- "reference": "8296916088e62f7cdf67b34abeebd419b301caff"
+ "reference": "d363b92f62770acdd8cd878810777f3a61eada4d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/8296916088e62f7cdf67b34abeebd419b301caff",
- "reference": "8296916088e62f7cdf67b34abeebd419b301caff",
+ "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/d363b92f62770acdd8cd878810777f3a61eada4d",
+ "reference": "d363b92f62770acdd8cd878810777f3a61eada4d",
"shasum": ""
},
"require": {
"Behat",
"moodle"
],
- "time": "2016-10-03 03:33:07"
+ "time": "2016-11-10 23:36:48"
},
{
"name": "myclabs/deep-copy",
JOIN {course} c ON cm.course = c.id
JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel
WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component
+ AND cm.deletioninprogress = 0
AND c.id %COURSEFILTER% AND cm.id %ITEMFILTER%";
$params = array('itemtype' => 'course_modules', 'tagid' => $tag->id, 'component' => 'core',
| Category ID number | CAT1 |
When I press "Create category"
Then I should see "ID number is already used for another category"
+
+ Scenario: Test that is possible to remove an idnumber from a course category
+ Given the following "categories" exist:
+ | name | category | idnumber |
+ | Cat 1 | 0 | CAT1 |
+ | Cat 2 | 0 ||
+ And I log in as "admin"
+ And I go to the courses management page
+ And I should see "CAT1" in the "#category-listing" "css_element"
+ When I click on "edit" action for "Cat 1" in management category listing
+ And I set the following fields to these values:
+ | Category name | Category 1 (edited) |
+ | Category ID number ||
+ And I press "Save changes"
+ # Redirect
+ Then I should see "Category 1 (edited)" in the "#category-listing" "css_element"
+ And I should not see "CAT1" in the "#course-listing" "css_element"
.user-enroller-panel {
width: 400px;
- background-color: #666;
position: absolute;
- top: 10%;
- left: 10%;
- border: 1px solid #666;
- border-width: 0 5px 5px 0;
}
.user-enroller-panel.hidden {
}
.user-enroller-panel .uep-wrap {
- margin-top: -5px;
- margin-left: -5px;
- background-color: #fff;
- border: 1px solid #999;
height: inherit;
}
-.user-enroller-panel .uep-header {
- background-color: #eee;
- padding: 1px;
-}
-
-.user-enroller-panel .uep-header h2 {
- margin: 3px 1em 0.5em 1em;
- font-size: 1em;
+.user-enroller-panel .uep-search-results .user .count {
+ display: none;
}
-.user-enroller-panel .uep-header .close {
- width: 25px;
- height: 15px;
- position: absolute;
- top: 2px;
- right: 1em;
- cursor: pointer;
- background: url("sprite.png") no-repeat scroll 0 0 transparent;
+.user-enroller-panel .uep-search-results .cohort .count {
+ display: none;
}
.user-enroller-panel .uep-content {
- text-align: center;
position: relative;
width: 100%;
- border-top: 1px solid #999;
- border-bottom: 1px solid #999;
-}
-
-.user-enroller-panel .uep-content .uep-controls {
- margin: 0;
- padding: 3px;
- background-color: #ddd;
- text-align: left;
- border-bottom: 1px solid #bbb;
-}
-
-.user-enroller-panel .uep-content .uep-controls label {
- display: inline;
- padding-right: 5px;
-}
-
-.user-enroller-panel .uep-content .uep-controls .uep-enrolment-option input {
- vertical-align: middle;
- margin-left: 1em;
+ box-sizing: border-box;
+ max-height: 1000px;
}
.user-enroller-panel .uep-ajax-content {
overflow: auto;
}
-.user-enroller-panel .uep-search-results .totalusers {
- background-color: #eee;
- padding: 5px;
- border-bottom: 1px solid #bbb;
- font-size: 7pt;
- font-weight: bold;
-}
-
-.user-enroller-panel .uep-search-results .user {
- width: 100%;
- text-align: left;
- font-size: 9pt;
- border-bottom: 1px solid #ddd;
- border-top: 1px solid #eee;
-}
-
-.user-enroller-panel .uep-search-results .user.odd {
- border-bottom: 1px solid #ddd;
- border-top: 1px solid #eee;
- background-color: #f9f9f9;
-}
-
-.user-enroller-panel .uep-search-results .user .count {
- width: 20px;
- float: left;
- font-size: 7pt;
- line-height: 41px;
- border-right: 1px solid #ddd;
- background-color: #eee;
- text-align: right;
- padding: 2px;
-}
-
.user-enroller-panel .uep-search-results .user .picture {
width: 45px;
float: left;
}
.user-enroller-panel .uep-search-results .user .details {
- width: 250px;
+ width: 180px;
float: left;
margin: 3px;
}
.user-enroller-panel .uep-search-results .user .options {
padding-right: 7px;
- font-size: 8pt;
margin: 3px;
}
cursor: pointer;
}
-.user-enroller-panel .uep-search-results .user.enrolled .count {
- width: 40px;
- color: #eee;
-}
-
-.user-enroller-panel .uep-search-results .uep-more-results {
- background-color: #eee;
- padding: 5px;
- border-top: 1px solid #bbb;
-}
-
-.user-enroller-panel .uep-search-results .totalcohorts {
- background-color: #eee;
- padding: 5px;
- border-bottom: 1px solid #bbb;
- font-size: 7pt;
- font-weight: bold;
-}
-
.user-enroller-panel .uep-search-results .cohort {
width: 100%;
text-align: left;
- font-size: 9pt;
- border-bottom: 1px solid #ddd;
- border-top: 1px solid #eee;
}
.user-enroller-panel .uep-search-results .cohort .count {
- width: 20px;
- float: left;
- font-size: 7pt;
- line-height: 27px;
- border-right: 1px solid #ddd;
- background-color: #eee;
- text-align: right;
- padding: 2px;
+ display: none;
}
.user-enroller-panel .uep-search-results .cohort .details {
- width: 250px;
+ width: 180px;
float: left;
margin: 5px;
}
-.user-enroller-panel .uep-search-results .cohort .options {
- padding-right: 7px;
- font-size: 8pt;
- margin: 3px;
-}
-
.user-enroller-panel .uep-search-results .cohort .options .enrol {
margin: 3px;
float: right;
.user-enroller-panel .uep-search-results .cohort.enrolled .count {
width: 40px;
- color: #eee;
}
.user-enroller-panel .uep-loading-lightbox {
position: absolute;
width: 100%;
height: 100%;
+ background-color: #ddd;
top: 0;
left: 0;
- background-color: #fff;
min-width: 50px;
min-height: 50px;
}
margin: auto;
vertical-align: middle;
margin-top: 125px;
+ display: block;
}
.user-enroller-panel .uep-footer {
- padding: 3px;
- background-color: #ddd;
- text-align: center;
+ text-align: right;
}
.user-enroller-panel .uep-search {
.user-enroller-panel .uep-search input {
width: 50%;
- margin: 0 0 10px 5px;
}
.user-enroller-panel .uep-search input.uep-search-btn {
}
.user-enroller-panel .collapsiblearea {
- border: 1px solid #bbb;
- background-color: #f6f6f6;
+ border: 1px solid #ddd;
+ padding: 0.5rem;
}
.user-enroller-panel .collapsiblearea.hidden {
/** CSS classes for nodes in structure **/
var CSS = {
PANEL : 'user-enroller-panel',
- WRAP : 'uep-wrap',
+ WRAP : 'uep-wrap modal-dialog',
HEADER : 'uep-header',
CONTENT : 'uep-content',
AJAXCONTENT : 'uep-ajax-content',
if (this.get(UEP.DISABLEGRADEHISTORY) != true) {
recovergrades = create('<div class="'+CSS.ENROLMENTOPTION+' '+CSS.RECOVERGRADES+'"></div>')
.append(create('<label class="'+CSS.RECOVERGRADESTITLE+'" for="'+CSS.RECOVERGRADES+'">'+M.util.get_string('recovergrades', 'enrol')+'</label>'))
- .append(create('<input type="checkbox" id="'+CSS.RECOVERGRADES+'" name="'+CSS.RECOVERGRADES+'"'+ this.get(UEP.RECOVERGRADESDEFAULT) +' />'))
+ .append(create('<input type="checkbox" class="m-x-1" id="'+CSS.RECOVERGRADES+'" name="'+CSS.RECOVERGRADES+'"'+ this.get(UEP.RECOVERGRADESDEFAULT) +' />'))
}
this.set(UEP.BASE, create('<div class="'+CSS.PANEL+' '+CSS.HIDDEN+'"></div>')
- .append(create('<div class="'+CSS.WRAP+'"></div>')
- .append(create('<div class="'+CSS.HEADER+' header"></div>')
- .append(create('<div class="'+CSS.CLOSE+'"></div>'))
- .append(create('<h2>'+M.util.get_string('enrolusers', 'enrol')+'</h2>')))
- .append(create('<div class="'+CSS.CONTENT+'"></div>')
- .append(create('<div class="'+CSS.SEARCHCONTROLS+'"></div>')
+ .append(create('<div class="'+CSS.WRAP+' modal show modal-dialog modal-content"></div>')
+ .append(create('<div class="'+CSS.HEADER+' header modal-header"></div>')
+ .append(create('<div class="'+CSS.CLOSE+'">×</div>'))
+ .append(create('<h2 class="modal-title">'+M.util.get_string('enrolusers', 'enrol')+'</h2>')))
+ .append(create('<div class="'+CSS.CONTENT+' modal-body"></div>')
+ .append(create('<div class="'+CSS.SEARCHCONTROLS+' form-inline"></div>')
.append(create('<div class="'+CSS.ENROLMENTOPTION+' '+CSS.ROLE+'"><label for="id_enrol_manual_assignable_roles">'+M.util.get_string('assignroles', 'role')+'</label></div>')
- .append(create('<select id="id_enrol_manual_assignable_roles"><option value="">'+M.util.get_string('none', 'enrol')+'</option></select>'))
+ .append(create('<select id="id_enrol_manual_assignable_roles" class="custom-select"><option value="">'+M.util.get_string('none', 'enrol')+'</option></select>'))
)
- .append(create('<div class="'+CSS.ENTITYSELECTOR+'"></div>'))
+ .append(create('<div class="'+CSS.ENTITYSELECTOR+' m-y-1"></div>'))
.append(create('<div class="'+CSS.SEARCHOPTIONS+'"></div>')
.append(create('<div class="'+CSS.COLLAPSIBLEHEADING+'"><img alt="" />'+M.util.get_string('enrolmentoptions', 'enrol')+'</div>'))
.append(create('<div class="'+CSS.COLLAPSIBLEAREA+' '+CSS.HIDDEN+'"></div>')
.append(recovergrades)
.append(create('<div class="'+CSS.ENROLMENTOPTION+' '+CSS.STARTDATE+'">'+M.util.get_string('startingfrom', 'moodle')+'</div>')
- .append(create('<select></select>')))
+ .append(create('<select class="custom-select"></select>')))
.append(create('<div class="'+CSS.ENROLMENTOPTION+' '+CSS.DURATION+'">'+M.util.get_string('enrolperiod', 'enrol')+'</div>')
- .append(create('<select><option value="0" selected="selected">'+M.util.get_string('unlimitedduration', 'enrol')+'</option></select>')))
+ .append(create('<select class="custom-select"><option value="0" selected="selected">'+M.util.get_string('unlimitedduration', 'enrol')+'</option></select>')))
)
)
.append(create('<div class="'+CSS.SEARCH+'"><label for="enrolusersearch" class="accesshide">'+M.util.get_string('usersearch', 'enrol')+'</label></div>')
- .append(create('<input type="text" id="enrolusersearch" value="" />'))
- .append(create('<input type="button" id="searchbtn" class="'+CSS.SEARCHBTN+'" value="'+M.util.get_string('usersearch', 'enrol')+'" />'))
+ .append(create('<input class="form-control" type="text" id="enrolusersearch" value="" />'))
+ .append(create('<input type="button" id="searchbtn" class="'+CSS.SEARCHBTN+' btn btn-secondary m-l-1" value="'+M.util.get_string('usersearch', 'enrol')+'" />'))
)
)
.append(create('<div class="'+CSS.AJAXCONTENT+'"></div>'))
.append(create('<img alt="loading" class="'+CSS.LOADINGICON+'" />')
.setAttribute('src', M.util.image_url('i/loading', 'moodle')))
.setStyle('opacity', 0.5)))
- .append(create('<div class="'+CSS.FOOTER+'"></div>')
+ .append(create('<div class="'+CSS.FOOTER+' modal-footer"></div>')
.append(create('<div class="'+CSS.CLOSEBTN+'"></div>')
- .append(create('<input type="button" value="'+M.util.get_string('finishenrollingusers', 'enrol')+'" />'))
+ .append(create('<input type="button" class="btn btn-primary" value="'+M.util.get_string('finishenrollingusers', 'enrol')+'" />'))
)
)
)
if (this.get(UEP.COHORTSAVAILABLE)) {
this.get(UEP.BASE).one('.'+CSS.ENTITYSELECTOR)
- .append(create('<input type="radio" id="id_enrol_manual_entity_users" name="enrol_manual_entity" value="users" checked="checked"/>'))
+ .append(create('<input type="radio" id="id_enrol_manual_entity_users" name="enrol_manual_entity" value="users" class="m-r-1" checked="checked"/>'))
.append(create('<label for="id_enrol_manual_entity_users">'+ M.util.get_string('browseusers', 'enrol_manual')+'</label>'))
- .append(create('<input type="radio" id="id_enrol_manual_entity_cohorts" name="enrol_manual_entity" value="cohorts"/>'))
+ .append(create('<input type="radio" id="id_enrol_manual_entity_cohorts" name="enrol_manual_entity" class="m-x-1" value="cohorts"/>'))
.append(create('<label for="id_enrol_manual_entity_cohorts">'+M.util.get_string('browsecohorts', 'enrol_manual')+'</label>'));
this.get(UEP.BASE).one('#id_enrol_manual_entity_cohorts').on('change', this.search, this);
this.get(UEP.BASE).one('#id_enrol_manual_entity_users').on('change', this.search, this);
}
var users;
if (!args.append) {
- users = create('<div class="'+CSS.USERS+'"></div>');
+ users = create('<div class="'+CSS.USERS+' list-group"></div>');
} else {
users = this.get(UEP.BASE).one('.'+CSS.SEARCHRESULTS+' .'+CSS.USERS);
}
for (var i in result.response.users) {
count++;
var user = result.response.users[i];
- users.append(create('<div class="'+CSS.USER+' clearfix" rel="'+user.id+'"></div>')
+ users.append(create('<div class="'+CSS.USER+' clearfix list-group-item list-group-item-action" rel="'+user.id+'"></div>')
.addClass((count%2)?CSS.ODD:CSS.EVEN)
.append(create('<div class="'+CSS.COUNT+'">'+count+'</div>'))
.append(create('<div class="'+CSS.PICTURE+'"></div>')
.append(create('<div class="'+CSS.FULLNAME+'">'+user.fullname+'</div>'))
.append(create('<div class="'+CSS.EXTRAFIELDS+'">'+user.extrafields+'</div>')))
.append(create('<div class="'+CSS.OPTIONS+'"></div>')
- .append(create('<input type="button" class="'+CSS.ENROL+'" value="'+M.util.get_string('enrol', 'enrol')+'" />')))
+ .append(create('<input type="button" class="'+CSS.ENROL+' btn btn-secondary" value="'+M.util.get_string('enrol', 'enrol')+'" />')))
);
}
this.set(UEP.USERCOUNT, count);
.append(create('<div class="'+CSS.DETAILS+'"></div>')
.append(create('<div class="'+CSS.COHORTNAME+'">'+cohort.name+'</div>')))
.append(create('<div class="'+CSS.OPTIONS+'"></div>')
- .append(create('<input type="button" class="' + CSS.ENROL + '" value="' + M.util.get_string('enrolxusers', 'enrol', cohort.cnt) + '" />')))
+ .append(create('<input type="button" class="' + CSS.ENROL + ' btn btn-secondary" value="' + M.util.get_string('enrolxusers', 'enrol', cohort.cnt) + '" />')))
);
}
this.set(UEP.COHORTCOUNT, count);
submitevent : null,
initializer : function() {
var i, m = this.get(MANIPULATOR);
- var element = Y.Node.create('<div class="enrolpanel roleassign"><div class="container"><div class="header"><h2>'+M.util.get_string('assignroles', 'role')+'</h2><div class="close"></div></div><div class="content"></div></div></div>');
+ var element = Y.Node.create('<div class="popover popover-bottom"><div class="arrow"></div>' +
+ '<div class="header popover-title">' +
+ '<div role="button" class="close" aria-label="Close">' +
+ '<span aria-hidden="true">×</span></div>' +
+ '<h3>'+M.util.get_string('assignroles', 'role')+'</h3>' +
+ '</div><div class="content popover-content form-inline form-group"></div></div>');
var content = element.one('.content');
var roles = m.get(ASSIGNABLEROLES);
for (i in roles) {
var buttonid = 'add_assignable_role_' + roles[i].id;
- var buttonhtml = '<input type="button" value="' + roles[i].name + '" id="' + buttonid + '" />';
+ var buttonhtml = '<input type="button" class="btn btn-secondary m-r-1" value="' +
+ roles[i].name + '" id="' + buttonid + '" />';
var button = Y.Node.create(buttonhtml);
button.on('click', this.submit, this, roles[i].id);
content.append(button);
} else {
this.get('elementNode').setStyle('left', x).setStyle('top', y);
}
- this.get('elementNode').addClass('visible');
+ this.get('elementNode').setStyle('display', 'block');
this.escCloseEvent = Y.on('key', this.hide, document.body, 'down:27', this);
this.displayed = true;
},
}
this.roles = [];
this.user = null;
- this.get('elementNode').removeClass('visible');
+ this.get('elementNode').setStyle('display', 'none');
if (this.submitevent) {
this.submitevent.detach();
this.submitevent = null;
$context = context_course::instance(SITEID);
// If the section name is set we show it.
- if (!is_null($section->name)) {
+ if (trim($section->name) !== '') {
echo $OUTPUT->heading(
format_string($section->name, true, array('context' => $context)),
2,
$string['cachedef_groupdata'] = 'Course group information';
$string['cachedef_htmlpurifier'] = 'HTML Purifier - cleaned content';
$string['cachedef_langmenu'] = 'List of available languages';
+$string['cachedef_message_time_last_message_between_users'] = 'Time created for most recent message between users';
$string['cachedef_locking'] = 'Locking';
$string['cachedef_message_processors_enabled'] = "Message processors enabled status";
$string['cachedef_navigation_expandcourse'] = 'Navigation expandable courses';
$string['messages'] = 'Messages';
$string['messagingdisabled'] = 'Messaging is disabled on this site, emails will be sent instead';
$string['newonlymsg'] = 'Show only new';
+$string['newmessage'] = 'New message';
+$string['newmessagesearch'] = 'Select or search for a contact to send a new message.';
+$string['newsearch'] = 'New search';
$string['noframesjs'] = 'Use more accessible interface';
$string['nocontacts'] = 'No contacts';
$string['nomessages'] = 'No messages';
$string['searchforuser'] = 'Search for a user';
$string['searchforuserorcourse'] = 'Search for a user or course';
$string['searchmessages'] = 'Search messages';
+$string['searchcombined'] = 'Search people and messages';
+$string['seeall'] = 'See all';
$string['selectmessagestodelete'] = 'Select messages to delete';
$string['selectnotificationtoview'] = 'Select from the list of notifications on the side to view more details';
$string['send'] = 'Send';
require_once(__DIR__ . '/../../config.php');
require_once($CFG->libdir . '/externallib.php');
+define('PREFERRED_RENDERER_TARGET', RENDERER_TARGET_GENERAL);
+
$rawjson = file_get_contents('php://input');
$requests = json_decode($rawjson, true);
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle 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 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * A timer that will execute a callback with decreasing frequency. Useful for
+ * doing polling on the server without overwhelming it with requests.
+ *
+ * @module core/backoff_timer
+ * @class backoff_timer
+ * @package core
+ * @copyright 2016 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(function() {
+
+ /**
+ * Constructor for the back off timer.
+ *
+ * @param {function} callback The function to execute after each tick
+ * @param {function} backoffFunction The function to determine what the next timeout value should be
+ */
+ var BackoffTimer = function(callback, backoffFunction) {
+ this.callback = callback;
+ this.backOffFunction = backoffFunction;
+ };
+
+ /**
+ * @type {function} callback The function to execute after each tick
+ */
+ BackoffTimer.prototype.callback = null;
+
+ /**
+ * @type {function} backoffFunction The function to determine what the next timeout value should be
+ */
+ BackoffTimer.prototype.backOffFunction = null;
+
+ /**
+ * @type {int} time The timeout value to use
+ */
+ BackoffTimer.prototype.time = null;
+
+ /**
+ * @type {numeric} timeout The timeout identifier
+ */
+ BackoffTimer.prototype.timeout = null;
+
+ /**
+ * Generate the next timeout in the back off time sequence
+ * for the timer.
+ *
+ * The back off function is called to calculate the next value.
+ * It is given the current value and an array of all previous values.
+ *
+ * @method generateNextTime
+ * @return {int} The new timeout value (in milliseconds)
+ */
+ BackoffTimer.prototype.generateNextTime = function() {
+ var newTime = this.backOffFunction(this.time);
+ this.time = newTime;
+
+ return newTime;
+ };
+
+ /**
+ * Stop the current timer and clear the previous time values
+ *
+ * @method reset
+ * @return {object} this
+ */
+ BackoffTimer.prototype.reset = function() {
+ this.time = null;
+ this.stop();
+
+ return this;
+ };
+
+ /**
+ * Clear the current timeout, if one is set.
+ *
+ * @method stop
+ * @return {object} this
+ */
+ BackoffTimer.prototype.stop = function() {
+ if (this.timeout) {
+ window.clearTimeout(this.timeout);
+ this.timeout = null;
+ }
+
+ return this;
+ };
+
+ /**
+ * Start the current timer by generating the new timeout value and
+ * starting the ticks.
+ *
+ * This function recurses after each tick with a new timeout value
+ * generated each time.
+ *
+ * The callback function is called after each tick.
+ *
+ * @method start
+ * @return {object} this
+ */
+ BackoffTimer.prototype.start = function() {
+ // If we haven't already started.
+ if (!this.timeout) {
+ var time = this.generateNextTime();
+ this.timeout = window.setTimeout(function() {
+ this.callback();
+ // Clear the existing timer.
+ this.stop();
+ // Start the next timer.
+ this.start();
+ }.bind(this), time);
+ }
+
+ return this;
+ };
+
+ /**
+ * Reset the timer and start it again from the initial timeout
+ * values
+ *
+ * @method restart
+ * @return {object} this
+ */
+ BackoffTimer.prototype.restart = function() {
+ return this.reset().start();
+ };
+
+ /**
+ * Returns an incremental function for the timer.
+ *
+ * @param {int} minamount The minimum amount of time we wait before checking
+ * @param {int} incrementamount The amount to increment the timer by
+ * @param {int} maxamount The max amount to ever increment to
+ * @param {int} timeoutamount The timeout to use once we reach the max amount
+ * @return {function}
+ */
+ BackoffTimer.getIncrementalCallback = function(minamount, incrementamount, maxamount, timeoutamount) {
+
+ /**
+ * An incremental function for the timer.
+ *
+ * @param {(int|null)} time The current timeout value or null if none set
+ * @return {int} The new timeout value
+ */
+ return function(time) {
+ if (!time) {
+ return minamount;
+ }
+
+ // Don't go over the max amount.
+ if (time + incrementamount > maxamount) {
+ return timeoutamount;
+ }
+
+ return time + incrementamount;
+ };
+ };
+
+ return BackoffTimer;
+});
* @return bool
*/
public static function can_use_tls12(array $curlinfo, $uname) {
- if ($curlinfo['version_number'] < 467456 || !defined('CURL_SSLVERSION_TLSv1_2')) {
+ // Do not compare the cURL version, e.g. $curlinfo['version_number'], with v7.34.0 (467456):
+ // some Linux distros backport security issues and keep lower version numbers.
+ if (!defined('CURL_SSLVERSION_TLSv1_2')) {
return false;
}
$newcategory->name = $data->name;
// Validate and set idnumber.
- if (!empty($data->idnumber)) {
+ if (isset($data->idnumber)) {
if (core_text::strlen($data->idnumber) > 100) {
throw new moodle_exception('idnumbertoolong');
}
- if ($DB->record_exists('course_categories', array('idnumber' => $data->idnumber))) {
+ if (strval($data->idnumber) !== '' && $DB->record_exists('course_categories', array('idnumber' => $data->idnumber))) {
throw new moodle_exception('categoryidnumbertaken');
}
- }
- if (isset($data->idnumber)) {
$newcategory->idnumber = $data->idnumber;
}
$newcategory->name = $data->name;
}
- if (isset($data->idnumber) && $data->idnumber != $this->idnumber) {
+ if (isset($data->idnumber) && $data->idnumber !== $this->idnumber) {
if (core_text::strlen($data->idnumber) > 100) {
throw new moodle_exception('idnumbertoolong');
}
- if ($DB->record_exists('course_categories', array('idnumber' => $data->idnumber))) {
+ if (strval($data->idnumber) !== '' && $DB->record_exists('course_categories', array('idnumber' => $data->idnumber))) {
throw new moodle_exception('categoryidnumbertaken');
}
$newcategory->idnumber = $data->idnumber;
'staticacceleration' => true,
'staticaccelerationsize' => 3
),
+
+ // Caches the time of the last message between two users.
+ 'message_time_last_message_between_users' => array(
+ 'mode' => cache_store::MODE_APPLICATION,
+ 'simplekeys' => true, // The id of the sender and recipient is used.
+ 'simplevalues' => true,
+ 'datasource' => '\core_message\time_last_message_between_users',
+ ),
);
max-width: calc(100% - 1em);
}
+/*rtl:begin:ignore*/
.atto_image_button_left {
float: left;
margin: 0 0.5em 0 0;
margin: 0 0 0 0.5em;
max-width: calc(100% - 1em);
}
+/*rtl:end:ignore*/
\ No newline at end of file
return;
}
+ var extraclasses = this.textarea.getAttribute('class');
+
this._eventHandles = [];
this._wrapper = Y.Node.create('<div class="' + CSS.WRAPPER + '" />');
'role="textbox" ' +
'spellcheck="true" ' +
'aria-live="off" ' +
- 'class="{{CSS.CONTENT}}" ' +
+ 'class="{{CSS.CONTENT}} ' + extraclasses + '" ' +
'/>');
this.editor = Y.Node.create(template({
elementid: this.get('elementid'),
margin-left: -3px;
}
}
+
+.mceToolbar td {
+ box-sizing: content-box;
+}
}
}
+ // Only cache messages, not notifications.
+ if (empty($savemessage->notification)) {
+ // Cache the timecreated value of the last message between these two users.
+ $cache = cache::make('core', 'message_time_last_message_between_users');
+ $key = \core_message\helper::get_last_message_time_created_cache_key($savemessage->useridfrom,
+ $savemessage->useridto);
+ $cache->set($key, $savemessage->timecreated);
+ }
+
// Store unread message just in case we get a fatal error any time later.
$savemessage->id = $DB->insert_record('message', $savemessage);
$eventdata->savedmessageid = $savemessage->id;
if ($changeurl = $userauth->change_password_url()) {
redirect($changeurl);
} else {
+ require_once($CFG->dirroot . '/login/lib.php');
$SESSION->wantsurl = core_login_get_return_url();
redirect($CFG->httpswwwroot.'/login/change_password.php');
}
if ($USER->id != $user->id) {
$messageargs['user2'] = $user->id;
}
- if ($course->id != $SITE->id) {
- $messageargs['viewing'] = MESSAGE_VIEW_COURSE. $course->id;
- }
$url = new moodle_url('/message/index.php', $messageargs);
$usernode->add(get_string('messages', 'message'), $url, self::TYPE_SETTING, null, 'messages');
}
if ($USER->id != $user->id) {
$messageargs['user2'] = $user->id;
}
- if ($course->id != $SITE->id) {
- $messageargs['viewing'] = MESSAGE_VIEW_COURSE. $course->id;
- }
$url = new moodle_url('/message/index.php', $messageargs);
$dashboard->add(get_string('messages', 'message'), $url, self::TYPE_SETTING, null, 'messages');
}
// If the target hasn't been specified we need to guess the defaults.
// We also override the target with the default if the maintenance target has been provided.
// This ensures we don't use the maintenance renderer if we are processing a special target.
- if (CLI_SCRIPT) {
+ if (defined('PREFERRED_RENDERER_TARGET')) {
+ $target = PREFERRED_RENDERER_TARGET;
+ } else if (CLI_SCRIPT) {
$target = RENDERER_TARGET_CLI;
} else if (AJAX_SCRIPT) {
$target = RENDERER_TARGET_AJAX;
}
}
+ // Now resolve all theme settings or do any other postprocessing.
+ // This needs to be done before calling core parser, since the parser strips [[settings]] tags.
+ $csspostprocess = $this->csspostprocess;
+ if (function_exists($csspostprocess)) {
+ $css = $csspostprocess($css, $this);
+ }
+
// Post processing using an object representation of CSS.
$hastreeprocessor = !empty($this->csstreepostprocessor) && function_exists($this->csstreepostprocessor);
$needsparsing = $hastreeprocessor || !empty($this->rtlmode);
unset($csstree);
}
- // now resolve all theme settings or do any other postprocessing
- $csspostprocess = $this->csspostprocess;
- if (function_exists($csspostprocess)) {
- $css = $csspostprocess($css, $this);
- }
-
return $css;
}
</div>
{{> core/loading }}
</div>
+ {{$anchor}}
+ <a class="see-all-link"
+ href="{{{urls.seeall}}}">
+ <div class="popover-region-footer-container">
+ <div class="popover-region-seeall-text">{{#str}} seeall, message {{/str}}</div>
+ </div>
+ </a>
+ {{/anchor}}
</div>
</div>
} catch (moodle_exception $e) {
$this->assertInstanceOf('moodle_exception', $e);
}
+ // Test that duplicates with an idnumber of 0 cannot be created.
+ coursecat::create(array('name' => 'Cat3', 'idnumber' => '0'));
+ try {
+ coursecat::create(array('name' => 'Cat4', 'idnumber' => '0'));
+ $this->fail('Duplicate idnumber "0" exception expected in coursecat::create');
+ } catch (moodle_exception $e) {
+ $this->assertInstanceOf('moodle_exception', $e);
+ }
+ // Test an update cannot make a duplicate idnumber of 0.
+ try {
+ $cat2->update(array('idnumber' => '0'));
+ $this->fail('Duplicate idnumber "0" exception expected in coursecat::update');
+ } catch (Exception $e) {
+ $this->assertInstanceOf('moodle_exception', $e);
+ }
}
public function test_visibility() {
defined('MOODLE_INTERNAL') || die();
-// Hack to let tests run on travis..
+// Hack to let tests run on Travis CI.
defined('CURL_SSLVERSION_TLSv1_2') || define('CURL_SSLVERSION_TLSv1_2', 6);
/**
*/
class upgrade_util_testcase extends advanced_testcase {
- /**
- * A cURL version that supports TLS 1.2.
- */
- const VALID_CURL_VERSION = 467456;
-
- /**
- * A cURL version that does not support TLS 1.2.
- */
- const INVALID_CURL_VERSION = 467455;
-
/**
* The value of PHP_ZTS when thread safety is enabled.
*/
// Set the curl values we are testing to the passed data.
$curlinfo['ssl_version'] = $sslversion;
- $curlinfo['version_number'] = self::VALID_CURL_VERSION;
// Set uname to system value if none passed in test case.
$uname = !empty($uname) ? $uname : php_uname('r');
$this->assertSame($expected, \core\upgrade\util::can_use_tls12($curlinfo, $uname));
-
- // Now set the curl version to outdated one.
- $curlinfo['version_number'] = self::INVALID_CURL_VERSION;
- // Tls12 should never be possible now curl version is bad.
- $this->assertFalse(\core\upgrade\util::can_use_tls12($curlinfo, $uname));
}
/**
* Messagearea class.
*
* @param {String} selector The selector for the page region containing the message area.
+ * @param {int} pollmin
+ * @param {int} pollmax
+ * @param {int} polltimeout
*/
- function Messagearea(selector) {
+ function Messagearea(selector, pollmin, pollmax, polltimeout) {
this.node = $(selector);
+ this.pollmin = pollmin;
+ this.pollmax = pollmax;
+ this.polltimeout = polltimeout;
this._init();
}
/** @type {jQuery} The jQuery node for the page region containing the message area. */
Messagearea.prototype.node = null;
+ /** @type {int} The minimum time to poll for messages. */
+ Messagearea.prototype.pollmin = null;
+
+ /** @type {int} The maximum time to poll for messages. */
+ Messagearea.prototype.pollmax = null;
+
+ /** @type {int} The time used once we have reached the maximum polling time. */
+ Messagearea.prototype.polltimeout = null;
+
/**
* Initialise the other objects we require.
*/
return this.node.data('userid');
};
+ /**
+ * Function to determine if we should be showing contacts initially or messages.
+ *
+ * @return {boolean} True to show contacts first, otherwise show messages.
+ */
+ Messagearea.prototype.showContactsFirst = function() {
+ return !!this.node.data('displaycontacts');
+ };
+
return Messagearea;
}
);
this.messageArea.onDelegateEvent(CustomEvents.events.scrollBottom, SELECTORS.CONTACTS,
this._loadContacts.bind(this));
- // Set the number of conversations. We set this to the number of conversations we asked to retrieve not by
- // the number that was actually retrieved, see MDL-55870.
- this._numConversationsDisplayed = 20;
+ if (!this.messageArea.showContactsFirst()) {
+ // Set the initial number of conversations to retrieve. Otherwise it will display no conversations.
+ this._numConversationsDisplayed = 20;
+ }
};
/**
*/
define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/custom_interaction_events',
'core/auto_rows', 'core_message/message_area_actions', 'core/modal_factory', 'core/modal_events',
- 'core/str', 'core_message/message_area_events'],
- function($, Ajax, Templates, Notification, CustomEvents, AutoRows, Actions, ModalFactory, ModalEvents, Str, Events) {
+ 'core/str', 'core_message/message_area_events', 'core/backoff_timer'],
+ function($, Ajax, Templates, Notification, CustomEvents, AutoRows, Actions, ModalFactory,
+ ModalEvents, Str, Events, BackOffTimer) {
/** @type {int} The message area default height. */
var MESSAGES_AREA_DEFAULT_HEIGHT = 500;
STARTDELETEMESSAGES: "[data-action='start-delete-messages']"
};
+ /** @type {int} The number of milliseconds in a second. */
+ var MILLISECONDSINSEC = 1000;
+
/**
* Messages class.
*
/** @type {Modal} the confirmation modal */
Messages.prototype._confirmationModal = null;
+ /** @type {int} the timestamp for the earliest visible message */
+ Messages.prototype._earliestMessageTimestamp = 0;
+
+ /** @type {BackOffTimer} the backoff timer */
+ Messages.prototype._backoffTimer = null;
+
/** @type {Messagearea} The messaging area object. */
Messages.prototype.messageArea = null;
if (messages.length) {
this._addScrollEventListener(messages.find(SELECTORS.MESSAGE).length);
}
+
+ // Create a timer to poll the server for new messages.
+ this._backoffTimer = new BackOffTimer(this._loadNewMessages.bind(this),
+ BackOffTimer.getIncrementalCallback(this.messageArea.pollmin * MILLISECONDSINSEC, MILLISECONDSINSEC,
+ this.messageArea.pollmax * MILLISECONDSINSEC, this.messageArea.polltimeout * MILLISECONDSINSEC));
+
+ // Start the timer.
+ this._backoffTimer.start();
};
/**
Messages.prototype._viewMessages = function(event, userid) {
// We are viewing another user, or re-loading the panel, so set number of messages displayed to 0.
this._numMessagesDisplayed = 0;
+ // Stop the existing timer so we can set up the new user's messages.
+ this._backoffTimer.stop();
+ // Reset the earliest timestamp when we change the messages view.
+ this._earliestMessageTimestamp = 0;
// Mark all the messages as read.
var markMessagesAsRead = Ajax.call([{
}).then(function(html, js) {
Templates.replaceNodeContents(this.messageArea.find(SELECTORS.MESSAGESAREA), html, js);
this._addScrollEventListener(numberreceived);
+ // Restart the poll timer.
+ this._backoffTimer.restart();
}.bind(this)).fail(Notification.exception);
};
}.bind(this)).fail(Notification.exception);
};
+ /**
+ * Loads and renders messages newer than the most recently seen messages.
+ *
+ * @return {Promise|boolean} The promise resolved when the messages have been loaded.
+ * @private
+ */
+ Messages.prototype._loadNewMessages = function() {
+ if (this._isLoadingMessages) {
+ return false;
+ }
+
+ // If we have no user id yet then bail early.
+ if (!this._getUserId()) {
+ return false;
+ }
+
+ this._isLoadingMessages = true;
+
+ // Only scroll the message window if the user hasn't scrolled up.
+ var shouldScrollBottom = false;
+ var messages = this.messageArea.find(SELECTORS.MESSAGES);
+ if (messages.length !== 0) {
+ var scrollTop = messages.scrollTop();
+ var innerHeight = messages.innerHeight();
+ var scrollHeight = messages[0].scrollHeight;
+
+ if (scrollTop + innerHeight >= scrollHeight) {
+ shouldScrollBottom = true;
+ }
+ }
+
+ // Keep track of the number of messages received.
+ var numberreceived = 0;
+ return this._getMessages(this._getUserId(), true).then(function(data) {
+ // Filter out any messages already rendered.
+ var messagesArea = this.messageArea.find(SELECTORS.MESSAGES);
+ data.messages = data.messages.filter(function(message) {
+ var id = "" + message.id + message.isread;
+ var result = messagesArea.find(SELECTORS.MESSAGE + '[data-id="' + id + '"]');
+ return !result.length;
+ });
+
+ numberreceived = data.messages.length;
+ // We have the data - lets render the template with it.
+ return Templates.render('core_message/message_area_messages', data);
+ }.bind(this)).then(function(html, js) {
+ // Check if we got something to do.
+ if (numberreceived > 0) {
+ html = $(html);
+ // Remove the new block time as it's present above.
+ html.find(SELECTORS.BLOCKTIME).remove();
+ // Show the new content.
+ Templates.appendNodeContents(this.messageArea.find(SELECTORS.MESSAGES), html, js);
+ // Scroll the new message into view.
+ if (shouldScrollBottom) {
+ this._scrollBottom();
+ }
+ // Increment the number of messages displayed.
+ this._numMessagesDisplayed += numberreceived;
+ // Reset the poll timer because the user may be active.
+ this._backoffTimer.restart();
+ }
+ }.bind(this)).always(function() {
+ // Mark that we are no longer busy loading data.
+ this._isLoadingMessages = false;
+ }.bind(this)).fail(Notification.exception);
+ };
+
/**
* Handles returning a list of messages to display.
*
* @param {int} userid
+ * @param {bool} fromTimestamp Load messages from the earliest known timestamp
* @return {Promise} The promise resolved when the contact area has been rendered
* @private
*/
- Messages.prototype._getMessages = function(userid) {
+ Messages.prototype._getMessages = function(userid, fromTimestamp) {
+ var args = {
+ currentuserid: this.messageArea.getCurrentUserId(),
+ otheruserid: userid,
+ limitfrom: this._numMessagesDisplayed,
+ limitnum: this._numMessagesToRetrieve,
+ newest: true
+ };
+
+ // If we're trying to load new messages since the message UI was
+ // rendered. Used for ajax polling while user is on the message UI.
+ if (fromTimestamp) {
+ args.timefrom = this._earliestMessageTimestamp;
+ // Remove limit and offset. We want all new messages.
+ args.limitfrom = 0;
+ args.limitnum = 0;
+ }
+
// Call the web service to get our data.
var promises = Ajax.call([{
methodname: 'core_message_data_for_messagearea_messages',
- args: {
- currentuserid: this.messageArea.getCurrentUserId(),
- otheruserid: userid,
- limitfrom: this._numMessagesDisplayed,
- limitnum: this._numMessagesToRetrieve,
- newest: true
- }
+ args: args,
}]);
// Do stuff when we get data back.
- return promises[0];
+ return promises[0].then(function(data) {
+ var messages = data.messages;
+
+ // Did we get any new messages?
+ if (messages && messages.length) {
+ var earliestMessage = messages[messages.length - 1];
+
+ // If we haven't set the timestamp yet then just use the earliest message.
+ if (!this._earliestMessageTimestamp) {
+ // Next request should be for the second after the most recent message we've seen.
+ this._earliestMessageTimestamp = earliestMessage.timecreated + 1;
+ // Update our record of the earliest known message for future requests.
+ } else if (earliestMessage.timecreated < this._earliestMessageTimestamp) {
+ // Next request should be for the second after the most recent message we've seen.
+ this._earliestMessageTimestamp = earliestMessage.timecreated + 1;
+ }
+ }
+
+ return data;
+ }.bind(this)).fail(function(ex) {
+ // Stop the timer if we received an error so that we don't keep spamming the server.
+ this._backoffTimer.stop();
+ Notification.exception(ex);
+ }.bind(this));
};
/**
);
// Set the initial search area.
- this._searchArea = this._searchAreas.MESSAGES;
+ this._searchArea = (this.messageArea.showContactsFirst()) ? this._searchAreas.USERS : this._searchAreas.MESSAGES;
};
/**
* @param int $limitfrom
* @param int $limitnum
* @param string $sort
+ * @param int $timefrom the time from the message being sent
+ * @param int $timeto the time up until the message being sent
* @return array
*/
- public static function get_messages($userid, $otheruserid, $limitfrom = 0, $limitnum = 0, $sort = 'timecreated ASC') {
+ public static function get_messages($userid, $otheruserid, $limitfrom = 0, $limitnum = 0,
+ $sort = 'timecreated ASC', $timefrom = 0, $timeto = 0) {
+
+ if (!empty($timefrom)) {
+ // Check the cache to see if we even need to do a DB query.
+ $cache = \cache::make('core', 'message_time_last_message_between_users');
+ $key = helper::get_last_message_time_created_cache_key($otheruserid, $userid);
+ $lastcreated = $cache->get($key);
+
+ // The last known message time is earlier than the one being requested so we can
+ // just return an empty result set rather than having to query the DB.
+ if ($lastcreated && $lastcreated < $timefrom) {
+ return [];
+ }
+ }
+
$arrmessages = array();
- if ($messages = helper::get_messages($userid, $otheruserid, 0, $limitfrom, $limitnum, $sort)) {
+ if ($messages = helper::get_messages($userid, $otheruserid, 0, $limitfrom, $limitnum,
+ $sort, $timefrom, $timeto)) {
+
$arrmessages = helper::create_messages($userid, $messages);
}
* @param int $limitfrom
* @param int $limitnum
* @param string $sort
+ * @param int $timefrom the time from the message being sent
+ * @param int $timeto the time up until the message being sent
* @return array of messages
*/
public static function get_messages($userid, $otheruserid, $timedeleted = 0, $limitfrom = 0, $limitnum = 0,
- $sort = 'timecreated ASC') {
+ $sort = 'timecreated ASC', $timefrom = 0, $timeto = 0) {
global $DB;
$messageid = $DB->sql_concat("'message_'", 'id');
WHERE ((useridto = ? AND useridfrom = ? AND timeusertodeleted = ?)
OR (useridto = ? AND useridfrom = ? AND timeuserfromdeleted = ?))
AND notification = 0
+ %where%
UNION ALL
SELECT {$messagereadid} AS fakeid, id, useridfrom, useridto, subject, fullmessage, fullmessagehtml, fullmessageformat,
smallmessage, notification, timecreated, timeread
WHERE ((useridto = ? AND useridfrom = ? AND timeusertodeleted = ?)
OR (useridto = ? AND useridfrom = ? AND timeuserfromdeleted = ?))
AND notification = 0
+ %where%
ORDER BY $sort";
- $params = array($userid, $otheruserid, $timedeleted,
- $otheruserid, $userid, $timedeleted,
- $userid, $otheruserid, $timedeleted,
- $otheruserid, $userid, $timedeleted);
+ $params1 = array($userid, $otheruserid, $timedeleted,
+ $otheruserid, $userid, $timedeleted);
+
+ $params2 = array($userid, $otheruserid, $timedeleted,
+ $otheruserid, $userid, $timedeleted);
+ $where = array();
+
+ if (!empty($timefrom)) {
+ $where[] = 'AND timecreated >= ?';
+ $params1[] = $timefrom;
+ $params2[] = $timefrom;
+ }
+
+ if (!empty($timeto)) {
+ $where[] = 'AND timecreated <= ?';
+ $params1[] = $timeto;
+ $params2[] = $timeto;
+ }
+
+ $sql = str_replace('%where%', implode(' ', $where), $sql);
+ $params = array_merge($params1, $params2);
return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
}
return $params;
}
+
+ /**
+ * Returns the cache key for the time created value of the last message between two users.
+ *
+ * @param int $userid
+ * @param int $user2id
+ * @return string
+ */
+ public static function get_last_message_time_created_cache_key($userid, $user2id) {
+ $ids = [$userid, $user2id];
+ sort($ids);
+ return implode('_', $ids);
+ }
}
$message->position = 'right';
}
$message->timesent = userdate($this->timecreated, get_string('strftimetime'));
+ $message->timecreated = $this->timecreated;
$message->isread = !empty($this->timeread) ? 1 : 0;
return $message;
*/
public $requestedconversation;
+ /**
+ * @var int The minimum time to poll for messages.
+ */
+ public $pollmin;
+
+ /**
+ * @var int The maximum time to poll for messages.
+ */
+ public $pollmax;
+
+ /**
+ * @var int The time used once we have reached the maximum polling time.
+ */
+ public $polltimeout;
+
+ /**
+ * @var bool Are we creating a new message and show the contacts section first?
+ */
+ public $contactsfirst;
+
/**
* Constructor.
*
* @param array $contacts
* @param array|null $messages
* @param bool $requestedconversation
+ * @param bool $contactsfirst Whether we are viewing the contacts first.
+ * @param int $pollmin
+ * @param int $pollmax
+ * @param int $polltimeout
*/
- public function __construct($userid, $otheruserid, $contacts, $messages, $requestedconversation) {
+ public function __construct($userid, $otheruserid, $contacts, $messages, $requestedconversation, $contactsfirst, $pollmin,
+ $pollmax, $polltimeout) {
$this->userid = $userid;
- $this->otheruserid = $otheruserid;
+ // Setting the other user to null when showing contacts will remove any contact from being selected.
+ $this->otheruserid = (!$contactsfirst) ? $otheruserid : null;
$this->contacts = $contacts;
$this->messages = $messages;
$this->requestedconversation = $requestedconversation;
+ $this->pollmin = $pollmin;
+ $this->pollmax = $pollmax;
+ $this->polltimeout = $polltimeout;
+ $this->contactsfirst = $contactsfirst;
}
public function export_for_template(\renderer_base $output) {
$data->userid = $this->userid;
$contacts = new contacts($this->otheruserid, $this->contacts);
$data->contacts = $contacts->export_for_template($output);
- $messages = new messages($this->userid, $this->otheruserid, $this->messages);
+ if ($this->contactsfirst) {
+ // Don't show any messages if we are creating a new message.
+ $messages = new messages($this->userid, null, array());
+ } else {
+ $messages = new messages($this->userid, $this->otheruserid, $this->messages);
+ }
$data->messages = $messages->export_for_template($output);
- $data->isconversation = true;
+ $data->isconversation = ($this->contactsfirst) ? false : true;
$data->requestedconversation = $this->requestedconversation;
+ $data->pollmin = $this->pollmin;
+ $data->pollmax = $this->pollmax;
+ $data->polltimeout = $this->polltimeout;
+ $data->contactsfirst = $this->contactsfirst;
return $data;
}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle 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 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Cache data source for the time of the last message between users.
+ *
+ * @package core_message
+ * @category cache
+ * @copyright 2016 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_message;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cache data source for the time of the last message between users.
+ *
+ * @package core_message
+ * @category cache
+ * @copyright 2016 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class time_last_message_between_users implements \cache_data_source {
+
+ /** @var time_last_message_between_users the singleton instance of this class. */
+ protected static $instance = null;
+
+ /**
+ * Returns an instance of the data source class that the cache can use for loading data using the other methods
+ * specified by the cache_data_source interface.
+ *
+ * @param \cache_definition $definition
+ * @return object
+ */
+ public static function get_instance_for_cache(\cache_definition $definition) {
+ if (is_null(self::$instance)) {
+ self::$instance = new time_last_message_between_users();
+ }
+ return self::$instance;
+ }
+
+ /**
+ * Loads the data for the key provided ready formatted for caching.
+ *
+ * @param string|int $key The key to load.
+ * @return mixed What ever data should be returned, or false if it can't be loaded.
+ */
+ public function load_for_cache($key) {
+ list($userid1, $userid2) = explode('_', $key);
+
+ $message = api::get_most_recent_message($userid1, $userid2);
+
+ if ($message) {
+ return $message->timecreated;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Loads several keys for the cache.
+ *
+ * @param array $keys An array of keys each of which will be string|int.
+ * @return array An array of matching data items.
+ */
+ public function load_many_for_cache(array $keys) {
+ $results = [];
+
+ foreach ($keys as $key) {
+ $results[] = $this->load_for_cache($key);
+ }
+
+ return $results;
+ }
+}
'blocktime' => new external_value(PARAM_NOTAGS, 'The time to display above the message'),
'position' => new external_value(PARAM_ALPHA, 'The position of the text'),
'timesent' => new external_value(PARAM_NOTAGS, 'The time the message was sent'),
+ 'timecreated' => new external_value(PARAM_INT, 'The timecreated timestamp for the message'),
'isread' => new external_value(PARAM_INT, 'Determines if the message was read or not'),
)
);
'limitfrom' => new external_value(PARAM_INT, 'Limit from', VALUE_DEFAULT, 0),
'limitnum' => new external_value(PARAM_INT, 'Limit number', VALUE_DEFAULT, 0),
'newest' => new external_value(PARAM_BOOL, 'Newest first?', VALUE_DEFAULT, false),
+ 'timefrom' => new external_value(PARAM_INT,
+ 'The timestamp from which the messages were created', VALUE_DEFAULT, 0),
)
);
}
* @since 3.2
*/
public static function data_for_messagearea_messages($currentuserid, $otheruserid, $limitfrom = 0, $limitnum = 0,
- $newest = false) {
+ $newest = false, $timefrom = 0) {
global $CFG, $PAGE, $USER;
// Check if messaging is enabled.
'otheruserid' => $otheruserid,
'limitfrom' => $limitfrom,
'limitnum' => $limitnum,
- 'newest' => $newest
+ 'newest' => $newest,
+ 'timefrom' => $timefrom,
);
self::validate_parameters(self::data_for_messagearea_messages_parameters(), $params);
self::validate_context($systemcontext);
} else {
$sort = 'timecreated ASC';
}
- $messages = \core_message\api::get_messages($currentuserid, $otheruserid, $limitfrom, $limitnum, $sort);
+
+ // We need to enforce a one second delay on messages to avoid race conditions of current
+ // messages still being sent.
+ //
+ // There is a chance that we could request messages before the current time's
+ // second has elapsed and while other messages are being sent in that same second. In which
+ // case those messages will be lost.
+ //
+ // Instead we ignore the current time in the result set to ensure that second is allowed to finish.
+ if (!empty($timefrom)) {
+ $timeto = time() - 1;
+ } else {
+ $timeto = 0;
+ }
+
+ // No requesting messages from the current time, as stated above.
+ if ($timefrom == time()) {
+ $messages = [];
+ } else {
+ $messages = \core_message\api::get_messages($currentuserid, $otheruserid, $limitfrom,
+ $limitnum, $sort, $timefrom, $timeto);
+ }
+
$messages = new \core_message\output\messagearea\messages($currentuserid, $otheruserid, $messages);
$renderer = $PAGE->get_renderer('core_message');
// we are going to accept other URL parameters to figure this out.
$user1id = optional_param('user1', $USER->id, PARAM_INT);
$user2id = optional_param('user2', $id, PARAM_INT);
+$contactsfirst = optional_param('contactsfirst', 0, PARAM_INT);
$url = new moodle_url('/message/index.php');
if ($id) {
if ($user2id) {
$url->param('user2', $user2id);
}
+ if ($contactsfirst) {
+ $url->param('contactsfirst', $contactsfirst);
+ }
}
$PAGE->set_url($url);
// Get the renderer and the information we are going to be use.
$renderer = $PAGE->get_renderer('core_message');
$requestedconversation = false;
-$conversations = \core_message\api::get_conversations($user1->id, 0, 20);
+if ($contactsfirst) {
+ $conversations = \core_message\api::get_contacts($user1->id, 0, 20);
+} else {
+ $conversations = \core_message\api::get_conversations($user1->id, 0, 20);
+}
$messages = [];
if (!$user2realuser) {
// If there are conversations, but the user has not chosen a particular one, then render the most recent one.
$messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 20, 'timecreated DESC');
}
+$pollmin = !empty($CFG->messagingminpoll) ? $CFG->messagingminpoll : MESSAGE_DEFAULT_MIN_POLL_IN_SECONDS;
+$pollmax = !empty($CFG->messagingmaxpoll) ? $CFG->messagingmaxpoll : MESSAGE_DEFAULT_MAX_POLL_IN_SECONDS;
+$polltimeout = !empty($CFG->messagingtimeoutpoll) ? $CFG->messagingtimeoutpoll : MESSAGE_DEFAULT_TIMEOUT_POLL_IN_SECONDS;
$messagearea = new \core_message\output\messagearea\message_area($user1->id, $user2->id, $conversations, $messages,
- $requestedconversation);
+ $requestedconversation, $contactsfirst, $pollmin, $pollmax, $polltimeout);
// Now the page contents.
echo $OUTPUT->header();
require_once($CFG->libdir.'/eventslib.php');
-define ('MESSAGE_SHORTLENGTH', 300);
+define('MESSAGE_SHORTLENGTH', 300);
-define ('MESSAGE_DISCUSSION_WIDTH',600);
-define ('MESSAGE_DISCUSSION_HEIGHT',500);
-
-define ('MESSAGE_SHORTVIEW_LIMIT', 8);//the maximum number of messages to show on the short message history
-
-define('MESSAGE_HISTORY_SHORT',0);
-define('MESSAGE_HISTORY_ALL',1);
-
-define('MESSAGE_VIEW_UNREAD_MESSAGES','unread');
-define('MESSAGE_VIEW_RECENT_CONVERSATIONS','recentconversations');
-define('MESSAGE_VIEW_RECENT_NOTIFICATIONS','recentnotifications');
-define('MESSAGE_VIEW_CONTACTS','contacts');
-define('MESSAGE_VIEW_BLOCKED','blockedusers');
-define('MESSAGE_VIEW_COURSE','course_');
-define('MESSAGE_VIEW_SEARCH','search');
+define('MESSAGE_HISTORY_ALL', 1);
define('MESSAGE_SEARCH_MAX_RESULTS', 200);
-define('MESSAGE_CONTACTS_PER_PAGE',10);
-define('MESSAGE_MAX_COURSE_NAME_LENGTH', 30);
-
-define('MESSAGE_UNREAD', 'unread');
-define('MESSAGE_READ', 'read');
define('MESSAGE_TYPE_NOTIFICATION', 'notification');
define('MESSAGE_TYPE_MESSAGE', 'message');
*/
define('MESSAGE_DEFAULT_PERMITTED', 'permitted');
+/**
+ * Set default values for polling.
+ */
+define('MESSAGE_DEFAULT_MIN_POLL_IN_SECONDS', 10);
+define('MESSAGE_DEFAULT_MAX_POLL_IN_SECONDS', 2 * MINSECS);
+define('MESSAGE_DEFAULT_TIMEOUT_POLL_IN_SECONDS', 5 * MINSECS);
+
/**
* Retrieve users blocked by $user1
*
$context = [
'userid' => $USER->id,
'urls' => [
+ 'seeall' => (new moodle_url('/message/index.php'))->out(),
+ 'writeamessage' => (new moodle_url('/message/index.php', ['contactsfirst' => 1]))->out(),
'preferences' => (new moodle_url('/message/edit.php', ['id' => $USER->id]))->out(),
],
];
{{$headertext}}{{#str}} messages, message {{/str}}{{/headertext}}
{{$headeractions}}
+ <div class="newmessage-link">
+ {{$anchor}}
+ <a href="{{{urls.writeamessage}}}">{{#str}} newmessage, message {{/str}}
+ </a>
+ {{/anchor}}
+ </div>
{{< core/hover_tooltip }}
{{$anchor}}
<a class="mark-all-read-button"
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
-<div class="messaging-area-container" data-userid="{{userid}}">
+<div class="messaging-area-container" data-userid="{{userid}}" data-displaycontacts="{{contactsfirst}}">
<div class="messaging-area {{#requestedconversation}}show-messages{{/requestedconversation}}
{{^requestedconversation}}hide-messages{{/requestedconversation}}" data-region="messaging-area">
<div class="contacts-area" data-region="contacts-area" role="tablist">
{{#js}}
require(['core_message/message_area'],
function(Messagearea) {
- new Messagearea('.messaging-area-container');
+ new Messagearea('.messaging-area-container', {{pollmin}}, {{pollmax}}, {{polltimeout}});
}
);
{{/js}}
}}
<div class="searchtextarea" data-region="search-text-area">
<label class="accesshide" for="searchtext">{{#str}}search{{/str}}</label>
- <input data-region="search-box" type="text" id="searchtext" placeholder="{{#str}}searchmessages, message{{/str}}">
+ <input data-region="search-box" type="text" id="searchtext" placeholder="{{#contactsfirst}} {{#str}}searchforuserorcourse, message{{/str}} {{/contactsfirst}} {{^contactsfirst}} {{#str}}searchmessages, message{{/str}} {{/contactsfirst}}">
<div data-region="search-filter-area" class="searchfilterarea" style="display:none">
<div data-region="search-filter" class="searchfilter"></div>
<div data-action="search-filter-delete" class="searchfilterdelete">{{#pix}}t/delete{{/pix}}</div>
</div>
</div>
+{{#contactsfirst}}
+<div class="contacts" data-region="contacts" data-region-content="conversations" style="display:none;" role="tabpanel" id="conversations-tab-panel"></div>
+<div class="contacts" data-region="contacts" data-region-content="contacts" role="tabpanel" id="contacts-tab-panel">
+ {{> core_message/message_area_contacts }}
+</div>
+{{/contactsfirst}}
+{{^contactsfirst}}
<div class="contacts" data-region="contacts" data-region-content="conversations" role="tabpanel" id="conversations-tab-panel">
{{> core_message/message_area_contacts }}
</div>
-{{! Hidden divs to load the other tab and search panels via JS when appropriate. }}
<div class="contacts" data-region="contacts" data-region-content="contacts" style="display:none;" role="tabpanel" id="contacts-tab-panel"></div>
+{{/contactsfirst}}
+{{! Hidden divs to load the other tab and search panels via JS when appropriate. }}
<div class="contacts searcharea" data-region="search-results-area" style="display:none;"></div>
<div class="tabs">
- <div class="tab tabconversations selected" data-action="conversations-view" role="tab" aria-controls="conversations-tab-panel" aria-selected="true" tabindex="0">
+ <div class="tab tabconversations {{^contactsfirst}}selected{{/contactsfirst}} " data-action="conversations-view" role="tab" aria-controls="conversations-tab-panel" aria-selected="{{^contactsfirst}}true{{/contactsfirst}}{{#contactsfirst}}false{{/contactsfirst}}" tabindex="0">
<div class="tabimage">{{#pix}}t/message, moodle{{/pix}}</div>
<div>{{#str}}messages, message{{/str}}</div>
</div>
- <div class="tab tabcontacts" data-action="contacts-view" role="tab" aria-controls="contacts-tab-panel" aria-selected="false" tabindex="-1">
+ <div class="tab tabcontacts {{#contactsfirst}}selected{{/contactsfirst}}" data-action="contacts-view" role="tab" aria-controls="contacts-tab-panel" aria-selected="{{#contactsfirst}}true{{/contactsfirst}}{{^contactsfirst}}false{{/contactsfirst}}" tabindex="-1">
<div class="tabimage">{{#pix}}i/cohort, moodle{{/pix}}</div>
<div>{{#str}}contacts, message{{/str}}</div>
</div>
</div>
</div>
{{/otheruserid}}
+{{#contactsfirst}}
+<div class="messages-header">
+ <div class="name-container">
+ <div class="name">
+ {{#str}}newmessagesearch, message{{/str}}
+ </div>
+ </div>
+</div>
+{{/contactsfirst}}
<div class="messages" data-region="messages" data-userid="{{otheruserid}}">
{{> core_message/message_area_messages }}
</div>
$status = \core_message\api::is_processor_enabled($name);
$this->assertEquals(1, $status);
}
+
+ /**
+ * Test retrieving messages by providing a minimum timecreated value.
+ */
+ public function test_get_messages_time_from_only() {
+ // Create some users.
+ $user1 = self::getDataGenerator()->create_user();
+ $user2 = self::getDataGenerator()->create_user();
+
+ // The person doing the search.
+ $this->setUser($user1);
+
+ // Send some messages back and forth.
+ $time = 1;
+ $this->send_fake_message($user1, $user2, 'Message 1', 0, $time + 1);
+ $this->send_fake_message($user2, $user1, 'Message 2', 0, $time + 2);
+ $this->send_fake_message($user1, $user2, 'Message 3', 0, $time + 3);
+ $this->send_fake_message($user2, $user1, 'Message 4', 0, $time + 4);
+
+ // Retrieve the messages from $time, which should be all of them.
+ $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', $time);
+
+ // Confirm the message data is correct.
+ $this->assertEquals(4, count($messages));
+
+ $message1 = $messages[0];
+ $message2 = $messages[1];
+ $message3 = $messages[2];
+ $message4 = $messages[3];
+
+ $this->assertContains('Message 1', $message1->text);
+ $this->assertContains('Message 2', $message2->text);
+ $this->assertContains('Message 3', $message3->text);
+ $this->assertContains('Message 4', $message4->text);
+
+ // Retrieve the messages from $time + 3, which should only be the 2 last messages.
+ $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', $time + 3);
+
+ // Confirm the message data is correct.
+ $this->assertEquals(2, count($messages));
+
+ $message1 = $messages[0];
+ $message2 = $messages[1];
+
+ $this->assertContains('Message 3', $message1->text);
+ $this->assertContains('Message 4', $message2->text);
+ }
+
+ /**
+ * Test retrieving messages by providing a maximum timecreated value.
+ */
+ public function test_get_messages_time_to_only() {
+ // Create some users.
+ $user1 = self::getDataGenerator()->create_user();
+ $user2 = self::getDataGenerator()->create_user();
+
+ // The person doing the search.
+ $this->setUser($user1);
+
+ // Send some messages back and forth.
+ $time = 1;
+ $this->send_fake_message($user1, $user2, 'Message 1', 0, $time + 1);
+ $this->send_fake_message($user2, $user1, 'Message 2', 0, $time + 2);
+ $this->send_fake_message($user1, $user2, 'Message 3', 0, $time + 3);
+ $this->send_fake_message($user2, $user1, 'Message 4', 0, $time + 4);
+
+ // Retrieve the messages up until $time + 4, which should be all of them.
+ $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', 0, $time + 4);
+
+ // Confirm the message data is correct.
+ $this->assertEquals(4, count($messages));
+
+ $message1 = $messages[0];
+ $message2 = $messages[1];
+ $message3 = $messages[2];
+ $message4 = $messages[3];
+
+ $this->assertContains('Message 1', $message1->text);
+ $this->assertContains('Message 2', $message2->text);
+ $this->assertContains('Message 3', $message3->text);
+ $this->assertContains('Message 4', $message4->text);
+
+ // Retrieve the messages up until $time + 2, which should be the first two.
+ $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', 0, $time + 2);
+
+ // Confirm the message data is correct.
+ $this->assertEquals(2, count($messages));
+
+ $message1 = $messages[0];
+ $message2 = $messages[1];
+
+ $this->assertContains('Message 1', $message1->text);
+ $this->assertContains('Message 2', $message2->text);
+ }
+
+ /**
+ * Test retrieving messages by providing a minimum and maximum timecreated value.
+ */
+ public function test_get_messages_time_from_and_to() {
+ // Create some users.
+ $user1 = self::getDataGenerator()->create_user();
+ $user2 = self::getDataGenerator()->create_user();
+
+ // The person doing the search.
+ $this->setUser($user1);
+
+ // Send some messages back and forth.
+ $time = 1;
+ $this->send_fake_message($user1, $user2, 'Message 1', 0, $time + 1);
+ $this->send_fake_message($user2, $user1, 'Message 2', 0, $time + 2);
+ $this->send_fake_message($user1, $user2, 'Message 3', 0, $time + 3);
+ $this->send_fake_message($user2, $user1, 'Message 4', 0, $time + 4);
+
+ // Retrieve the messages from $time + 2 up until $time + 3, which should be 2nd and 3rd message.
+ $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', $time + 2, $time + 3);
+
+ // Confirm the message data is correct.
+ $this->assertEquals(2, count($messages));
+
+ $message1 = $messages[0];
+ $message2 = $messages[1];
+
+ $this->assertContains('Message 2', $message1->text);
+ $this->assertContains('Message 3', $message2->text);
+ }
}
Scenario: Search for messages
When I log in as "user1"
And I follow "Messages" in the user menu
- And I set the field "Search messages" to "User 2 to User 1"
+ And I set the field "searchtext" to "User 2 to User 1"
Then I should see "User 2" in the "search-results-area" "message_area_region"
And I should not see "User 3" in the "search-results-area" "message_area_region"
Scenario: Search for messages no results
When I log in as "user1"
And I follow "Messages" in the user menu
- And I set the field "Search messages" to "No message"
+ And I set the field "searchtext" to "No message"
Then I should see "No results" in the "search-results-area" "message_area_region"
$this->assertContains('Word.', $message4['text']);
}
+ /**
+ * Tests retrieving messages.
+ */
+ public function test_messagearea_messages_timefrom() {
+ $this->resetAfterTest(true);
+
+ // Create some users.
+ $user1 = self::getDataGenerator()->create_user();
+ $user2 = self::getDataGenerator()->create_user();
+
+ // The person asking for the messages.
+ $this->setUser($user1);
+
+ // Send some messages back and forth.
+ $time = time();
+ $this->send_message($user1, $user2, 'Message 1', 0, $time - 4);
+ $this->send_message($user2, $user1, 'Message 2', 0, $time - 3);
+ $this->send_message($user1, $user2, 'Message 3', 0, $time - 2);
+ $this->send_message($user2, $user1, 'Message 4', 0, $time - 1);
+
+ // Retrieve the messages from $time - 3, which should be the 3 most recent messages.
+ $result = core_message_external::data_for_messagearea_messages($user1->id, $user2->id, 0, 0, false, $time - 3);
+
+ // We need to execute the return values cleaning process to simulate the web service server.
+ $result = external_api::clean_returnvalue(core_message_external::data_for_messagearea_messages_returns(),
+ $result);
+
+ // Confirm the message data is correct. We shouldn't get 'Message 1' back.
+ $messages = $result['messages'];
+ $this->assertCount(3, $messages);
+
+ $message1 = $messages[0];
+ $message2 = $messages[1];
+ $message3 = $messages[2];
+
+ $this->assertContains('Message 2', $message1['text']);
+ $this->assertContains('Message 3', $message2['text']);
+ $this->assertContains('Message 4', $message3['text']);
+ }
+
/**
* Tests retrieving messages as another user.
*/
* This way it is possible to use FPDI for both FPDF and TCPDF with one FPDI version.
*/
-if (!class_exists('TCPDF', false)) {
- /**
- * Class fpdi_bridge
- */
- class fpdi_bridge extends FPDF
- {
- // empty body
- }
-
-} else {
-
/**
* Class fpdi_bridge
*
return current(unpack('H*', $str));
}
}
-}
\ No newline at end of file
3) Update mod/assign/feedback/editpdf/fpdi/fpdi_bridge.php (or the replacement file) to extend 'pdf' instead of 'TCPDF'.
4) Make a note below of any changes made.
+2016/11/15
+----------
+
+1) Class not exists check and the empty fpdi_bridge class has been removed from fpdi_bridge.php to fix a behat error.
+
+Updated by Simey Lameze (MDL-55848)
+
2015/12/04
----------
Updated to FPDI: 1.6.1
<script type="text/javascript">
//<![CDATA[
YUI().use("node", "event", function(Y) {
- //Take scrollbars off the outer document to prevent double scroll bar effect
var doc = Y.one("body");
- doc.setStyle("overflow", "hidden");
-
var frame = Y.one("#contentframe");
var padding = 15; //The bottom of the iframe wasn\'t visible on some themes. Probably because of border widths, etc.
var lastHeight;
}
$result = new stdClass();
- $result->info = get_string('grade') . ': ' . $grade->str_long_grade;
+ // If the user can't see hidden grades, don't return that information.
+ $gitem = grade_item::fetch(array('id' => $grades->items[0]->id));
+ if (!$gitem->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
+ $result->info = get_string('grade') . ': ' . $grade->str_long_grade;
+ } else {
+ $result->info = get_string('grade') . ': ' . get_string('hidden', 'grades');
+ }
// Datesubmitted == time created. dategraded == time modified or time overridden
// if grade was last modified by the user themselves use date graded. Otherwise use
$grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);
if (!empty($grades->items[0]->grades)) {
$grade = reset($grades->items[0]->grades);
- echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
- if ($grade->str_feedback) {
- echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
+ // If the user can't see hidden grades, don't return that information.
+ $gitem = grade_item::fetch(array('id' => $grades->items[0]->id));
+ if (!$gitem->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
+ echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
+ if ($grade->str_feedback) {
+ echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
+ }
+ } else {
+ echo $OUTPUT->container(get_string('grade') . ': ' . get_string('hidden', 'grades'));
+ if ($grade->str_feedback) {
+ echo $OUTPUT->container(get_string('feedback').': '.get_string('hidden', 'grades'));
+ }
}
}
if ($attempt->state != quiz_attempt::FINISHED) {
echo quiz_attempt_state_name($attempt->state);
} else {
- echo quiz_format_grade($quiz, $attempt->sumgrades) . '/' .
- quiz_format_grade($quiz, $quiz->sumgrades);
+ if (!isset($gitem)) {
+ if (!empty($grades->items[0]->grades)) {
+ $gitem = grade_item::fetch(array('id' => $grades->items[0]->id));
+ } else {
+ $gitem = new stdClass();
+ $gitem->hidden = true;
+ }
+ }
+ if (!$gitem->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
+ echo quiz_format_grade($quiz, $attempt->sumgrades) . '/' . quiz_format_grade($quiz, $quiz->sumgrades);
+ } else {
+ echo get_string('hidden', 'grades');
+ }
}
echo ' - '.userdate($attempt->timemodified).'<br />';
}
JOIN {course} c ON cm.course = c.id
JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel
WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component
+ AND cm.deletioninprogress = 0
AND wp.id %ITEMFILTER% AND c.id %COURSEFILTER%";
$params = array('itemtype' => 'wiki_pages', 'tagid' => $tag->id, 'component' => 'mod_wiki',
event.preventDefault();
var parent = $(element).closest('.form-group');
var feedback = parent.find('.form-control-feedback');
+
+ // Sometimes (atto) we have a hidden textarea backed by a real contenteditable div.
+ if (($(element).prop("tagName") == 'TEXTAREA') && parent.find('[contenteditable]')) {
+ element = parent.find('[contenteditable]');
+ }
if (msg !== '') {
parent.addClass('has-danger');
parent.data('client-validation-error', true);
'theme_boost/tooltip',
'theme_boost/popover'],
function() {
- jQuery('[data-toggle="popover"]').popover();
+ jQuery('body').popover({
+ selector: '[data-toggle="popover"]',
+ trigger: 'focus'
+ });
});
return {};
.dropdown-toggle {
/* So that the caret takes the colour of the icon. */
color: #999;
-
- &::after {
- margin-left: 0;
- }
}
}
border-bottom-left-radius: 0.5rem;
float: right;
}
+
+// Required for IE11 to prevent blocks being pushed under the content.
+#region-main {
+ float: left;
+ width: 100%;
+}
#region-main-settings-menu.has-blocks,
#region-main.has-blocks {
display: inline-block;
margin: 0 0 10px 5px;
}
-.user-enroller-panel {
- .uep-search-results {
- .user,
- .cohort {
- .options {
- .enrol {
- @extend .btn-sm;
- }
- }
- }
- }
-}
}
}
+/* Display month name above the calendar */
+table.calendartable caption {
+ caption-side: top;
+}
+
.ical-link {
font-size: 10px;
font-weight: bold;
@extend .card;
@extend .card-block;
}
-
- .user-enroller-panel .uep-search-results .user .details {
- width: 237px;
- }
-
- .user-enroller-panel .uep-search-results .cohort .details {
- width: 237px;
- }
}
#page-enrol-users .enrol-users-page-action input {
text-indent: -31px;
padding-left: 31px;
}
+
+ .dimmed {
+ .activityicon {
+ opacity: .5;
+ }
+ }
}
}
@extend .table-sm;
}
-.user-enroller-panel .uep-search-results .users,
-.user-enroller-panel .uep-search-results .cohorts,
table.grading-report,
.forumheaderlist,
.generaltable,
border-bottom: $border-width solid $table-border-color;
}
+.editor_atto_content.form-control {
+ width: 100%;
+}
+
.mform fieldset.collapsible legend a.fheader {
padding: 0 5px 0 ($spacer * 1.5);
margin-left: -($spacer * 1.5);
font-size: large;
}
+/* Non-bootstrap selects with a size show their contents outside of the element.
+ * Remove when we update to stable bootstrap 4. (MDL-56511) */
+select[size],
select[multiple] {
overflow: auto;
}
+select[size=1] {
+ overflow: visible;
+}
textarea[data-auto-rows] {
overflow-x: hidden;
margin-left: $grid-gutter-width / 2;
max-width: 30rem;
}
+
+/** Atto fields do not have form-control because that would break the layout of the editor.
+ So they need these extra styles to highlight the editor when there is a validation error. */
+.has-danger .editor_atto_content.form-control,
+.has-danger .editor_atto_content.form-control-danger {
+ @include form-control-validation($brand-danger);
+ background-image: $form-icon-danger;
+ padding-right: ($input-padding-x * 3);
+ background-repeat: no-repeat;
+ background-position: center right ($input-height / 4);
+ background-size: ($input-height / 2) ($input-height / 2);
+}
margin-left: 0;
}
-// Fix the first item in a drop down menu (it's a list of links and icons, but has been made vertical.
-.dropdown .dropdown-item .icon {
- margin-left: 0;
+// Fix the space between the dropdown icon and the caret.
+.action-menu {
+ .dropdown-toggle {
+ .icon {
+ // Use a negative margin on the icon to ensure that the dropdown toggle
+ // is an icon and not just text in which case the margin is needed.
+ margin-right: -$caret-width;
+ }
+ }
}
// YUI 2 Tree View icons must not have a margin left.
$region-container-width: 380px;
$region-container-z-index: 1;
$region-header-height: 25px;
+$region-footer-height: 30px;
$content-item-hover-colour-bg: #79b5e6;
$content-item-hover-colour-text: #fff;
$content-item-selected-colour-bg: #4f94cd;
$content-item-unread-colour: #f4f4f4;
+$content-header-footer-height: $region-header-height + $region-footer-height;
@mixin invisible() {
opacity: 0;
box-sizing: border-box;
}
+.popover-region-footer-container {
+ height: $region-footer-height;
+ text-align: center;
+ border-top: $standard-border;
+ background-color: $popover-bg;
+ padding-top: 3px;
+}
+
.popover-region-header-text {
float: left;
margin: 0;
height: 12px;
width: 12px;
}
+
+ .newmessage-link {
+ margin-right: 10px;
+ }
+
label {
display: inline-block;
text-align: center;
}
.popover-region-content-container {
- height: calc(100% - 25px);
+ height: calc(100% - #{$content-header-footer-height});
width: 100%;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
.userlist h3 .action-icon {
display: none;
}
+
+#page-enrol-users .popover {
+ max-width: none;
+}
+
+.user-enroller-panel {
+ width: 600px;
+}
<button class="fp-file-download btn btn-secondary">{{#str}}download{{/str}}</button>
<button class="fp-file-delete btn btn-secondary">{{#str}}delete{{/str}}</button>
<button class="fp-file-setmain btn btn-secondary">{{#str}}setmainfile, repository{{/str}}</button>
- <span class="fp-file-setmain-help btn btn-secondary">{{helpicon}}</span>
+ <span class="fp-file-setmain-help">{{{helpicon}}}</span>
<button class="fp-file-zip btn btn-secondary">{{#str}}zip, editor{{/str}}</button>
<button class="fp-file-unzip btn btn-secondary">{{#str}}unzip{{/str}}</button>
</div>
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle 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 3 of the License, or
+ (at your option) any later version.
+
+ Moodle 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.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+ @template theme_boost/flat_navigation
+
+ Display the flat navigation for the boost theme
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Context variables required for this template:
+ * flatnavigation - array of flat_navigation_nodes
+ * showdivider - boolean
+ * action - string
+ * isactive - boolean
+ * get_indent - integer
+ * is_section - boolean
+ * text - HTML
+
+ Example context (json):
+ {
+ "flatnavigation" : [
+ {
+ "showdivider": false,
+ "action": "#",
+ "isactive": true,
+ "get_indent": 1,
+ "is_section": false,
+ "text": "First"
+ },{
+ "showdivider": true,
+ "action": "#",
+ "isactive": false,
+ "get_indent": 0,
+ "is_section": true,
+ "text": "Last & Second"
+ }
+ ]
+ }
+}}
<nav class="list-group">
{{# flatnavigation }}
{{#showdivider}}
</nav>
<nav class="list-group m-t-1">
{{/showdivider}}
- {{#get_smscreenonly}}
- <div class="hidden-lg-up">
- {{/get_smscreenonly}}
{{#action}}
- <a class="list-group-item list-group-item-action {{#isactive}}font-weight-bold{{/isactive}}" href="{{{action}}}"><span class="m-l-{{get_indent}}">
- {{#is_section}}
- <div class="media">
- <span class="media-left">
- {{#pix}}i/folder{{/pix}}
- </span>
- <span class="media-body">{{text}}</span>
- </div>
- {{/is_section}}
- {{^is_section}}
- {{text}}
- {{/is_section}}
- </span></a>
+ <a class="list-group-item list-group-item-action {{#isactive}}font-weight-bold{{/isactive}}" href="{{{action}}}">
+ <div class="m-l-{{get_indent}}">
+ {{#is_section}}
+ <div class="media">
+ <span class="media-left">
+ {{#pix}}i/folder{{/pix}}
+ </span>
+ <span class="media-body">{{{text}}}</span>
+ </div>
+ {{/is_section}}
+ {{^is_section}}
+ {{{text}}}
+ {{/is_section}}
+ </div>
+ </a>
{{/action}}
{{^action}}
- <div class="list-group-item"><span class="m-l-{{get_indent}}">
- {{#is_section}}
- {{#pix}}i/folder{{/pix}}
- {{/is_section}}
- {{text}}
- </span></div>
- {{/action}}
- {{#get_smscreenonly}}
+ <div class="list-group-item">
+ <div class="m-l-{{get_indent}}">
+ {{#is_section}}
+ {{#pix}}i/folder{{/pix}}
+ {{/is_section}}
+ {{{text}}}
+ </div>
</div>
- {{/get_smscreenonly}}
+ {{/action}}
{{/ flatnavigation }}
</nav>
+// Bootstrap 4 compatibility for bootstrapbased themes.
+// Disable some stylelint rules which are necessary these compatbility classses.
+/* stylelint-disable declaration-no-important, block-no-empty */
// Add support for card. We map it to well.
// This is not meant to be perfect - but at least applies some styles to a bs4 card.
.card:extend(.well) {
- /* stylelint-disable-line no-empty-source */
}
-// Padding and margin utitilies.
-.padding-margin(3);
+// Due to lessphp limitations, the following classes must be declared statically without use of a loop.
+.p-l-1 {
+ padding-left: 1 * @baseFontSize !important;
+}
+.p-r-1 {
+ padding-right: 1 * @baseFontSize !important;
+}
+.p-t-1 {
+ padding-top: 1 * @baseFontSize !important;
+}
+.p-b-1 {
+ padding-bottom: 1 * @baseFontSize !important;
+}
+.p-x-1 {
+ padding-left: 1 * @baseFontSize !important;
+ padding-right: 1 * @baseFontSize !important;
+}
+.p-y-1 {
+ padding-top: 1 * @baseFontSize !important;
+ padding-bottom: 1 * @baseFontSize !important;
+}
+.p-a-1 {
+ padding: 1 * @baseFontSize !important;
+}
-.padding-margin(@n, @i : 1) when (@i =< @n) {
- @amount: @i * @baseFontSize;
- .p-l-@{i} {
- padding-left: @amount !important; /* stylelint-disable-line declaration-no-important */
- }
- .p-r-@{i} {
- padding-right: @amount !important; /* stylelint-disable-line declaration-no-important */
- }
- .p-t-@{i} {
- padding-top: @amount !important; /* stylelint-disable-line declaration-no-important */
- }
- .p-b-@{i} {
- padding-bottom: @amount !important; /* stylelint-disable-line declaration-no-important */
- }
- .p-x-@{i} {
- padding-left: @amount !important; /* stylelint-disable-line declaration-no-important */
- padding-right: @amount !important; /* stylelint-disable-line declaration-no-important */
- }
- .p-y-@{i} {
- padding-top: @amount !important; /* stylelint-disable-line declaration-no-important */
- padding-bottom: @amount !important; /* stylelint-disable-line declaration-no-important */
- }
- .p-a-@{i} {
- padding: @amount !important; /* stylelint-disable-line declaration-no-important */
- }
+.m-l-1 {
+ margin-left: 1 * @baseFontSize !important;
+}
+.m-r-1 {
+ margin-right: 1 * @baseFontSize !important;
+}
+.m-t-1 {
+ margin-top: 1 * @baseFontSize !important;
+}
+.m-b-1 {
+ margin-bottom: 1 * @baseFontSize !important;
+}
+.m-x-1 {
+ margin-left: 1 * @baseFontSize !important;
+ margin-right: 1 * @baseFontSize !important;
+}
+.m-y-1 {
+ margin-top: 1 * @baseFontSize !important;
+ margin-bottom: 1 * @baseFontSize !important;
+}
+.m-a-1 {
+ margin: 1 * @baseFontSize !important;
+}
- .m-l-@{i} {
- margin-left: @amount !important; /* stylelint-disable-line declaration-no-important */
- }
- .m-r-@{i} {
- margin-right: @amount !important; /* stylelint-disable-line declaration-no-important */
- }
- .m-t-@{i} {
- margin-top: @amount !important; /* stylelint-disable-line declaration-no-important */
- }
- .m-b-@{i} {
- margin-bottom: @amount !important; /* stylelint-disable-line declaration-no-important */
- }
- .m-x-@{i} {
- margin-left: @amount !important; /* stylelint-disable-line declaration-no-important */
- margin-right: @amount !important; /* stylelint-disable-line declaration-no-important */
- }
- .m-y-@{i} {
- margin-top: @amount !important; /* stylelint-disable-line declaration-no-important */
- margin-bottom: @amount !important; /* stylelint-disable-line declaration-no-important */
- }
- .m-a-@{i} {
- margin: @amount !important; /* stylelint-disable-line declaration-no-important */
- }
- .padding-margin(@n, (@i + 1));
+.p-l-2 {
+ padding-left: 2 * @baseFontSize !important;
+}
+.p-r-2 {
+ padding-right: 2 * @baseFontSize !important;
+}
+.p-t-2 {
+ padding-top: 2 * @baseFontSize !important;
+}
+.p-b-2 {
+ padding-bottom: 2 * @baseFontSize !important;
+}
+.p-x-2 {
+ padding-left: 2 * @baseFontSize !important;
+ padding-right: 2 * @baseFontSize !important;
+}
+.p-y-2 {
+ padding-top: 2 * @baseFontSize !important;
+ padding-bottom: 2 * @baseFontSize !important;
+}
+.p-a-2 {
+ padding: 2 * @baseFontSize !important;
+}
+
+.m-l-2 {
+ margin-left: 2 * @baseFontSize !important;
+}
+.m-r-2 {
+ margin-right: 2 * @baseFontSize !important;
+}
+.m-t-2 {
+ margin-top: 2 * @baseFontSize !important;
+}
+.m-b-2 {
+ margin-bottom: 2 * @baseFontSize !important;
+}
+.m-x-2 {
+ margin-left: 2 * @baseFontSize !important;
+ margin-right: 2 * @baseFontSize !important;
+}
+.m-y-2 {
+ margin-top: 2 * @baseFontSize !important;
+ margin-bottom: 2 * @baseFontSize !important;
+}
+.m-a-2 {
+ margin: 2 * @baseFontSize !important;
+}
+
+.p-l-3 {
+ padding-left: 3 * @baseFontSize !important;
+}
+.p-r-3 {
+ padding-right: 3 * @baseFontSize !important;
+}
+.p-t-3 {
+ padding-top: 3 * @baseFontSize !important;
+}
+.p-b-3 {
+ padding-bottom: 3 * @baseFontSize !important;
+}
+.p-x-3 {
+ padding-left: 3 * @baseFontSize !important;
+ padding-right: 3 * @baseFontSize !important;
+}
+.p-y-3 {
+ padding-top: 3 * @baseFontSize !important;
+ padding-bottom: 3 * @baseFontSize !important;
+}
+.p-a-3 {
+ padding: 3 * @baseFontSize !important;
+}
+
+.m-l-3 {
+ margin-left: 3 * @baseFontSize !important;
+}
+.m-r-3 {
+ margin-right: 3 * @baseFontSize !important;
+}
+.m-t-3 {
+ margin-top: 3 * @baseFontSize !important;
+}
+.m-b-3 {
+ margin-bottom: 3 * @baseFontSize !important;
+}
+.m-x-3 {
+ margin-left: 3 * @baseFontSize !important;
+ margin-right: 3 * @baseFontSize !important;
+}
+.m-y-3 {
+ margin-top: 3 * @baseFontSize !important;
+ margin-bottom: 3 * @baseFontSize !important;
+}
+.m-a-3 {
+ margin: 3 * @baseFontSize !important;
}
.d-inline {
- display: inline !important; /* stylelint-disable-line declaration-no-important */
+ display: inline !important;
}
.d-block {
- display: block !important; /* stylelint-disable-line declaration-no-important */
+ display: block !important;
}
.d-inline-block {
- display: inline-block !important; /* stylelint-disable-line declaration-no-important */
+ display: inline-block !important;
}
// Horizontal centering.
.m-x-auto {
- margin-left: auto !important; /* stylelint-disable-line declaration-no-important */
- margin-right: auto !important; /* stylelint-disable-line declaration-no-important */
+ margin-left: auto !important;
+ margin-right: auto !important;
}
// Support img-fluid.
.img-fluid:extend(.img-responsive) {
- /* stylelint-disable-line no-empty-source */
}
// Text danger
.text-danger:extend(.text-error) {
- /* stylelint-disable-line no-empty-source */
}
.btn-secondary {
// Floats
.pull-xs-left:extend(.pull-left) {
- /* stylelint-disable-line no-empty-source */
}
.pull-xs-right:extend(.pull-right) {
- /* stylelint-disable-line no-empty-source */
}
.text-nowrap {
- white-space: nowrap !important; /* stylelint-disable-line declaration-no-important */
+ white-space: nowrap !important;
}
}
}
-.user-enroller-panel {
- .uep-search-results {
- .user,
- .cohort {
- .options {
- .enrol {
- .btn-mini
- }
- }
- }
- }
-}
box-sizing: border-box;
}
+.popover-region-footer-container {
+ height: 30px;
+ text-align: center;
+ border-top: 1px solid #ddd;
+ background-color: @popoverBackground;
+
+ .popover-region-seeall-text {
+ padding-top: 4px;
+ }
+}
+
.popover-region-header-text {
float: left;
margin: 0;
height: 12px;
width: 12px;
}
+ .newmessage-link {
+ margin-right: 10px;
+ }
label {
display: inline-block;
text-align: center;
}
.popover-region-content-container {
- height: ~"calc(100% - 25px)";
+ height: ~"calc(100% - 56px)";
width: 100%;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
.popover-region-header-actions {
float: left;
+
+ .newmessage-link {
+ margin-left: 10px;
+ }
}
}
border-bottom: 1px solid #ddd;
box-sizing: border-box;
}
+.popover-region-footer-container {
+ height: 30px;
+ text-align: center;
+ border-top: 1px solid #ddd;
+ background-color: #fff;
+}
+.popover-region-footer-container .popover-region-seeall-text {
+ padding-top: 4px;
+}
.popover-region-header-text {
float: left;
margin: 0;
height: 12px;
width: 12px;
}
+.popover-region-header-actions .newmessage-link {
+ margin-right: 10px;
+}
.popover-region-header-actions label {
display: inline-block;
text-align: center;
margin-bottom: 0;
}
.popover-region-content-container {
- height: calc(100% - 25px);
+ height: calc(100% - 56px);
width: 100%;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
.dir-rtl .popover-region .popover-region-header-actions {
float: left;
}
+.dir-rtl .popover-region .popover-region-header-actions .newmessage-link {
+ margin-left: 10px;
+}
.dir-rtl .navbar .popover-region {
float: left;
}
form.search-cohort input[type="submit"],
form.search-cohort label {
margin-top: 14px !important;
- /* stylelint-disable-line declaration-no-important */
}
form.search-cohort label {
margin-bottom: 0;
button.yui3-button.closebutton:hover {
background-position: 0 0;
}
-.user-enroller-panel .uep-search-results .user .options .enrol,
-.user-enroller-panel .uep-search-results .cohort .options .enrol {
- padding: 0 6px;
- font-size: 10.5px;
- -webkit-border-radius: 3px;
- -moz-border-radius: 3px;
- border-radius: 3px;
-}
-.user-enroller-panel .uep-search-results .user .options .enrol .label,
-.user-enroller-panel .uep-search-results .cohort .options .enrol .label,
-.user-enroller-panel .uep-search-results .user .options .enrol .badge,
-.user-enroller-panel .uep-search-results .cohort .options .enrol .badge {
- top: 0;
-}
.gradetreebox h4 {
font-size: 14px;
}
ul.indented-list {
margin-left: 20px;
}
-.card {
- /* stylelint-disable-line no-empty-source */
-}
+/* stylelint-disable declaration-no-important, block-no-empty */
.p-l-1 {
padding-left: 14px !important;
- /* stylelint-disable-line declaration-no-important */
}
.p-r-1 {
padding-right: 14px !important;
- /* stylelint-disable-line declaration-no-important */
}
.p-t-1 {
padding-top: 14px !important;
- /* stylelint-disable-line declaration-no-important */
}
.p-b-1 {
padding-bottom: 14px !important;
- /* stylelint-disable-line declaration-no-important */
}
.p-x-1 {
padding-left: 14px !important;
- /* stylelint-disable-line declaration-no-important */
padding-right: 14px !important;
- /* stylelint-disable-line declaration-no-important */
}
.p-y-1 {
padding-top: 14px !important;
- /* stylelint-disable-line declaration-no-important */
padding-bottom: 14px !important;
- /* stylelint-disable-line declaration-no-important */
}
.p-a-1 {
padding: 14px !important;
- /* stylelint-disable-line declaration-no-important */
}
.m-l-1 {
margin-left: 14px !important;
- /* stylelint-disable-line declaration-no-important */
}
.m-r-1 {
margin-right: 14px !important;
- /* stylelint-disable-line declaration-no-important */
}
.m-t-1 {
margin-top: 14px !important;
- /* stylelint-disable-line declaration-no-important */
}
.m-b-1 {
margin-bottom: 14px !important;
- /* stylelint-disable-line declaration-no-important */
}
.m-x-1 {
margin-left: 14px !important;
- /* stylelint-disable-line declaration-no-important */
margin-right: 14px !important;
- /* stylelint-disable-line declaration-no-important */
}
.m-y-1 {
margin-top: 14px !important;
- /* stylelint-disable-line declaration-no-important */
margin-bottom: 14px !important;
- /* stylelint-disable-line declaration-no-important */
}
.m-a-1 {
margin: 14px !important;
- /* stylelint-disable-line declaration-no-important */
}
.p-l-2 {
padding-left: 28px !important;
- /* stylelint-disable-line declaration-no-important */
}
.p-r-2 {
padding-right: 28px !important;
- /* stylelint-disable-line declaration-no-important */
}
.p-t-2 {
padding-top: 28px !important;
- /* stylelint-disable-line declaration-no-important */
}
.p-b-2 {
padding-bottom: 28px !important;
- /* stylelint-disable-line declaration-no-important */
}
.p-x-2 {
padding-left: 28px !important;
- /* stylelint-disable-line declaration-no-important */
padding-right: 28px !important;
- /* stylelint-disable-line declaration-no-important */
}
.p-y-2 {
padding-top: 28px !important;
- /* stylelint-disable-line declaration-no-important */
padding-bottom: 28px !important;
- /* stylelint-disable-line declaration-no-important */
}
.p-a-2 {
padding: 28px !important;
- /* stylelint-disable-line declaration-no-important */
}
.m-l-2 {
margin-left: 28px !important;
- /* stylelint-disable-line declaration-no-important */
}
.m-r-2 {
margin-right: 28px !important;
- /* stylelint-disable-line declaration-no-important */
}
.m-t-2 {
margin-top: 28px !important;
- /* stylelint-disable-line declaration-no-important */
}
.m-b-2 {
margin-bottom: 28px !important;
- /* stylelint-disable-line declaration-no-important */
}
.m-x-2 {
margin-left: 28px !important;
- /* stylelint-disable-line declaration-no-important */
margin-right: 28px !important;
- /* stylelint-disable-line declaration-no-important */
}
.m-y-2 {
margin-top: 28px !important;
- /* stylelint-disable-line declaration-no-important */
margin-bottom: 28px !important;
- /* stylelint-disable-line declaration-no-important */
}
.m-a-2 {
margin: 28px !important;
- /* stylelint-disable-line declaration-no-important */
}
.p-l-3 {
padding-left: 42px !important;
- /* stylelint-disable-line declaration-no-important */
}
.p-r-3 {
padding-right: 42px !important;
- /* stylelint-disable-line declaration-no-important */
}
.p-t-3 {
padding-top: 42px !important;
- /* stylelint-disable-line declaration-no-important */
}
.p-b-3 {
padding-bottom: 42px !important;
- /* stylelint-disable-line declaration-no-important */
}
.p-x-3 {
padding-left: 42px !important;
- /* stylelint-disable-line declaration-no-important */
padding-right: 42px !important;
- /* stylelint-disable-line declaration-no-important */
}
.p-y-3 {
padding-top: 42px !important;
- /* stylelint-disable-line declaration-no-important */
padding-bottom: 42px !important;
- /* stylelint-disable-line declaration-no-important */
}
.p-a-3 {
padding: 42px !important;
- /* stylelint-disable-line declaration-no-important */
}
.m-l-3 {
margin-left: 42px !important;
- /* stylelint-disable-line declaration-no-important */
}
.m-r-3 {
margin-right: 42px !important;
- /* stylelint-disable-line declaration-no-important */
}
.m-t-3 {
margin-top: 42px !important;
- /* stylelint-disable-line declaration-no-important */
}
.m-b-3 {
margin-bottom: 42px !important;
- /* stylelint-disable-line declaration-no-important */
}
.m-x-3 {
margin-left: 42px !important;
- /* stylelint-disable-line declaration-no-important */
margin-right: 42px !important;
- /* stylelint-disable-line declaration-no-important */
}
.m-y-3 {
margin-top: 42px !important;
- /* stylelint-disable-line declaration-no-important */
margin-bottom: 42px !important;
- /* stylelint-disable-line declaration-no-important */
}
.m-a-3 {
margin: 42px !important;
- /* stylelint-disable-line declaration-no-important */
}
.d-inline {
display: inline !important;
- /* stylelint-disable-line declaration-no-important */
}
.d-block {
display: block !important;
- /* stylelint-disable-line declaration-no-important */
}
.d-inline-block {
display: inline-block !important;
- /* stylelint-disable-line declaration-no-important */
}
.m-x-auto {
margin-left: auto !important;
- /* stylelint-disable-line declaration-no-important */
margin-right: auto !important;
- /* stylelint-disable-line declaration-no-important */
-}
-.img-fluid {
- /* stylelint-disable-line no-empty-source */
-}
-.text-danger {
- /* stylelint-disable-line no-empty-source */
}
.btn-secondary {
background-color: #fff;
.tag-primary[href] {
background-color: #0378a9;
}
-.pull-xs-left {
- /* stylelint-disable-line no-empty-source */
-}
-.pull-xs-right {
- /* stylelint-disable-line no-empty-source */
-}
.text-nowrap {
white-space: nowrap !important;
- /* stylelint-disable-line declaration-no-important */
}
/* Custom CSS
-------------------------*/
div.logo {
- background: url([[setting:logo]]) no-repeat 0 0;
+ background: url([[setting:logo]]) 0 0 no-repeat;
display: block;
float: left;
height: 75px;
/* Custom CSS
-------------------------*/
div.logo {
- background: url([[setting:logo]]) no-repeat 0 0;
+ background: url([[setting:logo]]) 0 0 no-repeat;
display: block;
float: left;
height: 75px;
defined('MOODLE_INTERNAL') || die();
-$version = 2016111500.00; // YYYYMMDD = weekly release date of this DEV branch.
+$version = 2016111600.00; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
-$release = '3.2beta+ (Build: 20161115)'; // Human-friendly version name
+$release = '3.2beta+ (Build: 20161116)'; // Human-friendly version name
$branch = '32'; // This version's branch.
$maturity = MATURITY_BETA; // This version's maturity level.