Skip to content

Commit 05813c7

Browse files
committed
Issue #18: Add support to add an icon to a CMS type.
- Add ability to edit an icon file in CMS edit form - Display icon on activity list. - Include icon in import and export. - Include icon in cache key.
1 parent 831d0f7 commit 05813c7

10 files changed

+225
-6
lines changed

classes/form/cms_types_form.php

+37-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use core\form\persistent as persistent_form;
2828
use mod_cms\local\datasource\base as dsbase;
2929
use mod_cms\local\renderer;
30+
use mod_cms\local\model\cms_types;
3031

3132
/**
3233
* Form for manipulating the content types
@@ -39,7 +40,7 @@
3940
class cms_types_form extends persistent_form {
4041

4142
/** The maximum amount of files allowed. */
42-
const MAX_FILES = 50;
43+
const MAX_FILES = 1;
4344

4445
/** @var string Persistent class name. */
4546
protected static $persistentclass = 'mod_cms\\local\\model\\cms_types';
@@ -90,6 +91,22 @@ protected function definition() {
9091
$helptext .= \html_writer::tag('pre', implode(PHP_EOL, $renderer->get_variable_list()));
9192
$mform->addElement('static', 'mustache_help', '', $helptext);
9293

94+
$mform->addElement('static', 'iconfile_desc', get_string('icon'),
95+
get_string('cms_type:icon_desc', 'mod_cms'));
96+
$mform->addElement(
97+
'filemanager',
98+
'iconfile',
99+
null,
100+
null,
101+
[
102+
'subdirs' => 0,
103+
'maxbytes' => $CFG->maxbytes,
104+
'maxfiles' => 1,
105+
'accepted_types' => ['web_image'],
106+
'return_types' => FILE_INTERNAL | FILE_EXTERNAL,
107+
]
108+
);
109+
93110
// Add form elements for data sources.
94111
foreach (dsbase::get_datasources($cmstype) as $ds) {
95112
$ds->config_form_definition($mform);
@@ -154,6 +171,8 @@ public function add_action_buttons($cancel = true, $submitlabel=null) {
154171
* @return \stdClass
155172
*/
156173
protected function get_default_data() {
174+
global $CFG;
175+
157176
$data = parent::get_default_data();
158177

159178
if (is_string($data->datasources)) {
@@ -164,6 +183,23 @@ protected function get_default_data() {
164183
}
165184
}
166185

186+
// Set up a draft area for managing the type icon file.
187+
$draftitemid = file_get_submitted_draft_itemid(cms_types::ICON_FILE_AREA);
188+
189+
file_prepare_draft_area(
190+
$draftitemid,
191+
\context_system::instance()->id,
192+
'mod_cms',
193+
cms_types::ICON_FILE_AREA,
194+
$data->id,
195+
[
196+
'subdirs' => 0,
197+
'maxbytes' => $CFG->maxbytes,
198+
'maxfiles' => self::MAX_FILES,
199+
]
200+
);
201+
$data->iconfile = $draftitemid;
202+
167203
// Get default data for data sources.
168204
foreach (dsbase::get_datasources($this->get_persistent()) as $ds) {
169205
$ds->config_form_default_data($data);

classes/local/lib.php

+9-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
use core_course\local\entity\{content_item, string_title};
2020
use mod_cms\local\datasource\base as dsbase;
21+
use mod_cms\local\datasource\images as dsimages;
2122
use mod_cms\local\model\{cms, cms_types};
2223

2324

@@ -64,13 +65,19 @@ public static function get_course_content_items(content_item $defaultmoduleconte
6465
}
6566
$types = cms_types::get_records($filter);
6667
foreach ($types as $type) {
68+
$iconurl = $type->get_type_icon();
69+
if (!is_null($iconurl)) {
70+
$icon = \html_writer::empty_tag('img', ['src' => $iconurl->out(), 'alt' => $type->get('name'), 'class' => 'icon']);
71+
} else {
72+
$icon = $defaultmodulecontentitem->get_icon();
73+
}
6774
$linkurl->param('typeid', $type->get('id'));
6875
$items[] = new content_item(
6976
$type->get('id'),
7077
$defaultmodulecontentitem->get_name(),
7178
new string_title($type->get('name')),
7279
clone($linkurl),
73-
$defaultmodulecontentitem->get_icon(),
80+
$icon,
7481
$type->get('description'),
7582
$defaultmodulecontentitem->get_archetype(),
7683
$defaultmodulecontentitem->get_component_name(),
@@ -204,7 +211,7 @@ public static function cm_info_view(\cm_info $cminfo) {
204211
*/
205212
public static function pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, $options = []) {
206213
// Make sure the filearea is one of those used by the plugin.
207-
if ($filearea !== 'cms_type_images') {
214+
if (!in_array($filearea, [dsimages::FILE_AREA, cms_types::ICON_FILE_AREA])) {
208215
return false;
209216
}
210217

classes/local/model/cms_types.php

+86-1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ class cms_types extends persistent {
5050
*/
5151
const TABLE = 'cms_types';
5252

53+
/** File area name used for storing images. */
54+
const ICON_FILE_AREA = 'cms_types_icon';
55+
5356
/**
5457
* Return the definition of the properties of this model.
5558
*
@@ -143,6 +146,52 @@ public function get_custom_data(string $name) {
143146
return $cdata->$name ?? null;
144147
}
145148

149+
/**
150+
* Get the metadata for the icon, or null if no icon is stored.
151+
*
152+
* @return \stored_file|null
153+
*/
154+
public function get_icon_metadata(): ?\stored_file {
155+
$fs = get_file_storage();
156+
$files = $fs->get_area_files(
157+
\context_system::instance()->id,
158+
'mod_cms',
159+
self::ICON_FILE_AREA,
160+
$this->get('id')
161+
);
162+
// There should be 0 or 1 files. We ignore the directory (.) entry.
163+
foreach ($files as $file) {
164+
if ($file->get_filename() !== '.') {
165+
return $file;
166+
}
167+
}
168+
return null;
169+
}
170+
171+
/**
172+
* Gets the URL for the type icon.
173+
*
174+
* @return \moodle_url|null
175+
*/
176+
public function get_type_icon(): ?\moodle_url {
177+
$file = $this->get_icon_metadata();
178+
179+
$url = null;
180+
// There should only be at most one entry in the array.
181+
if (!is_null($file)) {
182+
$filename = $file->get_filename();
183+
$url = \moodle_url::make_pluginfile_url(
184+
\context_system::instance()->id,
185+
'mod_cms',
186+
self::ICON_FILE_AREA,
187+
$this->get('id'),
188+
'/',
189+
$filename
190+
);
191+
}
192+
return $url;
193+
}
194+
146195
/**
147196
* Validates idnumber parameter.
148197
*
@@ -261,6 +310,19 @@ public function get_for_export(): \stdClass {
261310
}
262311
unset($export->customdata);
263312

313+
$file = $this->get_icon_metadata();
314+
315+
// There should be at most one file.
316+
if (!is_null($file)) {
317+
$filename = $file->get_filename();
318+
$obj = new \stdClass();
319+
$obj->filename = $filename;
320+
$obj->filesize = $file->get_filesize();
321+
$obj->mimetype = $file->get_mimetype();
322+
$obj->content = base64_encode($file->get_content());
323+
$export->icon = $obj;
324+
}
325+
264326
$export->datasourcedefs = new \stdClass();
265327
foreach (dsbase::get_datasources($this) as $ds) {
266328
$name = $ds::get_shortname();
@@ -287,6 +349,20 @@ public function set_from_import(\stdClass $data) {
287349

288350
$this->create();
289351

352+
if (isset($data->icon)) {
353+
$fs = get_file_storage();
354+
$filerecord = [
355+
'component' => 'mod_cms',
356+
'filearea' => self::ICON_FILE_AREA,
357+
'itemid' => $this->get('id'),
358+
'filename' => $data->icon->filename,
359+
'filepath' => '/',
360+
'contextid' => \context_system::instance()->id,
361+
'mimetype' => $data->icon->mimetype,
362+
];
363+
$fs->create_file_from_string($filerecord, base64_decode($data->icon->content));
364+
}
365+
290366
foreach ($data->datasourcedefs as $name => $data) {
291367
$ds = dsbase::get_datasource($name, $this);
292368
$ds->set_from_import($data);
@@ -301,7 +377,16 @@ public function set_from_import(\stdClass $data) {
301377
* @return string|null
302378
*/
303379
public function get_cache_key(): ?string {
304-
return hash(lib::HASH_ALGO, serialize($this->to_record()));
380+
$data = $this->to_record();
381+
382+
$file = $this->get_icon_metadata();
383+
if (!is_null($file)) {
384+
$data->filename = $file->get_filename();
385+
$data->filesize = $file->get_filesize();
386+
$data->mimetype = $file->get_mimetype();
387+
}
388+
389+
return hash(lib::HASH_ALGO, serialize($data));
305390
}
306391

307392
/**

classes/local/renderer.php

+4
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ public function __construct($cms) {
5252
public function get_data(): \stdClass {
5353
$data = new \stdClass();
5454
$data->name = $this->cms->get('name');
55+
$data->icon = $this->cms->get_type()->get_type_icon();
56+
if ($data->icon instanceof \moodle_url) {
57+
$data->icon = $data->icon->out();
58+
}
5559

5660
foreach (dsbase::get_datasources($this->cms) as $ds) {
5761
$name = $ds::get_shortname();

classes/local/table/content_types_list.php

+17
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,14 @@ public function __construct(?int $id = null) {
6666

6767
// Column definition.
6868
$this->define_columns([
69+
'icon',
6970
'name',
7071
'numinstances',
7172
'actions',
7273
]);
7374

7475
$this->define_headers([
76+
'',
7577
get_string('table:name', 'mod_cms'),
7678
get_string('table:numinstances', 'mod_cms'),
7779
get_string('actions'),
@@ -82,6 +84,21 @@ public function __construct(?int $id = null) {
8284
$this->setup();
8385
}
8486

87+
/**
88+
* Display icon, if any.
89+
*
90+
* @param cms_types $type
91+
*
92+
* @return string
93+
*/
94+
protected function col_icon(cms_types $type): string {
95+
$iconurl = $type->get_type_icon();
96+
if (is_null($iconurl)) {
97+
return '';
98+
}
99+
return \html_writer::empty_tag('img', ['src' => $iconurl->out(), 'alt' => $type->get('name'), 'class' => 'icon']);
100+
}
101+
85102
/**
86103
* Display name column.
87104
*

classes/manage_content_types.php

+17-1
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ public static function get_base_url(): string {
240240
* @param int|null $id If no ID is provided, that means we are creating a new one
241241
*/
242242
protected function edit(string $action, ?int $id = null): void {
243-
global $PAGE;
243+
global $CFG, $PAGE;
244244

245245
$PAGE->set_url(new \moodle_url(self::get_base_url(), ['action' => $action, 'id' => $id]));
246246
$instance = null;
@@ -261,9 +261,25 @@ protected function edit(string $action, ?int $id = null): void {
261261
// Create new.
262262
if (empty($data->id)) {
263263
$instance = $this->create($data);
264+
$id = $instance->get('id');
264265
} else { // Update existing.
265266
$this->update($id, $data);
266267
}
268+
269+
// Save the icon file.
270+
file_save_draft_area_files(
271+
$data->iconfile,
272+
\context_system::instance()->id,
273+
'mod_cms',
274+
cms_types::ICON_FILE_AREA,
275+
$id,
276+
[
277+
'subdirs' => 0,
278+
'maxbytes' => $CFG->maxbytes,
279+
'maxfiles' => cms_types_form::MAX_FILES,
280+
]
281+
);
282+
267283
\core\notification::success(get_string('changessaved'));
268284
} catch (\Throwable $e) {
269285
\core\notification::error($e->getMessage());

lang/en/cms.php

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
$string['mustache'] = 'Content';
7373
$string['mustache_help'] = 'The two fields above will form the content displayed in the activity. They both will need to be valid {$a}. Variables that are available for use in these templates are given below.';
7474
$string['mustache_template'] = 'mustache templates';
75+
$string['cms_type:icon_desc'] = 'This icon will be displayed in the activity chooser menu.';
7576

7677
// Event strings.
7778
$string['event:cms_type_created'] = 'Custom content type created';

tests/cms_types_test.php

+36
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
use mod_cms\form\cms_types_form;
2020
use mod_cms\local\model\cms_types;
2121

22+
defined('MOODLE_INTERNAL') || die();
23+
24+
require_once(__DIR__ . '/fixtures/test_import1_trait.php');
25+
2226
/**
2327
* Unit tests for cms_types.
2428
*
@@ -28,6 +32,9 @@
2832
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2933
*/
3034
class cms_types_test extends \advanced_testcase {
35+
/** Test data for import/export. */
36+
public const IMPORT_DATAFILE = __DIR__ . '/fixtures/type_data.json';
37+
3138
/**
3239
* Set up before each test
3340
*/
@@ -117,4 +124,33 @@ public function idnumber_validity_datasource(): array {
117124
['test-exists', false],
118125
];
119126
}
127+
128+
/**
129+
* Tests the import/export functions.
130+
*
131+
* @covers \mod_cms\local\model\cms_types::get_for_export
132+
* @covers \mod_cms\local\model\cms_types::get_from_import
133+
* @covers \mod_cms\local\model\cms_types::get_cache_key
134+
* @covers \mod_cms\local\model\cms_types::get_icon_metadata
135+
*/
136+
public function test_import() {
137+
$importdata = json_decode(file_get_contents(self::IMPORT_DATAFILE));
138+
$cmstype = new cms_types();
139+
$cmstype->set_from_import($importdata);
140+
$cachekey = $cmstype->get_cache_key();
141+
$exportdata = $cmstype->get_for_export();
142+
143+
$this->assertNotNull($cmstype->get_icon_metadata());
144+
$this->assertEquals($importdata, $exportdata);
145+
146+
unset($importdata->icon);
147+
$importdata->idnumber = 'diffname';
148+
$cmstype = new cms_types();
149+
$cmstype->set_from_import($importdata);
150+
$exportdata = $cmstype->get_for_export();
151+
152+
$this->assertNull($cmstype->get_icon_metadata());
153+
$this->assertNotEquals($cachekey, $cmstype->get_cache_key());
154+
$this->assertEquals($importdata, $exportdata);
155+
}
120156
}

tests/fixtures/type_data.json

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "Some name",
3+
"idnumber": "somename",
4+
"description": "",
5+
"descriptionformat": 1,
6+
"title_mustache": "title",
7+
"mustache": "{{debug}}",
8+
"datasources": [""],
9+
"isvisible": true,
10+
"icon": {
11+
"filename": "x.png",
12+
"filesize": "196",
13+
"mimetype": "image\/png",
14+
"content": "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAAXNSR0IArs4c6QAAAH5JREFUKFOFz6ENwmAQhuGnCYIBcAzAAiWwAoqEiqZLYElFJYJRMCxQA0EicYyAQpEKBLmk4k9Dwrm7732T7zJ\/JkvyAk9cUyeAMQ5444y4rfDBPpY1lqh7s8EDc9wC2GCB3aDOFqMApr39wh0XzFDhmJYskWOCDi1OKfDz4S\/mihIlTY5RLwAAAABJRU5ErkJggg=="
15+
},
16+
"datasourcedefs": {}
17+
}

0 commit comments

Comments
 (0)