),
'idnumber' => array(
'type' => PARAM_RAW, // ID numbers are plain text.
- 'default' => ''
+ 'default' => '',
+ 'null' => NULL_ALLOWED
),
'visible' => array(
'type' => PARAM_BOOL,
use moodle_exception;
/**
- * API exposed by tool_mobile
+ * API exposed by tool_mobile, to be used mostly by external functions.
*
* @copyright 2016 Juan Leyva
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
// We need this to make work the format text functions.
$PAGE->set_context($context);
+ list($authinstructions, $notusedformat) = external_format_text($CFG->auth_instructions, FORMAT_MOODLE, $context->id);
+ list($maintenancemessage, $notusedformat) = external_format_text($CFG->maintenance_message, FORMAT_MOODLE, $context->id);
$settings = array(
'wwwroot' => $CFG->wwwroot,
'httpswwwroot' => $CFG->httpswwwroot,
'authloginviaemail' => $CFG->authloginviaemail,
'registerauth' => $CFG->registerauth,
'forgottenpasswordurl' => $CFG->forgottenpasswordurl,
- 'authinstructions' => format_text($CFG->auth_instructions),
+ 'authinstructions' => $authinstructions,
'authnoneenabled' => (int) is_enabled_auth('none'),
'enablewebservices' => $CFG->enablewebservices,
'enablemobilewebservice' => $CFG->enablemobilewebservice,
'maintenanceenabled' => $CFG->maintenance_enabled,
- 'maintenancemessage' => format_text($CFG->maintenance_message),
+ 'maintenancemessage' => $maintenancemessage,
);
$typeoflogin = get_config('tool_mobile', 'typeoflogin');
if (empty($section) or $section == 'frontpagesettings') {
require_once($CFG->dirroot . '/course/format/lib.php');
// First settings that anyone can deduce.
- $settings->fullname = $SITE->fullname;
- $settings->shortname = $SITE->shortname;
- $settings->summary = $SITE->summary;
+ $settings->fullname = external_format_string($SITE->fullname, $context->id);
+ $settings->shortname = external_format_string($SITE->shortname, $context->id);
+ list($settings->summary, $settings->summaryformat) = external_format_text($SITE->summary, $SITE->summaryformat,
+ $context->id);
$settings->frontpage = $CFG->frontpage;
$settings->frontpageloggedin = $CFG->frontpageloggedin;
$settings->maxcategorydepth = $CFG->maxcategorydepth;
// Test default values.
$context = context_system::instance();
+ list($authinstructions, $notusedformat) = external_format_text($CFG->auth_instructions, FORMAT_MOODLE, $context->id);
+ list($maintenancemessage, $notusedformat) = external_format_text($CFG->maintenance_message, FORMAT_MOODLE, $context->id);
+
$expected = array(
'wwwroot' => $CFG->wwwroot,
'httpswwwroot' => $CFG->httpswwwroot,
'authloginviaemail' => $CFG->authloginviaemail,
'registerauth' => $CFG->registerauth,
'forgottenpasswordurl' => $CFG->forgottenpasswordurl,
- 'authinstructions' => format_text($CFG->auth_instructions),
+ 'authinstructions' => $authinstructions,
'authnoneenabled' => (int) is_enabled_auth('none'),
'enablewebservices' => $CFG->enablewebservices,
'enablemobilewebservice' => $CFG->enablemobilewebservice,
'maintenanceenabled' => $CFG->maintenance_enabled,
- 'maintenancemessage' => format_text($CFG->maintenance_message),
+ 'maintenancemessage' => $maintenancemessage,
'typeoflogin' => api::LOGIN_VIA_APP,
'warnings' => array()
);
set_config('logo', 'mock.png', 'core_admin');
set_config('logocompact', 'mock.png', 'core_admin');
+ list($authinstructions, $notusedformat) = external_format_text($authinstructions, FORMAT_MOODLE, $context->id);
$expected['registerauth'] = 'email';
- $expected['authinstructions'] = format_text($authinstructions);
+ $expected['authinstructions'] = $authinstructions;
$expected['typeoflogin'] = api::LOGIN_VIA_BROWSER;
$expected['launchurl'] = "$CFG->wwwroot/$CFG->admin/tool/mobile/launch.php";
$result = external::get_config();
$result = external_api::clean_returnvalue(external::get_config_returns(), $result);
+ // SITE summary is null in phpunit which gets transformed to an empty string by format_text.
+ list($sitesummary, $unused) = external_format_text($SITE->summary, $SITE->summaryformat, context_system::instance()->id);
+
// Test default values.
$context = context_system::instance();
$expected = array(
array('name' => 'fullname', 'value' => $SITE->fullname),
array('name' => 'shortname', 'value' => $SITE->shortname),
- array('name' => 'summary', 'value' => $SITE->summary),
+ array('name' => 'summary', 'value' => $sitesummary),
+ array('name' => 'summaryformat', 'value' => FORMAT_HTML),
array('name' => 'frontpage', 'value' => $CFG->frontpage),
array('name' => 'frontpageloggedin', 'value' => $CFG->frontpageloggedin),
array('name' => 'maxcategorydepth', 'value' => $CFG->maxcategorydepth),
// Change a value and retrieve filtering by section.
set_config('commentsperpage', 1);
- $expected[9]['value'] = 1;
- unset($expected[10]);
+ $expected[10]['value'] = 1;
unset($expected[11]);
+ unset($expected[12]);
$result = external::get_config('frontpagesettings');
$result = external_api::clean_returnvalue(external::get_config_returns(), $result);
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();
$this->content->text .= "\n".'<form class="loginform" id="login" method="post" action="'.get_login_url().'" '.$autocomplete.'>';
- $this->content->text .= '<div class="c1 fld username"><label for="login_username">'.$strusername.'</label>';
- $this->content->text .= '<input type="text" name="username" id="login_username" value="'.s($username).'" /></div>';
+ $this->content->text .= '<div class="form-group"><label for="login_username">'.$strusername.'</label>';
+ $this->content->text .= '<input type="text" name="username" id="login_username" class="form-control" value="'.s($username).'" /></div>';
- $this->content->text .= '<div class="c1 fld password"><label for="login_password">'.get_string('password').'</label>';
+ $this->content->text .= '<div class="form-group"><label for="login_password">'.get_string('password').'</label>';
- $this->content->text .= '<input type="password" name="password" id="login_password" value="" '.$autocomplete.' /></div>';
+ $this->content->text .= '<input type="password" name="password" id="login_password" class="form-control" value="" '.$autocomplete.' /></div>';
if (isset($CFG->rememberusername) and $CFG->rememberusername == 2) {
$checked = $username ? 'checked="checked"' : '';
- $this->content->text .= '<div class="c1 rememberusername"><input type="checkbox" name="rememberusername" id="rememberusername" value="1" '.$checked.'/>';
- $this->content->text .= ' <label for="rememberusername">'.get_string('rememberusername', 'admin').'</label></div>';
+ $this->content->text .= '<div class="form-check-inline">';
+ $this->content->text .= '<input type="checkbox" name="rememberusername" id="rememberusername" class="form-check-input" value="1" '.$checked.'/>';
+ $this->content->text .= ' <label for="rememberusername" class="form-check-label">'.get_string('rememberusername', 'admin').'</label>';
+ $this->content->text .= '</div>';
}
- $this->content->text .= '<div class="c1 btn"><input type="submit" value="'.get_string('login').'" /></div>';
+ $this->content->text .= '<div class="form-group">';
+ $this->content->text .= '<input type="submit" class="btn btn-primary btn-block" value="'.get_string('login').'" />';
+ $this->content->text .= '</div>';
$this->content->text .= "</form>\n";
"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);
ELSE 0 END) AS timeend
FROM {user_enrolments} pue
JOIN {enrol} pe ON (pe.id = pue.enrolid AND pe.enrol <> 'meta' AND pe.enrol $enabled)
- JOIN {enrol} e ON (e.customint1 = pe.courseid AND e.enrol = 'meta' $onecourse)
+ JOIN {enrol} e ON (e.customint1 = pe.courseid AND e.enrol = 'meta' AND e.status = :enrolstatus $onecourse)
JOIN {user} u ON (u.id = pue.userid AND u.deleted = 0)
LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = pue.userid)
WHERE ue.id IS NULL
GROUP BY pue.userid, e.id";
+ $params['enrolstatus'] = ENROL_INSTANCE_ENABLED;
$rs = $DB->get_recordset_sql($sql, $params);
foreach($rs as $ue) {
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';
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+$string['activityisscheduledfordeletion'] = 'Activity deletion in progress...';
$string['authnotexisting'] = 'The autorization plugin doesn\'t exist';
$string['backupcontainexternal'] = 'This backup file contains external Moodle Network Hosts that are not configured locally';
$string['backuptablefail'] = 'Backup tables could NOT be set up successfully!';
$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;
}
* @return string
*/
function min_fix_utf8($value) {
- // Lower error reporting because glibc throws bogus notices.
- $olderror = error_reporting();
- if ($olderror & E_NOTICE) {
- error_reporting($olderror ^ E_NOTICE);
- }
-
// No null bytes expected in our data, so let's remove it.
$value = str_replace("\0", '', $value);
static $buggyiconv = null;
if ($buggyiconv === null) {
+ set_error_handler(function () {
+ return true;
+ });
$buggyiconv = (!function_exists('iconv') or iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
+ restore_error_handler();
}
if ($buggyiconv) {
}
} else {
- $result = iconv('UTF-8', 'UTF-8//IGNORE', $value);
- }
-
- if ($olderror & E_NOTICE) {
- error_reporting($olderror);
+ $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
}
return $result;
$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;
* @return mixed Void, exit, and die depending on path
* @throws coding_exception
* @throws require_login_exception
+ * @throws moodle_exception
*/
function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
// Make sure the USER has a sesskey set up. Used for CSRF protection.
sesskey();
- // Do not bother admins with any formalities.
- if (is_siteadmin()) {
+ // Do not bother admins with any formalities, except for activities pending deletion.
+ if (is_siteadmin() && !($cm && $cm->deletioninprogress)) {
// Set the global $COURSE.
if ($cm) {
$PAGE->set_cm($cm, $course);
}
}
+ // Check whether the activity has been scheduled for deletion. If so, then deny access, even for admins.
+ if ($cm && $cm->deletioninprogress) {
+ if ($preventredirect) {
+ throw new moodle_exception('activityisscheduledfordeletion');
+ }
+ require_once($CFG->dirroot . '/course/lib.php');
+ redirect(course_get_url($course), get_string('activityisscheduledfordeletion', 'error'));
+ }
+
// Check visibility of activity to current user; includes visible flag, conditional availability, etc.
if ($cm && !$cm->uservisible) {
if ($preventredirect) {
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;
}
$options[] = array($group => $opts);
}
echo html_writer::label(get_string('questioncategory', 'core_question'), 'id_movetocategory', false, array('class' => 'accesshide'));
- echo html_writer::select($options, 'category', $selected, $choose, array('id' => 'id_movetocategory'));
+ $attrs = array('id' => 'id_movetocategory', 'class' => 'custom-select');
+ echo html_writer::select($options, 'category', $selected, $choose, $attrs);
}
/**
}
// Acceptance tests needs special output to capture the errors,
-// but not necessary for behat CLI command.
-if (defined('BEHAT_SITE_RUNNING') && !defined('BEHAT_TEST')) {
+// but not necessary for behat CLI command and init script.
+if (defined('BEHAT_SITE_RUNNING') && !defined('BEHAT_TEST') && !defined('BEHAT_UTIL')) {
require_once(__DIR__ . '/behat/lib.php');
set_error_handler('behat_error_handler', E_ALL | E_STRICT);
}
</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));
}
/**
private $_header='';
private $_body='';
private $_connection_closed = false;
- private $_maxheaderlenth = 1000;
+ private $_maxheaderlenth = 65536;
private $_digestchallenge = null;
private $_cnonce = '';
private $_nc = 0;
* 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.
*/
}
// Copy data from notify students checkbox which was moved out of the form.
- var checked = $('[data-region="grading-actions-form"] [name="sendstudentnotifications"]').val();
+ var checked = $('[data-region="grading-actions-form"] [name="sendstudentnotifications"]').prop("checked");
$('.gradeform [name="sendstudentnotifications"]').val(checked);
};
* 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
if ($current->grade !== null) {
$current->grade = floatval($current->grade);
}
- $gradechanged = $gradecolpresent && $current->grade !== $modified->grade;
+ $gradechanged = $gradecolpresent && grade_floats_different($current->grade, $modified->grade);
$markingallocationchanged = $this->get_instance()->markingworkflow &&
$this->get_instance()->markingallocation &&
($modified->allocatedmarker !== false) &&
<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;
$output = '';
$output .= html_writer::start_div('maxgrade');
$output .= html_writer::start_tag('form', array('method' => 'post', 'action' => 'edit.php',
- 'class' => 'quizsavegradesform'));
+ 'class' => 'quizsavegradesform form-inline'));
$output .= html_writer::start_tag('fieldset', array('class' => 'invisiblefieldset'));
$output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
$output .= html_writer::input_hidden_params($pageurl);
$a = html_writer::empty_tag('input', array('type' => 'text', 'id' => 'inputmaxgrade',
'name' => 'maxgrade', 'size' => ($structure->get_decimal_places_for_grades() + 2),
- 'value' => $structure->formatted_quiz_grade()));
+ 'value' => $structure->formatted_quiz_grade(),
+ 'class' => 'form-control'));
$output .= html_writer::tag('label', get_string('maximumgradex', '', $a),
array('for' => 'inputmaxgrade'));
- $output .= html_writer::empty_tag('input', array('type' => 'submit',
+ $output .= html_writer::empty_tag('input', array('type' => 'submit', 'class' => 'btn btn-secondary m-l-1',
'name' => 'savechanges', 'value' => get_string('save', 'quiz')));
$output .= html_writer::end_tag('fieldset');
$output .= html_writer::end_tag('form');
'name' => 'repaginate',
'id' => 'repaginatecommand',
'value' => get_string('repaginatecommand', 'quiz'),
+ 'class' => 'btn btn-secondary m-b-1',
);
if (!$structure->can_be_repaginated()) {
$buttonoptions['disabled'] = 'disabled';
$hiddenurl->param('sesskey', sesskey());
$select = html_writer::select($perpage, 'questionsperpage',
- $structure->get_questions_per_page(), false);
+ $structure->get_questions_per_page(), false, array('class' => 'custom-select'));
- $buttonattributes = array('type' => 'submit', 'name' => 'repaginate', 'value' => get_string('go'));
+ $buttonattributes = array(
+ 'type' => 'submit',
+ 'name' => 'repaginate',
+ 'value' => get_string('go'),
+ 'class' => 'btn btn-secondary m-l-1'
+ );
$formcontent = html_writer::tag('form', html_writer::div(
html_writer::input_hidden_params($hiddenurl) .
}
$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 />';
}
'value' => sesskey()));
$output .= html_writer::start_tag('div', array('class' => 'submitbtns'));
$output .= html_writer::empty_tag('input', array('type' => 'submit',
- 'class' => 'questionflagsavebutton', 'name' => 'savingflags',
+ 'class' => 'questionflagsavebutton btn btn-secondary', 'name' => 'savingflags',
'value' => get_string('saveflags', 'question')));
$output .= html_writer::end_tag('div');
$output .= html_writer::end_tag('div');
return html_writer::empty_tag('input', array('type' => 'button',
'value' => get_string('finishreview', 'quiz'),
'id' => 'secureclosebutton',
- 'class' => 'mod_quiz-next-nav'));
+ 'class' => 'mod_quiz-next-nav btn btn-primary'));
} else {
return html_writer::link($url, get_string('finishreview', 'quiz'),
* @return string HTML fragment.
*/
protected function render_quiz_nav_question_button(quiz_nav_question_button $button) {
- $classes = array('qnbutton', $button->stateclass, $button->navmethod);
+ $classes = array('qnbutton', $button->stateclass, $button->navmethod, 'btn', 'btn-secondary');
$extrainfo = array();
if ($button->currentpage) {
$output .= html_writer::start_tag('div', array('class' => 'submitbtns'));
if ($page > 0 && $navmethod == 'free') {
$output .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'previous',
- 'value' => get_string('navigateprevious', 'quiz'), 'class' => 'mod_quiz-prev-nav'));
+ 'value' => get_string('navigateprevious', 'quiz'), 'class' => 'mod_quiz-prev-nav btn btn-secondary'));
}
if ($lastpage) {
$nextlabel = get_string('endtest', 'quiz');
$nextlabel = get_string('navigatenext', 'quiz');
}
$output .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'next',
- 'value' => $nextlabel, 'class' => 'mod_quiz-next-nav'));
+ 'value' => $nextlabel, 'class' => 'mod_quiz-next-nav btn btn-primary'));
$output .= html_writer::end_tag('div');
return $output;
protected function submit_buttons() {
global $PAGE;
if (has_capability('mod/quiz:deleteattempts', $this->context)) {
- echo '<input type="submit" id="deleteattemptsbutton" name="delete" value="' .
+ echo '<input type="submit" class="btn btn-secondary m-r-1" id="deleteattemptsbutton" name="delete" value="' .
get_string('deleteselected', 'quiz_overview') . '"/>';
$PAGE->requires->event_handler('#deleteattemptsbutton', 'click', 'M.util.show_confirm_dialog',
array('message' => get_string('deleteattemptcheck', 'quiz')));
protected function submit_buttons() {
if (has_capability('mod/quiz:regrade', $this->context)) {
- echo '<input type="submit" name="regrade" value="' .
+ echo '<input type="submit" class="btn btn-secondary m-r-1" name="regrade" value="' .
get_string('regradeselected', 'quiz_overview') . '"/>';
}
parent::submit_buttons();
echo '<form action="'.$displayurl->out_omit_querystring().'">';
echo '<div>';
echo html_writer::input_hidden_params($displayurl);
- echo '<input type="submit" name="regradeall" value="'.$regradealllabel.'"/>';
- echo '<input type="submit" name="regradealldry" value="' .
+ echo '<input type="submit" class="btn btn-secondary" name="regradeall" value="'.$regradealllabel.'"/>';
+ echo '<input type="submit" class="btn btn-secondary m-l-1" name="regradealldry" value="' .
$regradealldrylabel . '"/>';
if ($regradesneeded) {
- echo '<input type="submit" name="regradealldrydo" value="' .
+ echo '<input type="submit" class="btn btn-secondary m-l-1" name="regradealldrydo" value="' .
$regradealldrydolabel . '"/>';
}
echo '</div>';
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',
echo \html_writer::start_div('choosecategory');
$catmenu = question_category_options($contexts, false, 0, true);
echo \html_writer::label(get_string('selectacategory', 'question'), 'id_selectacategory');
- echo \html_writer::select($catmenu, 'category', $current, array(), array('class' => 'searchoptions', 'id' => 'id_selectacategory'));
+ echo \html_writer::select($catmenu, 'category', $current, array(), array('class' => 'searchoptions custom-select', 'id' => 'id_selectacategory'));
echo \html_writer::end_div() . "\n";
}
// Print delete and move selected question.
if ($caneditall) {
- echo '<input type="submit" name="deleteselected" value="' . get_string('delete') . "\" />\n";
+ echo '<input type="submit" class="btn btn-secondary" name="deleteselected" value="' . get_string('delete') . "\" />\n";
}
if ($canmoveall && count($addcontexts)) {
- echo '<input type="submit" name="move" value="' . get_string('moveto', 'question') . "\" />\n";
+ echo '<input type="submit" class="btn btn-secondary" name="move" value="' . get_string('moveto', 'question') . "\" />\n";
question_category_select_menu($addcontexts, false, 0, "{$category->id},{$category->contextid}");
}
}
}
$menu1 = html_writer::label(get_string('lengthoption', 'qtype_calculated'),
'menucalclength', false, array('class' => 'accesshide'));
- $menu1 .= html_writer::select($lengthoptions, 'calclength[]', $regs[4], null);
+ $menu1 .= html_writer::select($lengthoptions, 'calclength[]', $regs[4], null, array('class' => 'custom-select'));
$options = array('uniform' => get_string('uniformbit', 'qtype_calculated'),
'loguniform' => get_string('loguniformbit', 'qtype_calculated'));
$menu2 = html_writer::label(get_string('distributionoption', 'qtype_calculated'),
'menucalcdistribution', false, array('class' => 'accesshide'));
- $menu2 .= html_writer::select($options, 'calcdistribution[]', $regs[1], null);
- return '<input type="submit" onclick="'
+ $menu2 .= html_writer::select($options, 'calcdistribution[]', $regs[1], null, array('class' => 'custom-select'));
+ return '<input type="submit" class="btn btn-secondary" onclick="'
. "getElementById('addform').regenerateddefid.value='{$defid}'; return true;"
.'" value="'. get_string('generatevalue', 'qtype_calculated') . '"/><br/>'
- . '<input type="text" size="3" name="calcmin[]" '
+ . '<input type="text" class="form-control" size="3" name="calcmin[]" '
. " value=\"{$regs[2]}\"/> & <input name=\"calcmax[]\" "
- . ' type="text" size="3" value="' . $regs[3] .'"/> '
+ . ' type="text" class="form-control" size="3" value="' . $regs[3] .'"/> '
. $menu1 . '<br/>'
. $menu2;
} else {
'<div class="markertexts"></div>' +
'<div class="droparea"></div>' +
'<div class="dropzones"></div>' +
- '<ul class="pager">' +
+ '<ul class="pager unstyled list-unstyled">' +
'<li><span id="xcoordpreview">X = </span></li>' +
'<li><span id="ycoordpreview">Y = </span></li>' +
'</ul>' +
$attributes = array(
'id' => $this->box_id($qa, 'p' . $place),
- 'class' => 'place' . $place,
+ 'class' => 'custom-select place' . $place,
);
$groupclass = 'group' . $group;
'menu' . $qa->get_qt_field_name('sub' . $key), false,
array('class' => 'accesshide')) .
html_writer::select($choices, $qa->get_qt_field_name('sub' . $key), $selected,
- array('0' => 'choose'), array('disabled' => $options->readonly)) .
+ array('0' => 'choose'), array('disabled' => $options->readonly, 'class' => 'custom-select m-l-1')) .
' ' . $feedbackimage, array('class' => $classes));
$result .= html_writer::end_tag('tr');
'value' => $response,
'id' => $qa->get_qt_field_name($fieldname),
'size' => $size,
+ 'class' => 'form-control',
);
if ($options->readonly) {
$inputattributes['readonly'] = 'readonly';
$feedbackimg = '';
if ($options->correctness) {
- $inputattributes['class'] = $this->feedback_class($matchinganswer->fraction);
+ $inputattributes['class'] .= ' ' . $this->feedback_class($matchinganswer->fraction);
$feedbackimg = $this->feedback_image($matchinganswer->fraction);
}
$qa, 'question', 'answerfeedback', $matchinganswer->id),
s($correctanswer->answer), $options);
- $output = html_writer::start_tag('span', array('class' => 'subquestion'));
+ $output = html_writer::start_tag('span', array('class' => 'subquestion form-inline'));
$output .= html_writer::tag('label', get_string('answer'),
array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
$output .= html_writer::empty_tag('input', $inputattributes);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_marked_out_of_summary(),
- new question_pattern_expectation('~<input[^>]* class="incorrect" [^>]*/>~'),
+ new question_pattern_expectation('~<input[^>]* class="[^"]*incorrect[^"]*" [^>]*/>~'),
$this->get_contains_subq_status(question_state::$gaveup),
$this->get_does_not_contain_validation_error_expectation());
}
$question->make_html_inline($question->format_text(
$ans->answer, $ans->answerformat,
$qa, 'question', 'answer', $ansid)),
- array('for' => $inputattributes['id']));
+ array('for' => $inputattributes['id'], 'class' => 'm-l-1'));
// Param $options->suppresschoicefeedback is a hack specific to the
// oumultiresponse question type. It would be good to refactor to
'value' => $currentanswer,
'id' => $inputname,
'size' => 80,
+ 'class' => 'form-control',
);
if ($options->readonly) {
} else {
$fraction = 0;
}
- $inputattributes['class'] = $this->feedback_class($fraction);
+ $inputattributes['class'] .= ' ' . $this->feedback_class($fraction);
$feedbackimg = $this->feedback_image($fraction);
}
$result = html_writer::tag('div', $questiontext, array('class' => 'qtext'));
if (!$placeholder) {
- $result .= html_writer::start_tag('div', array('class' => 'ablock'));
+ $result .= html_writer::start_tag('div', array('class' => 'ablock form-inline'));
$result .= html_writer::tag('label', get_string('answercolon', 'qtype_numerical'), array('for' => $inputattributes['id']));
$result .= html_writer::tag('span', $input, array('class' => 'answer'));
$result .= html_writer::end_tag('div');
'value' => $currentanswer,
'id' => $inputname,
'size' => 80,
+ 'class' => 'form-control',
);
if ($options->readonly) {
} else {
$fraction = 0;
}
- $inputattributes['class'] = $this->feedback_class($fraction);
+ $inputattributes['class'] .= ' ' . $this->feedback_class($fraction);
$feedbackimg = $this->feedback_image($fraction);
}
$result = html_writer::tag('div', $questiontext, array('class' => 'qtext'));
if (!$placeholder) {
- $result .= html_writer::start_tag('div', array('class' => 'ablock'));
+ $result .= html_writer::start_tag('div', array('class' => 'ablock form-inline'));
$result .= html_writer::tag('label', get_string('answer', 'qtype_shortanswer',
html_writer::tag('span', $input, array('class' => 'answer'))),
array('for' => $inputattributes['id']));
$radiotrue = html_writer::empty_tag('input', $trueattributes) .
html_writer::tag('label', get_string('true', 'qtype_truefalse'),
- array('for' => $trueattributes['id']));
+ array('for' => $trueattributes['id'], 'class' => 'm-l-1'));
$radiofalse = html_writer::empty_tag('input', $falseattributes) .
html_writer::tag('label', get_string('false', 'qtype_truefalse'),
- array('for' => $falseattributes['id']));
+ array('for' => $falseattributes['id'], 'class' => 'm-l-1'));
$result = '';
$result .= html_writer::tag('div', $question->format_questiontext($qa),
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.
}
// Quiz module
+.path-mod-quiz .mod-quiz-edit-content {
+ // Force the quiz edit page to always be large enough to prevent scrolling when accessing the menu.
+ margin-bottom: 10rem;
+}
+
+#page-mod-quiz-mod #id_reviewoptionshdr .col-md-3,
+#page-mod-quiz-mod #id_reviewoptionshdr .col-md-9 {
+ width: auto;
+}
+#page-mod-quiz-mod #id_reviewoptionshdr .form-group {
+ float: left;
+ width: 20rem;
+ display: inline-block;
+ height: 12rem;
+}
+
+#page-mod-quiz-mod #id_reviewoptionshdr .btn-link {
+ line-height: 1.5;
+ vertical-align: bottom;
+}
+#page-mod-quiz-mod #id_reviewoptionshdr .form-inline {
+ float: left;
+ clear: left;
+}
+#page-mod-quiz-mod #id_reviewoptionshdr .form-check-inline {
+ width: 90%;
+ height: 22px;
+}
// Question navigation block.
.path-mod-quiz #mod_quiz_navblock {
font-size: 14px;
line-height: 20px;
font-weight: normal;
- background-color: $body-color;
+ background-color: $card-bg;
background-image: none;
height: 40px;
width: 30px;
.qnbutton .trafficlight {
border: 0;
- background: $body-color none center / 10px no-repeat scroll;
+ background: $card-bg none center / 10px no-repeat scroll;
height: 20px;
margin-top: 20px;
border-radius: 0 0 3px 3px;
.qnbutton.notyetanswered .trafficlight,
.qnbutton.invalidanswer .trafficlight {
- background-color: $body-color;
+ background-color: $card-bg;
}
.qnbutton.invalidanswer .trafficlight {
$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;
.que .comment {
@extend .alert;
}
+.que .outcome,
+.que .comment {
+ @extend .alert-warning;
+}
.que .formulation {
@extend .alert-info;
margin-bottom: 1.5em;
}
-.que .correctness {
- @extend label;
-
- &.correct {
- @extend .text-success;
- }
-
- &.partiallycorrect {
- @extend .text-warning;
- }
-
- &.notanswered,
- &.incorrect {
- @extend .text-danger;
- }
-}
-
.que .validationerror {
@extend .text-danger;
}
// copied from .formFieldState in mixin.less
// and made more specific
-@mixin answerState($textColor, $backgroundColor) {
- $borderColor: $textColor;
- color: $textColor;
- background-color: $backgroundColor;
- border-color: $borderColor;
-
- @include box-shadow(inset 0 1px 1px rgba(0, 0, 0, .075)); // Redeclare so transitions work
- &:focus {
- border-color: darken($borderColor, 10%);
- $shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px lighten($borderColor, 20%);
-
- @include box-shadow($shadow);
- }
-}
-
-.formulation .correct {
- @extend .text-success;
-}
-
-.formulation .partiallycorrect {
- @extend .text-warning;
-}
-
-.formulation .incorrect {
- @extend .text-danger;
-}
-
-.formulation select.correct,
-.formulation input.correct {
- @include answerState($btn-success-color, $btn-success-bg);
-}
-
-.formulation select.partiallycorrect,
-.formulation input.partiallycorrect {
- @include answerState($btn-warning-color, $btn-warning-bg);
-}
-
-.formulation select.incorrect,
-.formulation input.incorrect {
- @include answerState($btn-danger-color, $btn-danger-bg);
-}
-
.que .grading,
.que .comment,
.que .commentlink,
padding: 0;
margin: 0.7em 0 0;
}
+
+// Fix for accurate positioning of ddwtos drag and drop question types.
+.que.ddwtos {
+ box-sizing: content-box;
+}
+
+@include media-breakpoint-down(sm) {
+ .que .info {
+ float: none;
+ width: auto;
+ }
+ .que .content {
+ margin: 0;
+ }
+}
visibility: inherit;
}
-
-/* .controls vs .controls
- *
- * in expanding course list, can be fixed in renderer though
- */
-
-
-/* .btn vs .btn
- *
- * links, inputs and buttons are commonly styled with .btn in
- * bootstrap but it seems that some buttons in Moodle are
- * wrapped in div.btn e.g. the login block's "login" button
- */
-
-div.c1.btn {
- display: block;
- padding: 0;
- margin-bottom: 0;
- font-size: inherit;
- line-height: inherit;
- text-align: inherit;
- cursor: default;
- color: inherit;
- text-shadow: inherit;
- background-color: inherit;
- background-image: none;
- background-repeat: no-repeat;
- border: none;
- border-radius: 0;
- box-shadow: none;
-}
/* .container vs .container
*
* bootstrap uses .container to set the width of the layout at 960px or so, Moodle uses it
.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>
+{{!
+ 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/login
+
+ Login page template
+
+ Example context (json):
+ {
+ "output": {
+ "doctype": "<!DOCTYPE html>",
+ "page_title": "Login page",
+ "favicon": "favicon.ico",
+ "main_content": "<h1>Headers keep HTML validators happy</h1>"
+ }
+ }
+}}
{{{ output.doctype }}}
<html {{{ output.htmlattributes }}}>
<head>
</body>
</html>
+{{#js}}
+require(['theme_boost/loader']);
+{{/js}}
{{{ output.standard_top_of_body_html }}}
- <div id="page" class="container card">
+ <div id="page" class="container card card-block">
<div class="row">
<div class="col-xs-12 p-y-1">
+// 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;
+ }
}
}
visibility: inherit;
}
-
-/* .controls vs .controls
- *
- * in expanding course list, can be fixed in renderer though
- */
-
-
-/* .btn vs .btn
- *
- * links, inputs and buttons are commonly styled with .btn in
- * bootstrap but it seems that some buttons in Moodle are
- * wrapped in div.btn e.g. the login block's "login" button
- */
-
-div.c1.btn {
- display: block;
- padding: 0;
- margin-bottom: 0;
- font-size: inherit;
- line-height: inherit;
- text-align: inherit;
- cursor: default;
- color: inherit;
- text-shadow: inherit;
- background-color: inherit;
- background-image: none;
- background-repeat: no-repeat;
- border: none;
- border-radius: 0;
- box-shadow: none;
-}
/* .container vs .container
*
* bootstrap uses .container to set the width of the layout at 960px or so, Moodle uses it
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;
}
display: inherit;
visibility: inherit;
}
-/* .controls vs .controls
- *
- * in expanding course list, can be fixed in renderer though
- */
-/* .btn vs .btn
- *
- * links, inputs and buttons are commonly styled with .btn in
- * bootstrap but it seems that some buttons in Moodle are
- * wrapped in div.btn e.g. the login block's "login" button
- */
-div.c1.btn {
- display: block;
- padding: 0;
- margin-bottom: 0;
- font-size: inherit;
- line-height: inherit;
- text-align: inherit;
- cursor: default;
- color: inherit;
- text-shadow: inherit;
- background-color: inherit;
- background-image: none;
- background-repeat: no-repeat;
- border: none;
- border-radius: 0;
- box-shadow: none;
-}
/* .container vs .container
*
* bootstrap uses .container to set the width of the layout at 960px or so, Moodle uses it
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.02; // 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.