Skip to content

Commit

Permalink
Workaround for core bug with preferences limit #88
Browse files Browse the repository at this point in the history
  • Loading branch information
marinaglancy committed Oct 2, 2024
1 parent 81d4a70 commit 49ce966
Show file tree
Hide file tree
Showing 4 changed files with 355 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.
- When a section used to be displayed on a course page and is now displayed as a link,
the old student preferences about collapsed state affect the visibility of
the section summary #68
- Coding error when trying to collapse a large number of sections #88
(workaround for the core bug MDL-78073)

## [4.1.1] - 2024-10-02
### Fixed
Expand Down
162 changes: 162 additions & 0 deletions classes/local/helpers/preferences.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<?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 format_flexsections\local\helpers;

use cache;
use core_text;

/**
* Helps to store and retrieve large user preferences
*
* @package format_flexsections
* @copyright Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait preferences {

/**
* Name for the part of the preference
*
* @param string $name
* @param int $cnt
* @return string
*/
protected static function get_preference_name(string $name, int $cnt = 0): string {
return $cnt ? "{$name}#{$cnt}" : $name;
}

/**
* Set a preference for the user (value can be longer than 1333)
*
* @param string $name
* @param string|null $value
* @return void
*/
public static function set_long_preference(string $name, ?string $value): void {
$allpreferences = array_filter(get_user_preferences(), function($prefname) use ($name) {
return $prefname === $name || (strpos($prefname, "{$name}#") === 0);
}, ARRAY_FILTER_USE_KEY);
$len = ceil(core_text::strlen((string)$value) / 1300);
for ($cnt = 0; $cnt < $len; $cnt++) {
$pref = self::get_preference_name($name, $cnt);
set_user_preference($pref, core_text::substr($value, $cnt * 1300, 1300));
unset($allpreferences[$pref]);
}
foreach (array_keys($allpreferences) as $pref) {
unset_user_preference($pref);
}
}

/**
* Get a preference for the user (value can be longer than 1333)
*
* @param string $name
* @return string
*/
public static function get_long_preference(string $name): string {
$allpreferences = array_filter(get_user_preferences(), function($prefname) use ($name) {
return $prefname === $name || (strpos($prefname, "{$name}#") === 0);
}, ARRAY_FILTER_USE_KEY);
$value = '';
for ($cnt = 0; $cnt < count($allpreferences); $cnt++) {
$value .= $allpreferences[self::get_preference_name($name, $cnt)] ?? '';
}
return $value;
}

/**
* Return the format section preferences.
*
* @return array of preferences indexed by preference name
*/
public function get_sections_preferences_by_preference(): array {
$course = $this->get_course();
try {
$sectionpreferences = json_decode(
self::get_long_preference("coursesectionspreferences_{$course->id}"),
true,
) ?: [];
} catch (\Throwable $e) {
$sectionpreferences = [];
}
return $sectionpreferences;
}

/**
* Return the format section preferences.
*
* @param string $preferencename preference name
* @param int[] $sectionids affected section ids
*
*/
public function set_sections_preference(string $preferencename, array $sectionids) {
$sectionpreferences = $this->get_sections_preferences_by_preference();
$sectionpreferences[$preferencename] = $sectionids;
$this->persist_to_user_preference($sectionpreferences);
}

/**
* Add section preference ids.
*
* @param string $preferencename preference name
* @param array $sectionids affected section ids
*/
public function add_section_preference_ids(string $preferencename, array $sectionids): void {
$sectionpreferences = $this->get_sections_preferences_by_preference();
if (!isset($sectionpreferences[$preferencename])) {
$sectionpreferences[$preferencename] = [];
}
foreach ($sectionids as $sectionid) {
if (!in_array($sectionid, $sectionpreferences[$preferencename])) {
$sectionpreferences[$preferencename][] = $sectionid;
}
}
$this->persist_to_user_preference($sectionpreferences);
}

/**
* Remove section preference ids.
*
* @param string $preferencename preference name
* @param array $sectionids affected section ids
*/
public function remove_section_preference_ids(string $preferencename, array $sectionids): void {
$sectionpreferences = $this->get_sections_preferences_by_preference();
if (!isset($sectionpreferences[$preferencename])) {
$sectionpreferences[$preferencename] = [];
}
foreach ($sectionids as $sectionid) {
if (($key = array_search($sectionid, $sectionpreferences[$preferencename])) !== false) {
unset($sectionpreferences[$preferencename][$key]);
}
}
$this->persist_to_user_preference($sectionpreferences);
}

/**
* Persist the section preferences to the user preferences.
*
* @param array $sectionpreferences the section preferences
*/
private function persist_to_user_preference(array $sectionpreferences): void {
$course = $this->get_course();
self::set_long_preference('coursesectionspreferences_' . $course->id, json_encode($sectionpreferences));
// Invalidate section preferences cache.
$coursesectionscache = cache::make('core', 'coursesectionspreferences');
$coursesectionscache->delete($course->id);
}
}
2 changes: 2 additions & 0 deletions lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

use format_flexsections\constants;
use core\output\inplace_editable;
use format_flexsections\local\helpers\preferences;

