MDL-67810 core_contentbank: added dropdown menu to create content
authorVíctor Déniz Falcón <victor@moodle.com>
Wed, 29 Apr 2020 13:00:43 +0000 (14:00 +0100)
committerVictor Deniz Falcon <victor@moodle.com>
Wed, 27 May 2020 09:27:13 +0000 (10:27 +0100)
16 files changed:
contentbank/classes/contentbank.php
contentbank/classes/contenttype.php
contentbank/classes/output/bankcontent.php
contentbank/index.php
contentbank/templates/bankcontent.mustache
contentbank/templates/bankcontent/toolbar.mustache
contentbank/templates/bankcontent/toolbar_dropdown.mustache [new file with mode: 0644]
contentbank/tests/contentbank_test.php
contentbank/tests/fixtures/testable_contenttype.php
lang/en/contentbank.php
lang/en/role.php
lib/db/access.php
theme/boost/scss/moodle/contentbank.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css
version.php

index 8393f6a..326e304 100644 (file)
@@ -24,6 +24,7 @@
 
 namespace core_contentbank;
 
+use core_plugin_manager;
 use stored_file;
 use context;
 
@@ -35,6 +36,8 @@ use context;
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class contentbank {
+    /** @var array Enabled content types. */
+    private $enabledcontenttypes = null;
 
     /**
      * Obtains the list of core_contentbank_content objects currently active.
@@ -44,16 +47,20 @@ class contentbank {
      * @return string[] Array of contentbank contenttypes.
      */
     public function get_enabled_content_types(): array {
+        if (!is_null($this->enabledcontenttypes)) {
+            return $this->enabledcontenttypes;
+        }
+
         $enabledtypes = \core\plugininfo\contenttype::get_enabled_plugins();
         $types = [];
         foreach ($enabledtypes as $name) {
             $contenttypeclassname = "\\contenttype_$name\\contenttype";
             $contentclassname = "\\contenttype_$name\\content";
             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;
     }
+
+    /**
+     * 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;
+    }
 }
index e2c1940..ba9442b 100644 (file)
@@ -41,7 +41,10 @@ abstract class contenttype {
     /** Plugin implements uploading feature */
     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;
 
     /**
@@ -59,7 +62,7 @@ abstract class contenttype {
     /**
      * 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.
      */
     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.
      *
      * @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.
      */
     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.
      *
      * @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.
      */
     public function move_content(content $content, \context $context): bool {
@@ -325,6 +328,37 @@ abstract class contenttype {
         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.
      *
@@ -348,4 +382,17 @@ abstract class contenttype {
      * @return 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;
 }
index 6574b04..b851222 100644 (file)
@@ -98,7 +98,56 @@ class bankcontent implements renderable, templatable {
             );
         }
         $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;
     }
+
+    /**
+     * 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;
+    }
 }
index 3b15b1e..e4bb6d6 100644 (file)
@@ -62,6 +62,19 @@ $foldercontents = $cb->search_contents($search, $contextid, $contenttypes);
 
 // Get the toolbar ready.
 $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)) {
     // Don' show upload button if there's no plugin to support any file extension.
     $accepted = $cb->get_supported_extensions_as_string($context);
index 0826a65..020b905 100644 (file)
@@ -15,7 +15,7 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template core_contentbank/list
+    @template core_contentbank/bankcontent
 
     Example context (json):
     {
             },
             {
                 "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"
             }
         ],
         "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",
                 "link": "http://something/contentbank/contenttype/h5p/view.php?url=http://something/pluginfile.php/1/contentbank/public/accordion.h5p",
index 04c762a..242fa1a 100644 (file)
     Example context (json):
     {
         "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",
                 "link": "http://something/contentbank/contenttype/h5p/view.php?url=http://something/pluginfile.php/1/contentbank/public/accordion.h5p",
 }}
 
 {{#tools}}
-    <a href="{{{ link }}}" class="icon-no-margin btn btn-secondary" title="{{{ name }}}">
-        {{#pix}} {{{ icon }}} {{/pix}} {{{ name }}}
-    </a>
+    {{#dropdown}}
+        {{>core_contentbank/bankcontent/toolbar_dropdown}}
+    {{/dropdown}}
+    {{^dropdown}}
+        <a href="{{{ link }}}" class="icon-no-margin btn btn-secondary" title="{{{ name }}}">
+            {{#pix}} {{{ icon }}} {{/pix}} {{{ name }}}
+        </a>
+    {{/dropdown}}
 {{/tools}}
 <button class="icon-no-margin btn btn-secondary active ml-2"
 title="{{#str}}  displayicons, contentbank  {{/str}}"
@@ -47,4 +73,4 @@ data-action="viewgrid">
 title="{{#str}} displaydetails, contentbank {{/str}}"
 data-action="viewlist">
     {{#pix}}t/viewdetails, core, {{#str}} displaydetails, contentbank {{/str}} {{/pix}}
-</button>
\ No newline at end of file
+</button>
diff --git a/contentbank/templates/bankcontent/toolbar_dropdown.mustache b/contentbank/templates/bankcontent/toolbar_dropdown.mustache
new file mode 100644 (file)
index 0000000..7a2fbf5
--- /dev/null
@@ -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>
index ebb6888..d347a72 100644 (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.
         $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));
+    }
 }
index a70bccb..2a5411d 100644 (file)
@@ -37,6 +37,9 @@ class contenttype extends \core_contentbank\contenttype {
     /** Feature for testing */
     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.
      *
@@ -55,7 +58,13 @@ class contenttype extends \core_contentbank\contenttype {
      * @return 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 {
         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;
+    }
 }
index 55108d3..8b451e9 100644 (file)
@@ -24,6 +24,7 @@
 
 $string['author'] = 'Author';
 $string['contentbank'] = 'Content bank';
+$string['close'] = 'Close';
 $string['contentdeleted'] = 'The content has been deleted.';
 $string['contentname'] = 'Content name';
 $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['lastmodified'] = 'Last modified';
 $string['name'] = 'Content';
+$string['nocontenttypes'] = 'No content types available';
 $string['nopermissiontodelete'] = 'You do not have permission to delete 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.';
index 21530de..3095d56 100644 (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:manageowncontent'] = 'Manage content from own content bank (rename, move, publish, share, etc.)';
 $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['course:activityvisibility'] = 'Hide/show activities';
 $string['course:bulkmessaging'] = 'Send a message to many people';
index 46af6d3..8037f92 100644 (file)
@@ -2544,4 +2544,16 @@ $capabilities = array(
             '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,
+        )
+    ],
 );
index 49e47bc..0eea802 100644 (file)
             }
         }
     }
+}
+
+.cb-toolbar .dropdown-scrollable {
+    max-height: 190px;
+    overflow-y: auto;
 }
\ No newline at end of file
index 9d14485..95f5945 100644 (file)
@@ -12944,6 +12944,10 @@ table.calendartable caption {
 .content-bank-container.view-list .cb-btnsort.dir-desc .desc {
   display: block; }
 
+.cb-toolbar .dropdown-scrollable {
+  max-height: 190px;
+  overflow-y: auto; }
+
 /* course.less */
 /* COURSE CONTENT */
 .section_add_menus {
index e131ec7..ce82009 100644 (file)
@@ -13156,6 +13156,10 @@ table.calendartable caption {
 .content-bank-container.view-list .cb-btnsort.dir-desc .desc {
   display: block; }
 
+.cb-toolbar .dropdown-scrollable {
+  max-height: 190px;
+  overflow-y: auto; }
+
 /* course.less */
 /* COURSE CONTENT */
 .section_add_menus {
index 775c624..d1a9b53 100644 (file)
@@ -29,7 +29,7 @@
 
 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.
                                         //           .XX = incremental changes.
 $release  = '3.9dev+ (Build: 20200527)'; // Human-friendly version name