MDL-67810 core_contentbank: added dropdown menu to create content

This commit is contained in:
Víctor Déniz Falcón 2020-04-29 14:00:43 +01:00 committed by Victor Deniz Falcon
parent 68fd8d8bdf
commit 75f58cbfa2
16 changed files with 437 additions and 14 deletions

View file

@ -24,6 +24,7 @@
namespace core_contentbank; namespace core_contentbank;
use core_plugin_manager;
use stored_file; use stored_file;
use context; use context;
@ -35,6 +36,8 @@ use context;
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/ */
class contentbank { class contentbank {
/** @var array Enabled content types. */
private $enabledcontenttypes = null;
/** /**
* Obtains the list of core_contentbank_content objects currently active. * Obtains the list of core_contentbank_content objects currently active.
@ -44,16 +47,20 @@ class contentbank {
* @return string[] Array of contentbank contenttypes. * @return string[] Array of contentbank contenttypes.
*/ */
public function get_enabled_content_types(): array { public function get_enabled_content_types(): array {
if (!is_null($this->enabledcontenttypes)) {
return $this->enabledcontenttypes;
}
$enabledtypes = \core\plugininfo\contenttype::get_enabled_plugins(); $enabledtypes = \core\plugininfo\contenttype::get_enabled_plugins();
$types = []; $types = [];
foreach ($enabledtypes as $name) { foreach ($enabledtypes as $name) {
$contenttypeclassname = "\\contenttype_$name\\contenttype"; $contenttypeclassname = "\\contenttype_$name\\contenttype";
$contentclassname = "\\contenttype_$name\\content"; $contentclassname = "\\contenttype_$name\\content";
if (class_exists($contenttypeclassname) && class_exists($contentclassname)) { if (class_exists($contenttypeclassname) && class_exists($contentclassname)) {
$types[] = $name; $types[$contenttypeclassname] = $name;
} }
} }
return $types; return $this->enabledcontenttypes = $types;
} }
/** /**
@ -292,4 +299,37 @@ class contentbank {
} }
return $result; return $result;
} }
/**
* Get the list of content types that have the requested feature.
*
* @param string $feature Feature code e.g CAN_UPLOAD.
* @param null|\context $context Optional context to check the permission to use the feature.
* @param bool $enabled Whether check only the enabled content types or all of them.
*
* @return string[] List of content types where the user has permission to access the feature.
*/
public function get_contenttypes_with_capability_feature(string $feature, \context $context = null, bool $enabled = true): array {
$contenttypes = [];
// Check enabled content types or all of them.
if ($enabled) {
$contenttypestocheck = $this->get_enabled_content_types();
} else {
$plugins = core_plugin_manager::instance()->get_plugins_of_type('contenttype');
foreach ($plugins as $plugin) {
$contenttypeclassname = "\\{$plugin->type}_{$plugin->name}\\contenttype";
$contenttypestocheck[$contenttypeclassname] = $plugin->name;
}
}
foreach ($contenttypestocheck as $classname => $name) {
$contenttype = new $classname($context);
// The method names that check the features permissions must follow the pattern can_feature.
if ($contenttype->{"can_$feature"}()) {
$contenttypes[$classname] = $name;
}
}
return $contenttypes;
}
} }

View file