define('FORMAT_FLEXSECTIONS_COLLAPSED', 1);
define('FORMAT_FLEXSECTIONS_EXPANDED', 0);
Expand All @@ -39,6 +40,7 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class format_flexsections extends core_courseformat\base {
use preferences;

/**
* Returns true if this course format uses sections.
Expand Down
189 changes: 189 additions & 0 deletions tests/local/helpers/preferences_test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<?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 format_flexsections\local\helpers;
use format_flexsections;

/**
* Tests for Flexible sections format
*
* @covers \format_flexsections\local\helpers\preferences
* @package format_flexsections
* @category test
* @copyright Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class preferences_test extends \advanced_testcase {

public function setUp(): void {
global $CFG;
require_once($CFG->dirroot . '/course/lib.php');
parent::setUp();
}

public function test_long_preferences(): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);

format_flexsections::set_long_preference('name', 'value');
$this->assertEquals('value', format_flexsections::get_long_preference('name'));
$preferences = get_user_preferences();
$this->assertArrayHasKey('name', $preferences);
$this->assertArrayNotHasKey('name#1', $preferences);

$longpref = random_string(2000);
format_flexsections::set_long_preference('name', $longpref);
$this->assertEquals($longpref, format_flexsections::get_long_preference('name'));
$preferences = get_user_preferences();
$this->assertArrayHasKey('name', $preferences);
$this->assertArrayHasKey('name#1', $preferences);
$this->assertArrayNotHasKey('name#2', $preferences);

$verylongpref = random_string(1300 * 5 + 500);
format_flexsections::set_long_preference('name', $verylongpref);
$this->assertEquals($verylongpref, format_flexsections::get_long_preference('name'));
$preferences = get_user_preferences();
$this->assertArrayHasKey('name', $preferences);
$this->assertArrayHasKey('name#1', $preferences);
$this->assertArrayHasKey('name#2', $preferences);
$this->assertArrayHasKey('name#3', $preferences);
$this->assertArrayHasKey('name#4', $preferences);
$this->assertArrayHasKey('name#5', $preferences);
$this->assertArrayNotHasKey('name#6', $preferences);

format_flexsections::set_long_preference('name', 'value again');
$this->assertEquals('value again', format_flexsections::get_long_preference('name'));
$preferences = get_user_preferences();
$this->assertArrayHasKey('name', $preferences);
$this->assertArrayNotHasKey('name#1', $preferences);

format_flexsections::set_long_preference('name', null);
$this->assertEquals('', format_flexsections::get_long_preference('name'));
$preferences = get_user_preferences();
$this->assertArrayNotHasKey('name', $preferences);
}

/**
* Test for the default delete format data behaviour.
*
* @covers ::set_sections_preference
*/
public function test_set_sections_preference(): void {
$this->resetAfterTest();
$generator = $this->getDataGenerator();
$course = $generator->create_course(['numsections' => 200, 'format' => 'flexsections'],
['createsections' => true]);
$user = $generator->create_and_enrol($course, 'student');

$format = course_get_format($course);
$this->setUser($user);

// Load data from user 1.
$format->set_sections_preference('pref1', [1, 2]);
$format->set_sections_preference('pref2', [1]);
$format->set_sections_preference('pref3', []);

$preferences = $format->get_sections_preferences();
$this->assertEquals(
(object)['pref1' => true, 'pref2' => true],
$preferences[1]
);
$this->assertEquals(
(object)['pref1' => true],
$preferences[2]
);
}

public function test_add_section_preference_ids(): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['numsections' => 200, 'format' => 'flexsections'],
['createsections' => true]);
$user = $this->getDataGenerator()->create_and_enrol($course);
$this->setUser($user);

$format = course_get_format($course);

// Add section preference ids.
$format->add_section_preference_ids('pref1', [1, 2]);
$format->add_section_preference_ids('pref1', [3]);
$format->add_section_preference_ids('pref2', [1]);

// Get section preferences.
$sectionpreferences = $format->get_sections_preferences_by_preference();
$this->assertCount(3, $sectionpreferences['pref1']);
$this->assertContains(1, $sectionpreferences['pref1']);
$this->assertContains(2, $sectionpreferences['pref1']);
$this->assertContains(3, $sectionpreferences['pref1']);
$this->assertCount(1, $sectionpreferences['pref2']);
$this->assertContains(1, $sectionpreferences['pref1']);
}

public function test_add_section_preference_ids_long(): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['numsections' => 500, 'format' => 'flexsections'],
['createsections' => true]);
$user = $this->getDataGenerator()->create_and_enrol($course);
$this->setUser($user);

$format = course_get_format($course);

// Add section preference ids.
$vals = array_keys(array_fill(0, 500, true));
$format->add_section_preference_ids('contentcollapsed', $vals);

// Get section preferences.
$sectionpreferences = $format->get_sections_preferences_by_preference();
$this->assertCount(500, $sectionpreferences['contentcollapsed']);
}

/**
* Test remove_section_preference_ids() method.
*
* @covers \core_courseformat\base::persist_to_user_preference
*/
public function test_remove_section_preference_ids(): void {
$this->resetAfterTest();
// Create initial data.
$generator = $this->getDataGenerator();
$course = $generator->create_course(['numsections' => 200, 'format' => 'flexsections'],
['createsections' => true]);
$user = $generator->create_and_enrol($course);
// Get the course format.
$format = course_get_format($course);
// Login as the user.
$this->setUser($user);
// Set initial preferences.
$format->set_sections_preference('pref1', [1, 2, 3]);
$format->set_sections_preference('pref2', [1]);

// Remove section with id = 3 out of the pref1.
$format->remove_section_preference_ids('pref1', [3]);
// Get section preferences.
$sectionpreferences = $format->get_sections_preferences_by_preference();
$this->assertCount(2, $sectionpreferences['pref1']);
$this->assertCount(1, $sectionpreferences['pref2']);

// Remove section with id = 2 out of the pref1.
$format->remove_section_preference_ids('pref1', [2]);
// Remove section with id = 1 out of the pref2.
$format->remove_section_preference_ids('pref2', [1]);
// Get section preferences.
$sectionpreferences = $format->get_sections_preferences_by_preference();
$this->assertCount(1, $sectionpreferences['pref1']);
$this->assertEmpty($sectionpreferences['pref2']);
}
}

0 comments on commit 49ce966

Please sign in to comment.