mirror of
https://github.com/moodle/moodle.git
synced 2025-01-19 06:18:28 +01:00
MDL-83514 customfield_number: add missing event observers
This commit is contained in:
parent
ec7711b9a6
commit
e681b5219d
112
customfield/field/number/classes/observer.php
Normal file
112
customfield/field/number/classes/observer.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?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 customfield_number;
|
||||
|
||||
use customfield_number\local\numberproviders\nofactivities;
|
||||
use customfield_number\task\recalculate;
|
||||
|
||||
/**
|
||||
* Event observers for customfield_number
|
||||
*
|
||||
* @package customfield_number
|
||||
* @copyright Marina Glancy
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class observer {
|
||||
|
||||
/**
|
||||
* When a 'number' custom field was created, schedule recalculation for the field data
|
||||
*
|
||||
* @param \core_customfield\event\field_created $event
|
||||
*/
|
||||
public static function field_created(\core_customfield\event\field_created $event): void {
|
||||
$field = $event->get_record_snapshot('customfield_field', $event->objectid);
|
||||
if ($field->type === 'number') {
|
||||
recalculate::schedule_for_field($event->objectid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When a 'number' custom field was updated, schedule recalculation for the field data
|
||||
*
|
||||
* @param \core_customfield\event\field_updated $event
|
||||
*/
|
||||
public static function field_updated(\core_customfield\event\field_updated $event): void {
|
||||
$field = $event->get_record_snapshot('customfield_field', $event->objectid);
|
||||
if ($field->type === 'number') {
|
||||
recalculate::schedule_for_field($event->objectid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When a course module was created, schedule recalculation for all 'nofactivities' custom fields
|
||||
*
|
||||
* @param \core\event\course_module_created $event
|
||||
*/
|
||||
public static function course_module_created(\core\event\course_module_created $event): void {
|
||||
if (self::has_nofactivities_fields()) {
|
||||
recalculate::schedule_for_fieldtype(fieldtype: nofactivities::class,
|
||||
component: 'core_course', area: 'course', instanceid: $event->courseid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When a course module was deleted, schedule recalculation for all 'nofactivities' custom fields
|
||||
*
|
||||
* @param \core\event\course_module_deleted $event
|
||||
*/
|
||||
public static function course_module_deleted(\core\event\course_module_deleted $event): void {
|
||||
if (self::has_nofactivities_fields()) {
|
||||
recalculate::schedule_for_fieldtype(fieldtype: nofactivities::class,
|
||||
component: 'core_course', area: 'course', instanceid: $event->courseid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When a course module was updated, schedule recalculation for all 'nofactivities' custom fields
|
||||
*
|
||||
* Module visibility may change following an 'updated' event and it will affect the activities count
|
||||
*
|
||||
* @param \core\event\course_module_updated $event
|
||||
*/
|
||||
public static function course_module_updated(\core\event\course_module_updated $event): void {
|
||||
if (self::has_nofactivities_fields()) {
|
||||
recalculate::schedule_for_fieldtype(fieldtype: nofactivities::class,
|
||||
component: 'core_course', area: 'course', instanceid: $event->courseid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a 'number' field with 'nofactivities' provider exists in the course fields
|
||||
*
|
||||
* This method is very fast, it only performs one DB query and only once per request
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected static function has_nofactivities_fields(): bool {
|
||||
$handler = \core_course\customfield\course_handler::create();
|
||||
foreach ($handler->get_categories_with_fields() as $category) {
|
||||
foreach ($category->get_fields() as $field) {
|
||||
if ($field->get('type') === 'number' &&
|
||||
$field->get_configdata_property('fieldtype') === nofactivities::class) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
118
customfield/field/number/classes/task/recalculate.php
Normal file
118
customfield/field/number/classes/task/recalculate.php
Normal file
@ -0,0 +1,118 @@
|
||||
<?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 customfield_number\task;
|
||||
|
||||
use coding_exception;
|
||||
use core\task\adhoc_task;
|
||||
use core_customfield\field_controller;
|
||||
use customfield_number\provider_base;
|
||||
|
||||
/**
|
||||
* Recalculates data for the given number field with a provider
|
||||
*
|
||||
* @since Moodle 4.5.1
|
||||
* @package customfield_number
|
||||
* @copyright Marina Glancy
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class recalculate extends adhoc_task {
|
||||
#[\Override]
|
||||
public function execute() {
|
||||
global $DB;
|
||||
$customdata = $this->get_custom_data();
|
||||
$fieldid = clean_param($customdata->fieldid ?? null, PARAM_INT);
|
||||
$fieldtype = $customdata->fieldtype ?? null;
|
||||
|
||||
// Find all fields that we need to recalculate (either by 'fieldid' or 'fieldtype').
|
||||
$fields = [];
|
||||
if ($fieldid) {
|
||||
try {
|
||||
$fields[] = field_controller::create($fieldid);
|
||||
} catch (\Exception $e) {
|
||||
// Could be a race condition when the field was already deleted by the time ad-hoc task runs.
|
||||
return;
|
||||
}
|
||||
} else if ($fieldtype) {
|
||||
$records = $DB->get_records('customfield_field', ['type' => 'number']);
|
||||
foreach ($records as $record) {
|
||||
$configdata = @json_decode($record->configdata, true);
|
||||
if (($configdata['fieldtype'] ?? '') === $fieldtype) {
|
||||
$fields[] = field_controller::create(0, $record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule recalculate for each field, checking component, area and the presense of provider.
|
||||
$instanceid = clean_param($customdata->instanceid ?? null, PARAM_INT);
|
||||
foreach ($fields as $field) {
|
||||
if ($this->field_is_scheduled($field) && ($provider = provider_base::instance($field))) {
|
||||
$provider->recalculate($instanceid ?: null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method validating that the field should be recalculated
|
||||
*
|
||||
* @param \core_customfield\field_controller $field
|
||||
* @return bool
|
||||
*/
|
||||
protected function field_is_scheduled(field_controller $field): bool {
|
||||
$customdata = $this->get_custom_data();
|
||||
if (!empty($customdata->component) && $field->get_handler()->get_component() !== $customdata->component) {
|
||||
return false;
|
||||
}
|
||||
if (!empty($customdata->area) && $field->get_handler()->get_area() !== $customdata->area) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule recalculation for the given number custom field (and optionally, instanceid)
|
||||
*
|
||||
* @param int $fieldid in of the custom field
|
||||
* @param int|null $instanceid if specified, only recalculates for the given instance id
|
||||
* @return void
|
||||
*/
|
||||
public static function schedule_for_field(int $fieldid, ?int $instanceid = null) {
|
||||
$task = new static();
|
||||
$task->set_custom_data(['fieldid' => $fieldid, 'instanceid' => $instanceid]);
|
||||
\core\task\manager::queue_adhoc_task($task, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule recalculation for all number custom fields that use the provider (optionally with instanceid)
|
||||
*
|
||||
* @param string $fieldtype name of the class extending provider_base
|
||||
* @param string|null $component
|
||||
* @param string|null $area
|
||||
* @param int|null $instanceid
|
||||
* @return void
|
||||
*/
|
||||
public static function schedule_for_fieldtype(string $fieldtype, ?string $component = null, ?string $area = null,
|
||||
?int $instanceid = null) {
|
||||
$task = new static();
|
||||
$task->set_custom_data([
|
||||
'fieldtype' => $fieldtype,
|
||||
'component' => $component,
|
||||
'area' => $area,
|
||||
'instanceid' => $instanceid,
|
||||
]);
|
||||
\core\task\manager::queue_adhoc_task($task, true);
|
||||
}
|
||||
}
|
49
customfield/field/number/db/events.php
Normal file
49
customfield/field/number/db/events.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Event observers for Number
|
||||
*
|
||||
* @package customfield_number
|
||||
* @category event
|
||||
* @copyright Marina Glancy
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$observers = [
|
||||
[
|
||||
'eventname' => core_customfield\event\field_created::class,
|
||||
'callback' => 'customfield_number\observer::field_created',
|
||||
],
|
||||
[
|
||||
'eventname' => core_customfield\event\field_updated::class,
|
||||
'callback' => 'customfield_number\observer::field_updated',
|
||||
],
|
||||
[
|
||||
'eventname' => core\event\course_module_created::class,
|
||||
'callback' => 'customfield_number\observer::course_module_created',
|
||||
],
|
||||
[
|
||||
'eventname' => core\event\course_module_deleted::class,
|
||||
'callback' => 'customfield_number\observer::course_module_deleted',
|
||||
],
|
||||
[
|
||||
'eventname' => core\event\course_module_updated::class,
|
||||
'callback' => 'customfield_number\observer::course_module_updated',
|
||||
],
|
||||
];
|
245
customfield/field/number/tests/observer_test.php
Normal file
245
customfield/field/number/tests/observer_test.php
Normal file
@ -0,0 +1,245 @@
|
||||
<?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 customfield_number;
|
||||
|
||||
use context_module;
|
||||
use customfield_number\local\numberproviders\nofactivities;
|
||||
use customfield_number\task\recalculate;
|
||||
|
||||
/**
|
||||
* Testing event observers
|
||||
*
|
||||
* @covers \customfield_number\observer
|
||||
* @covers \customfield_number\task\recalculate
|
||||
* @package customfield_number
|
||||
* @category test
|
||||
* @copyright Marina Glancy
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
final class observer_test extends \advanced_testcase {
|
||||
|
||||
/**
|
||||
* Create a number custom field
|
||||
*
|
||||
* @param array $configdata
|
||||
* @return \customfield_number\field_controller
|
||||
*/
|
||||
protected function create_number_custom_field(array $configdata): field_controller {
|
||||
/** @var \core_customfield_generator $generator */
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_customfield');
|
||||
|
||||
// Create a category and field.
|
||||
$category = $generator->create_category();
|
||||
$field = $generator->create_field([
|
||||
'categoryid' => $category->get('id'),
|
||||
'type' => 'number',
|
||||
'configdata' => $configdata,
|
||||
]);
|
||||
return $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that checks if the recalculate ad-hoc task is scheduled
|
||||
*
|
||||
* @param bool $mustbescheduled - when false checks that the adhoc task is NOT scheduled
|
||||
* @return void
|
||||
*/
|
||||
protected function ensure_number_adhoc_task_is_scheduled(bool $mustbescheduled): void {
|
||||
$tasks = array_filter(
|
||||
\core\task\manager::get_candidate_adhoc_tasks(time(), 1200, null),
|
||||
fn($task) => $task->classname === '\\' . recalculate::class
|
||||
);
|
||||
if ($mustbescheduled && empty($tasks)) {
|
||||
$this->fail('Recalculate ad-hoc task is not scheduled.');
|
||||
} else if (!$mustbescheduled && !empty($tasks)) {
|
||||
$this->fail('Recalculate ad-hoc task is scheduled when it is not expected.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for observer for field_created event
|
||||
*/
|
||||
public function test_field_created(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
$course1 = $this->getDataGenerator()->create_course();
|
||||
$course2 = $this->getDataGenerator()->create_course();
|
||||
$assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
|
||||
$assign1 = $assigngenerator->create_instance(['course' => $course1->id, 'visible' => 1]);
|
||||
$assigngenerator->create_instance(['course' => $course1->id, 'visible' => 1]);
|
||||
|
||||
// Create a number field with a provider.
|
||||
$field = $this->create_number_custom_field(['fieldtype' => nofactivities::class, 'activitytypes' => ['assign', 'forum']]);
|
||||
|
||||
// Execute scheduled ad-hoc tasks and it will populate the data for the course.
|
||||
$this->ensure_number_adhoc_task_is_scheduled(true);
|
||||
$this->run_all_adhoc_tasks();
|
||||
|
||||
$alldata = $DB->get_records_menu('customfield_data',
|
||||
['fieldid' => $field->get('id')], 'instanceid', 'instanceid, decvalue');
|
||||
$this->assertEquals([$course1->id => 2], $alldata);
|
||||
|
||||
// Creating another field type does not schedule tasks.
|
||||
$this->ensure_number_adhoc_task_is_scheduled(false);
|
||||
$this->getDataGenerator()->get_plugin_generator('core_customfield')->create_field((object)[
|
||||
'categoryid' => $field->get_category()->get('id'),
|
||||
'type' => 'textarea',
|
||||
]);
|
||||
$this->ensure_number_adhoc_task_is_scheduled(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for observer for field_updated event
|
||||
*/
|
||||
public function test_field_updated(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
$course1 = $this->getDataGenerator()->create_course();
|
||||
$course2 = $this->getDataGenerator()->create_course();
|
||||
/** @var \mod_assign_generator $assigngenerator */
|
||||
$assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
|
||||
$assign1 = $assigngenerator->create_instance(['course' => $course1->id, 'visible' => 1]);
|
||||
$assigngenerator->create_instance(['course' => $course1->id, 'visible' => 1]);
|
||||
|
||||
// Create a simple number field.
|
||||
$field = $this->create_number_custom_field([]);
|
||||
|
||||
// There is no data for this field yet.
|
||||
$this->ensure_number_adhoc_task_is_scheduled(true);
|
||||
$this->run_all_adhoc_tasks();
|
||||
$this->assertEmpty($DB->get_records('customfield_data', ['fieldid' => $field->get('id')]));
|
||||
|
||||
// Update this field to use nofactivities as provider.
|
||||
$params = ['fieldtype' => nofactivities::class, 'activitytypes' => ['assign', 'forum']];
|
||||
\core_customfield\api::save_field_configuration($field, (object)['configdata' => $params]);
|
||||
|
||||
// Now an ad-hoc task is scheduled and the data is populated.
|
||||
$this->ensure_number_adhoc_task_is_scheduled(true);
|
||||
$this->run_all_adhoc_tasks();
|
||||
|
||||
$alldata = $DB->get_records_menu(
|
||||
'customfield_data',
|
||||
['fieldid' => $field->get('id')],
|
||||
'instanceid',
|
||||
'instanceid, decvalue'
|
||||
);
|
||||
$this->assertEquals([$course1->id => 2], $alldata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for observer for course_module_created, course_module_updated and course_module_deleted events
|
||||
*/
|
||||
public function test_course_module_events(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
$course1 = $this->getDataGenerator()->create_course();
|
||||
$course2 = $this->getDataGenerator()->create_course();
|
||||
|
||||
// Create a number field with a provider.
|
||||
$field = $this->create_number_custom_field(['fieldtype' => nofactivities::class, 'activitytypes' => ['assign', 'forum']]);
|
||||
|
||||
// There is no data for this field yet.
|
||||
$this->ensure_number_adhoc_task_is_scheduled(true);
|
||||
$this->run_all_adhoc_tasks();
|
||||
$this->assertEmpty($DB->get_records('customfield_data', ['fieldid' => $field->get('id')]));
|
||||
|
||||
// Create modules.
|
||||
$assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
|
||||
$assign1 = $assigngenerator->create_instance(['course' => $course1->id, 'visible' => 1]);
|
||||
$assign2 = $assigngenerator->create_instance(['course' => $course1->id, 'visible' => 0]);
|
||||
$assigngenerator->create_instance(['course' => $course1->id, 'visible' => 1]);
|
||||
|
||||
// Execute scheduled ad-hoc tasks and it will populate the data for the course.
|
||||
$this->ensure_number_adhoc_task_is_scheduled(true);
|
||||
$this->run_all_adhoc_tasks();
|
||||
|
||||
$alldata = $DB->get_records_menu('customfield_data',
|
||||
['fieldid' => $field->get('id')], 'instanceid', 'instanceid, decvalue');
|
||||
$this->assertEquals([$course1->id => 2], $alldata);
|
||||
|
||||
// Update visibility of one module.
|
||||
set_coursemodule_visible($assign2->cmid, 1);
|
||||
[$course, $cm] = get_course_and_cm_from_cmid($assign2->cmid);
|
||||
\core\event\course_module_updated::create_from_cm($cm, context_module::instance($assign2->cmid))->trigger();
|
||||
$this->ensure_number_adhoc_task_is_scheduled(true);
|
||||
$this->run_all_adhoc_tasks();
|
||||
|
||||
$alldata = $DB->get_records_menu('customfield_data',
|
||||
['fieldid' => $field->get('id')], 'instanceid', 'instanceid, decvalue');
|
||||
$this->assertEquals([$course1->id => 3], $alldata);
|
||||
|
||||
// Delete one module.
|
||||
course_delete_module($assign1->cmid);
|
||||
|
||||
// Execute scheduled ad-hoc tasks and it will update the data for the course.
|
||||
$this->ensure_number_adhoc_task_is_scheduled(true);
|
||||
$this->run_all_adhoc_tasks();
|
||||
|
||||
$alldata = $DB->get_records_menu('customfield_data',
|
||||
['fieldid' => $field->get('id')], 'instanceid', 'instanceid, decvalue');
|
||||
$this->assertEquals([$course1->id => 2], $alldata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creating, updating and deleting modules when there are no 'nofactivities' custom fields does not schedule the ad-hoc task
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_course_module_events_without_custom_fields(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
$course1 = $this->getDataGenerator()->create_course();
|
||||
$course2 = $this->getDataGenerator()->create_course();
|
||||
|
||||
// Create a number field without a provider.
|
||||
$field = $this->create_number_custom_field([]);
|
||||
|
||||
// Initial ad-hoc task was scheduled.
|
||||
$this->ensure_number_adhoc_task_is_scheduled(true);
|
||||
$this->run_all_adhoc_tasks();
|
||||
|
||||
// Create modules.
|
||||
$assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
|
||||
$assign1 = $assigngenerator->create_instance(['course' => $course1->id, 'visible' => 1]);
|
||||
$assign2 = $assigngenerator->create_instance(['course' => $course1->id, 'visible' => 0]);
|
||||
$assigngenerator->create_instance(['course' => $course1->id, 'visible' => 1]);
|
||||
|
||||
$this->ensure_number_adhoc_task_is_scheduled(false);
|
||||
|
||||
// Update visibility of one module.
|
||||
set_coursemodule_visible($assign2->cmid, 1);
|
||||
[$course, $cm] = get_course_and_cm_from_cmid($assign2->cmid);
|
||||
\core\event\course_module_updated::create_from_cm($cm, context_module::instance($assign2->cmid))->trigger();
|
||||
$this->ensure_number_adhoc_task_is_scheduled(false);
|
||||
|
||||
// Delete one module.
|
||||
course_delete_module($assign1->cmid);
|
||||
$this->ensure_number_adhoc_task_is_scheduled(false);
|
||||
}
|
||||
}
|
@ -25,6 +25,6 @@
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
$plugin->component = 'customfield_number';
|
||||
$plugin->version = 2024100700;
|
||||
$plugin->version = 2024100703;
|
||||
$plugin->requires = 2024100100;
|
||||
$plugin->maturity = MATURITY_STABLE;
|
||||
|
Loading…
x
Reference in New Issue
Block a user