Skip to content

Commit

Permalink
MDL-66667 course: add course image cache
Browse files Browse the repository at this point in the history
  • Loading branch information
dmitriim committed Mar 15, 2021
1 parent fc335f5 commit 2affc87
Show file tree
Hide file tree
Showing 8 changed files with 371 additions and 15 deletions.
102 changes: 102 additions & 0 deletions course/classes/cache/course_image.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?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/>.

namespace core_course\cache;

use cache_data_source;
use cache_definition;
use moodle_url;
use core_course_list_element;

/**
* Class to describe cache data source for course image.
*
* @package core
* @subpackage course
* @author Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @copyright 2021 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_image implements cache_data_source {

/** @var course_image */
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 this interface.
*
* @param cache_definition $definition
* @return \core_course\cache\course_image
*/
public static function get_instance_for_cache(cache_definition $definition): course_image {
if (is_null(self::$instance)) {
self::$instance = new course_image();
}
return self::$instance;
}

/**
* Loads the data for the key provided ready formatted for caching.
*
* @param string|int $key The key to load.
* @return string|bool Returns course image url as a string or false if the image is not exist
*/
public function load_for_cache($key) {
$course = get_fast_modinfo($key)->get_course();
return $this->get_image_url_from_overview_files($course);
}

/**
* Returns image URL from course overview files.
*
* @param \stdClass $course Course object.
* @return null|string Image URL or null if it's not exists.
*/
protected function get_image_url_from_overview_files(\stdClass $course): ?string {
$courseinlist = new core_course_list_element($course);
foreach ($courseinlist->get_course_overviewfiles() as $file) {
if ($file->is_valid_image()) {
return moodle_url::make_pluginfile_url(
$file->get_contextid(),
$file->get_component(),
$file->get_filearea(),
null,
$file->get_filepath(),
$file->get_filename()
)->out();
}
}

// Returning null if no image found to let it be cached
// as false is what cache API returns then a data is not found in cache.
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): array {
$records = [];
foreach ($keys as $key) {
$records[$key] = $this->load_for_cache($key);
}
return $records;
}
}
22 changes: 7 additions & 15 deletions course/classes/external/course_summary_exporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,24 +165,16 @@ public static function define_other_properties() {
* Get the course image if added to course.
*
* @param object $course
* @return string url of course image
* @return string|false url of course image or false if it's not exist.
*/
public static function get_course_image($course) {
global $CFG;
$courseinlist = new \core_course_list_element($course);
foreach ($courseinlist->get_course_overviewfiles() as $file) {
if ($file->is_valid_image()) {
$pathcomponents = [
'/pluginfile.php',
$file->get_contextid(),
$file->get_component(),
$file->get_filearea() . $file->get_filepath() . $file->get_filename()
];
$path = implode('/', $pathcomponents);
return (new moodle_url($path))->out();
}
$image = \cache::make('core', 'course_image')->get($course->id);

if (is_null($image)) {
$image = false;
}
return false;

return $image;
}

/**
Expand Down
3 changes: 3 additions & 0 deletions course/lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -2551,6 +2551,9 @@ function update_course($data, $editoroptions = NULL) {
// make sure the modinfo cache is reset
rebuild_course_cache($data->id);

// Purge course image cache in case if course image has been updated.
\cache::make('core', 'course_image')->delete($data->id);

// update course format options with full course data
course_get_format($data->id)->update_course_format_options($data, $oldcourse);

Expand Down
176 changes: 176 additions & 0 deletions course/tests/course_image_cache_test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
<?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/>.

namespace tests\core_course;

use context_user;
use context_course;
use ReflectionMethod;
use cache_definition;
use core_course\cache\course_image;

/**
* Functional test for class course_image
*
* @package core
* @subpackage course
* @author Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @copyright 2021 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_image_cache_testcase extends \advanced_testcase {

/**
* Initial setup.
*/
protected function setUp(): void {
global $CFG;

parent::setUp();
$this->resetAfterTest();
$this->setAdminUser();

// Allow multiple overview files.
$CFG->courseoverviewfileslimit = 3;
}

/**
* Helper method to create a draft area for current user and fills it with fake files
*
* @param array $files array of files that need to be added to filearea, filename => filecontents
* @return int draftid for the filearea
*/
protected function fill_draft_area(array $files): int {
global $USER;

$draftid = file_get_unused_draft_itemid();
foreach ($files as $filename => $filecontents) {
// Add actual file there.
$filerecord = [
'component' => 'user',
'filearea' => 'draft',
'contextid' => context_user::instance($USER->id)->id, 'itemid' => $draftid,
'filename' => $filename, 'filepath' => '/'
];
$fs = get_file_storage();
$fs->create_file_from_string($filerecord, $filecontents);
}
return $draftid;
}

/**
* A helper method to generate expected file URL.
*
* @param \stdClass $course Course object.
* @param string $filename File name.
* @return string
*/
protected function build_expected_course_image_url(\stdClass $course, string $filename): string {
$contextid = context_course::instance($course->id)->id;
return 'https://www.example.com/moodle/pluginfile.php/' . $contextid. '/course/overviewfiles/' . $filename;
}

/**
* Test exception if try to get an image for non existing course.
*/
public function test_getting_data_if_course_is_not_exist() {
$this->expectException('dml_missing_record_exception');
$this->expectExceptionMessageMatches("/Can't find data record in database table course./");
$this->assertFalse(\cache::make('core', 'course_image')->get(999));
}

/**
* Test get_image_url_from_overview_files when no summary files in the course.
*/
public function test_get_image_url_from_overview_files_return_null_if_no_summary_files_in_the_course() {
$method = new ReflectionMethod(course_image::class, 'get_image_url_from_overview_files');
$cache = course_image::get_instance_for_cache(new cache_definition());
$method->setAccessible(true);

// Create course without files.
$course = $this->getDataGenerator()->create_course();
$this->assertNull($method->invokeArgs($cache, [$course]));
}

/**
* Test get_image_url_from_overview_files when no summary images in the course.
*/
public function test_get_image_url_from_overview_files_returns_null_if_no_summary_images_in_the_course() {
$method = new ReflectionMethod(course_image::class, 'get_image_url_from_overview_files');
$cache = course_image::get_instance_for_cache(new cache_definition());
$method->setAccessible(true);

// Create course without image files.
$draftid2 = $this->fill_draft_area(['filename2.zip' => 'Test file contents2']);
$course2 = $this->getDataGenerator()->create_course(['overviewfiles_filemanager' => $draftid2]);
$this->assertNull($method->invokeArgs($cache, [$course2]));
}

/**
* Test get_image_url_from_overview_files when no summary images in the course.
*/
public function test_get_image_url_from_overview_files_returns_url_if_there_is_a_summary_image() {
$method = new ReflectionMethod(course_image::class, 'get_image_url_from_overview_files');
$cache = course_image::get_instance_for_cache(new cache_definition());
$method->setAccessible(true);

// Create course without one image.
$draftid1 = $this->fill_draft_area(['filename1.jpg' => file_get_contents(__DIR__ . '/fixtures/image.jpg')]);
$course1 = $this->getDataGenerator()->create_course(['overviewfiles_filemanager' => $draftid1]);
$expected = $this->build_expected_course_image_url($course1, 'filename1.jpg');
$this->assertEquals($expected, $method->invokeArgs($cache, [$course1]));
}

/**
* Test get_image_url_from_overview_files when several summary images in the course.
*/
public function test_get_image_url_from_overview_files_returns_url_of_the_first_image_if_there_are_many_summary_images() {
$method = new ReflectionMethod(course_image::class, 'get_image_url_from_overview_files');
$cache = course_image::get_instance_for_cache(new cache_definition());
$method->setAccessible(true);

// Create course with two image files.
$draftid1 = $this->fill_draft_area([
'filename1.jpg' => file_get_contents(__DIR__ . '/fixtures/image.jpg'),
'filename2.jpg' => file_get_contents(__DIR__ . '/fixtures/image.jpg'),
]);
$course1 = $this->getDataGenerator()->create_course(['overviewfiles_filemanager' => $draftid1]);

$expected = $this->build_expected_course_image_url($course1, 'filename1.jpg');
$this->assertEquals($expected, $method->invokeArgs($cache, [$course1]));
}

/**
* Test get_image_url_from_overview_files when several summary files in the course.
*/
public function test_get_image_url_from_overview_files_returns_url_of_the_first_image_if_there_are_many_summary_files() {
$method = new ReflectionMethod(course_image::class, 'get_image_url_from_overview_files');
$cache = course_image::get_instance_for_cache(new cache_definition());
$method->setAccessible(true);

// Create course with two image files and one zip file.
$draftid1 = $this->fill_draft_area([
'filename1.zip' => 'Test file contents2',
'filename2.jpg' => file_get_contents(__DIR__ . '/fixtures/image.jpg'),
'filename3.jpg' => file_get_contents(__DIR__ . '/fixtures/image.jpg'),
]);
$course1 = $this->getDataGenerator()->create_course(['overviewfiles_filemanager' => $draftid1]);

$expected = $this->build_expected_course_image_url($course1, 'filename2.jpg');
$this->assertEquals($expected, $method->invokeArgs($cache, [$course1]));
}

}
73 changes: 73 additions & 0 deletions course/tests/course_summary_exporter_test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?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/>.

namespace tests\core_course;

use core_course\external\course_summary_exporter;
use context_user;
use context_course;

/**
* Functional test for class course_summary_exporter
*
* @package core
* @subpackage course
* @author Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @copyright 2021 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_summary_exporter_testcase extends \advanced_testcase {

/**
* Test that if no course overview images uploaded get_course_image returns false.
*/
public function test_get_course_image_when_no_overview_images_uploaded() {
$this->resetAfterTest(true);
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();

$this->assertFalse(course_summary_exporter::get_course_image($course));
}

/**
* Test that if course overview images uploaded get_course_image returns an image URL.
*/
public function test_get_course_image_when_overview_images_are_uploaded() {
global $USER;

$this->resetAfterTest(true);
$this->setAdminUser();

$draftid = file_get_unused_draft_itemid();
$filerecord = [
'component' => 'user',
'filearea' => 'draft',
'contextid' => context_user::instance($USER->id)->id,
'itemid' => $draftid,
'filename' => 'image.jpg',
'filepath' => '/',
];
$fs = get_file_storage();
$fs->create_file_from_string($filerecord, file_get_contents(__DIR__ . '/fixtures/image.jpg'));
$course = $this->getDataGenerator()->create_course(['overviewfiles_filemanager' => $draftid]);
$coursecontext = context_course::instance($course->id);

$expected = 'https://www.example.com/moodle/pluginfile.php/' . $coursecontext->id . '/course/overviewfiles/image.jpg';
$actual = course_summary_exporter::get_course_image($course);
$this->assertSame($expected, $actual);
}

}
Binary file added course/tests/fixtures/image.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 2affc87

Please sign in to comment.