@ -41,7 +41,10 @@ abstract class contenttype {
/** Plugin implements uploading feature */ /** Plugin implements uploading feature */
const CAN_UPLOAD = 'upload'; const CAN_UPLOAD = 'upload';
/** @var context This contenttype's context. **/ /** Plugin implements edition feature */
const CAN_EDIT = 'edit';
/** @var \context This contenttype's context. **/
protected $context = null; protected $context = null;
/** /**
@ -59,7 +62,7 @@ abstract class contenttype {
/** /**
* Fills content_bank table with appropiate information. * Fills content_bank table with appropiate information.
* *
* @param stdClass $record An optional content record compatible object (default null) * @param \stdClass $record An optional content record compatible object (default null)
* @return content Object with content bank information. * @return content Object with content bank information.
*/ */
public function create_content(\stdClass $record = null): ?content { public function create_content(\stdClass $record = null): ?content {
@ -127,7 +130,7 @@ abstract class contenttype {
* This method can be overwritten by the plugins if they need to change some other specific information. * This method can be overwritten by the plugins if they need to change some other specific information.
* *
* @param content $content The content to rename. * @param content $content The content to rename.
* @param string $name The name of the content. * @param string $name The name of the content.
* @return boolean true if the content has been renamed; false otherwise. * @return boolean true if the content has been renamed; false otherwise.
*/ */
public function rename_content(content $content, string $name): bool { public function rename_content(content $content, string $name): bool {
@ -139,7 +142,7 @@ abstract class contenttype {
* This method can be overwritten by the plugins if they need to change some other specific information. * This method can be overwritten by the plugins if they need to change some other specific information.
* *
* @param content $content The content to rename. * @param content $content The content to rename.
* @param context $context The new context. * @param \context $context The new context.
* @return boolean true if the content has been renamed; false otherwise. * @return boolean true if the content has been renamed; false otherwise.
*/ */
public function move_content(content $content, \context $context): bool { public function move_content(content $content, \context $context): bool {
@ -325,6 +328,37 @@ abstract class contenttype {
return true; return true;
} }
/**
* Returns whether or not the user has permission to use the editor.
*
* @return bool True if the user can edit content. False otherwise.
*/
final public function can_edit(): bool {
if (!$this->is_feature_supported(self::CAN_EDIT)) {
return false;
}
if (!$this->can_access()) {
return false;
}
$classname = 'contenttype/'.$this->get_plugin_name();
$editioncap = $classname.':useeditor';
$hascapabilities = has_all_capabilities(['moodle/contentbank:useeditor', $editioncap], $this->context);
return $hascapabilities && $this->is_edit_allowed();
}
/**
* Returns plugin allows edition.
*
* @return bool True if plugin allows edition. False otherwise.
*/
protected function is_edit_allowed(): bool {
// Plugins can overwrite this function to add any check they need.
return true;
}
/** /**
* Returns the plugin supports the feature. * Returns the plugin supports the feature.
* *
@ -348,4 +382,17 @@ abstract class contenttype {
* @return array * @return array
*/ */
abstract public function get_manageable_extensions(): array; abstract public function get_manageable_extensions(): array;
/**
* Returns the list of different types of the given content type.
*
* A content type can have one or more options for creating content. This method will report all of them or only the content
* type itself if it has no other options.
*
* @return array An object for each type:
* - string typename: descriptive name of the type.
* - string typeeditorparams: params required by this content type editor.
* - url typeicon: this type icon.
*/
abstract public function get_contenttype_types(): array;
} }

View file

@ -98,7 +98,56 @@ class bankcontent implements renderable, templatable {
); );
} }
$data->contents = $contentdata; $data->contents = $contentdata;
$data->tools = $this->toolbar; // The tools are displayed in the action bar on the index page.
foreach ($this->toolbar as $tool) {
// Customize the output of a tool, like dropdowns.
$method = 'export_tool_'.$tool['name'];
if (method_exists($this, $method)) {
$this->$method($tool);
}
$data->tools[] = $tool;
}
return $data; return $data;
} }
/**
* Adds the content type items to display to the Add dropdown.
*
* Each content type is represented as an object with the properties:
* - name: the name of the content type.
* - baseurl: the base content type editor URL.
* - types: different types of the content type to display as dropdown items.
*
* @param array $tool Data for rendering the Add dropdown, including the editable content types.
*/
private function export_tool_add(array &$tool) {
$editabletypes = $tool['contenttypes'];
$addoptions = [];
foreach ($editabletypes as $class => $type) {
$contentype = new $class($this->context);
// Get the creation options of each content type.
$types = $contentype->get_contenttype_types();
if ($types) {
// Add a text describing the content type as first option. This will be displayed in the drop down to
// separate the options for the different content types.
$contentdesc = new stdClass();
$contentdesc->typename = get_string('description', $contentype->get_contenttype_name());
array_unshift($types, $contentdesc);
// Context data for the template.
$addcontenttype = new stdClass();
// Content type name.
$addcontenttype->name = $type;
// Content type editor base URL.
$tool['link']->param('plugin', $type);
$addcontenttype->baseurl = $tool['link']->out();
// Different types of the content type.
$addcontenttype->types = $types;
$addoptions[] = $addcontenttype;
}
}
$tool['contenttypes'] = $addoptions;
}
} }

View file

@ -62,6 +62,19 @@ $foldercontents = $cb->search_contents($search, $contextid, $contenttypes);
// Get the toolbar ready. // Get the toolbar ready.
$toolbar = array (); $toolbar = array ();
// Place the Add button in the toolbar.
if (has_capability('moodle/contentbank:useeditor', $context)) {
// Get the content types for which the user can use an editor.
$editabletypes = $cb->get_contenttypes_with_capability_feature(\core_contentbank\contenttype::CAN_EDIT, $context);
if (!empty($editabletypes)) {
// Editor base URL.
$editbaseurl = new moodle_url('/contentbank/edit.php', ['contextid' => $contextid]);
$toolbar[] = ['name' => get_string('add'), 'link' => $editbaseurl, 'dropdown' => true, 'contenttypes' => $editabletypes];
}
}
// Place the Upload button in the toolbar.
if (has_capability('moodle/contentbank:upload', $context)) { if (has_capability('moodle/contentbank:upload', $context)) {
// Don' show upload button if there's no plugin to support any file extension. // Don' show upload button if there's no plugin to support any file extension.
$accepted = $cb->get_supported_extensions_as_string($context); $accepted = $cb->get_supported_extensions_as_string($context);

View file

@ -15,7 +15,7 @@
along with Moodle. If not, see <http://www.gnu.org/licenses/>. along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}} }}
{{! {{!
@template core_contentbank/list @template core_contentbank/bankcontent
Example context (json): Example context (json):
{ {
@ -32,10 +32,36 @@
}, },
{ {
"name": "resume.pdf", "name": "resume.pdf",
"title": "resume",
"timemodified": 1589792039,
"size": "699.3KB",
"bytes": 716126,
"type": "Archive (PDF)",
"icon": "http://something/theme/image.php/boost/core/1584597850/f/pdf-64" "icon": "http://something/theme/image.php/boost/core/1584597850/f/pdf-64"
} }
], ],
"tools": [ "tools": [
{
"name": "Add",
"dropdown": true,
"link": "http://something/contentbank/edit.php?contextid=1",
"contenttypes": [
{
"name": "H5P Interactive Content",
"baseurl": "http://something/contentbank/edit.php?contextid=1&plugin=h5p",
"types": [
{
"typename": "H5P Interactive Content"
},
{
"typename": "Accordion",
"typeeditorparams": "library=Accordion-1.4",
"typeicon": "http://something/pluginfile.php/1/core_h5p/libraries/13/H5P.Accordion-1.4/icon.svg"
}
]
}
]
},
{ {
"name": "Upload", "name": "Upload",
"link": "http://something/contentbank/contenttype/h5p/view.php?url=http://something/pluginfile.php/1/contentbank/public/accordion.h5p", "link": "http://something/contentbank/contenttype/h5p/view.php?url=http://something/pluginfile.php/1/contentbank/public/accordion.h5p",

View file

@ -20,6 +20,27 @@
Example context (json): Example context (json):
{ {
"tools": [ "tools": [
{
"name": "Add",
"dropdown": true,
"link": "http://something/contentbank/edit.php?contextid=1",
"contenttypes": [
{
"name": "h5p",
"baseurl": "http://something/contentbank/edit.php?contextid=1&plugin=h5p",
"types": [
{
"typename": "H5P Interactive Content"
},
{
"typename": "Accordion",
"typeeditorparams": "library=Accordion-1.4",
"typeicon": "http://something/pluginfile.php/1/core_h5p/libraries/13/H5P.Accordion-1.4/icon.svg"
}
]
}
]
},
{ {
"name": "Upload", "name": "Upload",
"link": "http://something/contentbank/contenttype/h5p/view.php?url=http://something/pluginfile.php/1/contentbank/public/accordion.h5p", "link": "http://something/contentbank/contenttype/h5p/view.php?url=http://something/pluginfile.php/1/contentbank/public/accordion.h5p",
@ -34,9 +55,14 @@
}} }}
{{#tools}} {{#tools}}
<a href="{{{ link }}}" class="icon-no-margin btn btn-secondary" title="{{{ name }}}"> {{#dropdown}}
{{#pix}} {{{ icon }}} {{/pix}} {{{ name }}} {{>core_contentbank/bankcontent/toolbar_dropdown}}
</a> {{/dropdown}}
{{^dropdown}}
<a href="{{{ link }}}" class="icon-no-margin btn btn-secondary" title="{{{ name }}}">
{{#pix}} {{{ icon }}} {{/pix}} {{{ name }}}
</a>
{{/dropdown}}
{{/tools}} {{/tools}}
<button class="icon-no-margin btn btn-secondary active ml-2" <button class="icon-no-margin btn btn-secondary active ml-2"
title="{{#str}} displayicons, contentbank {{/str}}" title="{{#str}} displayicons, contentbank {{/str}}"
@ -47,4 +73,4 @@ data-action="viewgrid">
title="{{#str}} displaydetails, contentbank {{/str}}" title="{{#str}} displaydetails, contentbank {{/str}}"
data-action="viewlist"> data-action="viewlist">
{{#pix}}t/viewdetails, core, {{#str}} displaydetails, contentbank {{/str}} {{/pix}} {{#pix}}t/viewdetails, core, {{#str}} displaydetails, contentbank {{/str}} {{/pix}}
</button> </button>

View file

@ -0,0 +1,64 @@
{{!
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 core_contentbank/bankcontent/toolbar_dropdown
Example context (json):
{
"name": "Add",
"dropdown": true,
"link": "http://something/contentbank/edit.php?contextid=1",
"contenttypes": [
{
"name": "h5p",
"baseurl": "http://something/contentbank/edit.php?contextid=1&plugin=h5p",
"types": [
{
"typename": "H5P Interactive Content"
},
{
"typename": "Accordion",
"typeeditorparams": "library=Accordion-1.4",
"typeicon": "http://something/pluginfile.php/1/core_h5p/libraries/13/H5P.Accordion-1.4/icon.svg"
}
]
}
]
}
}}
<div class="btn-group mr-1" role="group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" data-action="{{name}}-content"
aria-haspopup="true" aria-expanded="false" {{^contenttypes}}title="{{#str}}nocontenttypes, core_contentbank{{/str}}"
disabled{{/contenttypes}}>
{{#name}} {{name}} {{/name}}
</button>
<div class="dropdown-menu dropdown-scrollable dropdown-menu-right">
{{#contenttypes}}
{{#types}}
{{^typeeditorparams}}
<h6 class="dropdown-header">{{ typename }}</h6>
{{/typeeditorparams}}
{{#typeeditorparams}}
<a class="dropdown-item icon-size-4" href="{{{ baseurl }}}&{{{ typeeditorparams }}}">
<img alt="" class="icon" src="{{{ typeicon }}}"> {{ typename }}
</a>
{{/typeeditorparams}}
{{/types}}
{{/contenttypes}}
</div>
</div>

View file

@ -507,4 +507,100 @@ class core_contentbank_testcase extends advanced_testcase {
// Check there's no error when trying to move content context from an empty content bank. // Check there's no error when trying to move content context from an empty content bank.
$this->assertTrue($cb->delete_contents($systemcontext, $coursecontext)); $this->assertTrue($cb->delete_contents($systemcontext, $coursecontext));
} }
/**
* Data provider for get_contenttypes_with_capability_feature.
*
* @return array
*/
public function get_contenttypes_with_capability_feature_provider(): array {
return [
'no-contenttypes_enabled' => [
'contenttypesenabled' => [],
'contenttypescanfeature' => [],
],
'contenttype_enabled_noeditable' => [
'contenttypesenabled' => ['testable'],
'contenttypescanfeature' => [],
],
'contenttype_enabled_editable' => [
'contenttypesenabled' => ['testable'],
'contenttypescanfeature' => ['testable'],
],
'no-contenttype_enabled_editable' => [
'contenttypesenabled' => [],
'contenttypescanfeature' => ['testable'],
],
];
}
/**
* Tests for get_contenttypes_with_capability_feature() function.
*
* @dataProvider get_contenttypes_with_capability_feature_provider
* @param array $contenttypesenabled Content types enabled.
* @param array $contenttypescanfeature Content types the user has the permission to use the feature.
*
* @covers ::get_contenttypes_with_capability_feature
*/
public function test_get_contenttypes_with_capability_feature(array $contenttypesenabled, array $contenttypescanfeature): void {
$this->resetAfterTest();
$cb = new contentbank();
$plugins = [];
// Content types not enabled where the user has permission to use a feature.
if (empty($contenttypesenabled) && !empty($contenttypescanfeature)) {
$enabled = false;
// Mock core_plugin_manager class and the method get_plugins_of_type.
$pluginmanager = $this->getMockBuilder(\core_plugin_manager::class)
->disableOriginalConstructor()
->setMethods(['get_plugins_of_type'])
->getMock();
// Replace protected singletoninstance reference (core_plugin_manager property) with mock object.
$ref = new \ReflectionProperty(\core_plugin_manager::class, 'singletoninstance');
$ref->setAccessible(true);
$ref->setValue(null, $pluginmanager);
// Return values of get_plugins_of_type method.
foreach ($contenttypescanfeature as $contenttypepluginname) {
$contenttypeplugin = new \stdClass();
$contenttypeplugin->name = $contenttypepluginname;
$contenttypeplugin->type = 'contenttype';
// Add the feature to the fake content type.
$classname = "\\contenttype_$contenttypepluginname\\contenttype";
$classname::$featurestotest = ['test2'];
$plugins[] = $contenttypeplugin;
}
// Set expectations and return values.
$pluginmanager->expects($this->once())
->method('get_plugins_of_type')
->with('contenttype')
->willReturn($plugins);
} else {
$enabled = true;
// Get access to private property enabledcontenttypes.
$rc = new \ReflectionClass(\core_contentbank\contentbank::class);
$rcp = $rc->getProperty('enabledcontenttypes');
$rcp->setAccessible(true);
foreach ($contenttypesenabled as $contenttypename) {
$plugins["\\contenttype_$contenttypename\\contenttype"] = $contenttypename;
// Add to the testable contenttype the feature to test.
if (in_array($contenttypename, $contenttypescanfeature)) {
$classname = "\\contenttype_$contenttypename\\contenttype";
$classname::$featurestotest = ['test2'];
}
}
// Set as enabled content types only those in the test.
$rcp->setValue($cb, $plugins);
}
$actual = $cb->get_contenttypes_with_capability_feature('test2', null, $enabled);
$this->assertEquals($contenttypescanfeature, array_values($actual));
}
} }

View file

@ -37,6 +37,9 @@ class contenttype extends \core_contentbank\contenttype {
/** Feature for testing */ /** Feature for testing */
const CAN_TEST = 'test'; const CAN_TEST = 'test';
/** @var array Additional features for testing */
public static $featurestotest;
/** /**
* Returns the HTML code to render the icon for content bank contents. * Returns the HTML code to render the icon for content bank contents.
* *
@ -55,7 +58,13 @@ class contenttype extends \core_contentbank\contenttype {
* @return array * @return array
*/ */
protected function get_implemented_features(): array { protected function get_implemented_features(): array {
return [self::CAN_TEST]; $features = [self::CAN_TEST];
if (!empty(self::$featurestotest)) {
$features = array_merge($features, self::$featurestotest);
}
return $features;
} }
/** /**
@ -66,4 +75,29 @@ class contenttype extends \core_contentbank\contenttype {
public function get_manageable_extensions(): array { public function get_manageable_extensions(): array {
return ['.txt', '.png', '.h5p']; return ['.txt', '.png', '.h5p'];
} }
/**
* Returns the list of different types of the given content type.
*
* @return array
*/
public function get_contenttype_types(): array {
$type = new \stdClass();
$type->typename = 'testable';
return [$type];
}
/**
* Returns true, so the user has permission on the feature.
*
* @return bool True if content could be edited or created. False otherwise.
*/
final public function can_test2(): bool {
if (!$this->is_feature_supported('test2')) {
return false;
}
return true;
}
} }

View file

@ -24,6 +24,7 @@
$string['author'] = 'Author'; $string['author'] = 'Author';
$string['contentbank'] = 'Content bank'; $string['contentbank'] = 'Content bank';
$string['close'] = 'Close';
$string['contentdeleted'] = 'The content has been deleted.'; $string['contentdeleted'] = 'The content has been deleted.';
$string['contentname'] = 'Content name'; $string['contentname'] = 'Content name';
$string['contentnotdeleted'] = 'An error was encountered while trying to delete the content.'; $string['contentnotdeleted'] = 'An error was encountered while trying to delete the content.';
@ -45,6 +46,7 @@ $string['file_help'] = 'Files may be stored in the content bank for use in cours
$string['itemsfound'] = '{$a} items found'; $string['itemsfound'] = '{$a} items found';
$string['lastmodified'] = 'Last modified'; $string['lastmodified'] = 'Last modified';
$string['name'] = 'Content'; $string['name'] = 'Content';
$string['nocontenttypes'] = 'No content types available';
$string['nopermissiontodelete'] = 'You do not have permission to delete content.'; $string['nopermissiontodelete'] = 'You do not have permission to delete content.';
$string['nopermissiontomanage'] = 'You do not have permission to manage content.'; $string['nopermissiontomanage'] = 'You do not have permission to manage content.';
$string['privacy:metadata:content:contenttype'] = 'The contenttype plugin of the content in the content bank.'; $string['privacy:metadata:content:contenttype'] = 'The contenttype plugin of the content in the content bank.';

View file

@ -156,6 +156,7 @@ $string['contentbank:deleteowncontent'] = 'Delete content from own content bank'
$string['contentbank:manageanycontent'] = 'Manage any content from the content bank (rename, move, publish, share, etc.)'; $string['contentbank:manageanycontent'] = 'Manage any content from the content bank (rename, move, publish, share, etc.)';
$string['contentbank:manageowncontent'] = 'Manage content from own content bank (rename, move, publish, share, etc.)'; $string['contentbank:manageowncontent'] = 'Manage content from own content bank (rename, move, publish, share, etc.)';
$string['contentbank:upload'] = 'Upload new content in the content bank'; $string['contentbank:upload'] = 'Upload new content in the content bank';
$string['contentbank:useeditor'] = 'Create or edit content using a content type editor';
$string['context'] = 'Context'; $string['context'] = 'Context';
$string['course:activityvisibility'] = 'Hide/show activities'; $string['course:activityvisibility'] = 'Hide/show activities';
$string['course:bulkmessaging'] = 'Send a message to many people'; $string['course:bulkmessaging'] = 'Send a message to many people';

View file

@ -2544,4 +2544,16 @@ $capabilities = array(
'editingteacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW,
) )
], ],
// Allow users to create/edit content within the content bank.
'moodle/contentbank:useeditor' => [
'riskbitmask' => RISK_SPAM,
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE,
'archetypes' => array(
'manager' => CAP_ALLOW,
'coursecreator' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW,
)
],
); );

View file

@ -120,4 +120,9 @@
} }
} }
} }
}
.cb-toolbar .dropdown-scrollable {
max-height: 190px;
overflow-y: auto;
} }

View file

@ -12944,6 +12944,10 @@ table.calendartable caption {
.content-bank-container.view-list .cb-btnsort.dir-desc .desc { .content-bank-container.view-list .cb-btnsort.dir-desc .desc {
display: block; } display: block; }
.cb-toolbar .dropdown-scrollable {
max-height: 190px;
overflow-y: auto; }
/* course.less */ /* course.less */
/* COURSE CONTENT */ /* COURSE CONTENT */
.section_add_menus { .section_add_menus {

View file

@ -13156,6 +13156,10 @@ table.calendartable caption {
.content-bank-container.view-list .cb-btnsort.dir-desc .desc { .content-bank-container.view-list .cb-btnsort.dir-desc .desc {
display: block; } display: block; }
.cb-toolbar .dropdown-scrollable {
max-height: 190px;
overflow-y: auto; }
/* course.less */ /* course.less */
/* COURSE CONTENT */ /* COURSE CONTENT */
.section_add_menus { .section_add_menus {

View file

@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die(); defined('MOODLE_INTERNAL') || die();
$version = 2020052700.00; // YYYYMMDD = weekly release date of this DEV branch. $version = 2020052700.01; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches. // RR = release increments - 00 in DEV branches.
// .XX = incremental changes. // .XX = incremental changes.
$release = '3.9dev+ (Build: 20200527)'; // Human-friendly version name $release = '3.9dev+ (Build: 20200527)'; // Human-friendly version name