diff --git a/customfield/classes/privacy/customfield_provider.php b/customfield/classes/privacy/customfield_provider.php new file mode 100644 index 00000000000..8d05a5b2bd0 --- /dev/null +++ b/customfield/classes/privacy/customfield_provider.php @@ -0,0 +1,84 @@ +. + +/** + * Contains interface customfield_provider + * + * @package core_customfield + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_customfield\privacy; + +use core_customfield\data_controller; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Interface customfield_provider, all customfield plugins need to implement it + * + * @package core_customfield + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface customfield_provider extends + \core_privacy\local\request\plugin\subplugin_provider, + + // The customfield plugins do not need to do anything themselves for the shared_userlist. + // This is all handled by the component core_customfield. + \core_privacy\local\request\shared_userlist_provider + { + + /** + * Preprocesses data object that is going to be exported + * + * Minimum implementation: + * writer::with_context($data->get_context())->export_data($subcontext, $exportdata); + * + * @param data_controller $data + * @param \stdClass $exportdata generated object to be exported + * @param array $subcontext subcontext to use when exporting + * @return mixed + */ + public static function export_customfield_data(data_controller $data, \stdClass $exportdata, array $subcontext); + + /** + * Allows plugins to delete everything they store related to the data (usually files) + * + * If plugin does not store any related files or other information, implement as an empty function + * + * @param string $dataidstest select query for data id (note that it may also return data for other field types) + * @param array $params named parameters for the select query + * @param array $contextids list of affected data contexts + * @return mixed + */ + public static function before_delete_data(string $dataidstest, array $params, array $contextids); + + /** + * Allows plugins to delete everything they store related to the field configuration (usually files) + * + * The implementation should not delete data or anything related to the data, since "before_delete_data" is + * invoked separately. + * + * If plugin does not store any related files or other information, implement as an empty function + * + * @param string $fieldidstest select query for field id (note that it may also return fields of other types) + * @param array $params named parameters for the select query + * @param int[] $contextids list of affected configuration contexts + */ + public static function before_delete_fields(string $fieldidstest, array $params, array $contextids); +} diff --git a/customfield/field/checkbox/tests/plugin_test.php b/customfield/field/checkbox/tests/plugin_test.php new file mode 100644 index 00000000000..6d117500112 --- /dev/null +++ b/customfield/field/checkbox/tests/plugin_test.php @@ -0,0 +1,177 @@ +. + +/** + * Tests for class customfield_checkbox + * + * @package customfield_checkbox + * @copyright 2019 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use customfield_checkbox\field_controller; +use customfield_checkbox\data_controller; + +/** + * Functional test for customfield_checkbox + * + * @package customfield_checkbox + * @copyright 2019 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class customfield_checkbox_plugin_testcase extends advanced_testcase { + + /** @var stdClass[] */ + private $courses = []; + /** @var \core_customfield\category_controller */ + private $cfcat; + /** @var \core_customfield\field_controller[] */ + private $cfields; + /** @var \core_customfield\data_controller[] */ + private $cfdata; + + /** + * Tests set up. + */ + public function setUp() { + $this->resetAfterTest(); + + $this->cfcat = $this->get_generator()->create_category(); + + $this->cfields[1] = $this->get_generator()->create_field( + ['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield1', 'type' => 'checkbox']); + $this->cfields[2] = $this->get_generator()->create_field( + ['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield2', 'type' => 'checkbox', + 'configdata' => ['required' => 1]]); + $this->cfields[3] = $this->get_generator()->create_field( + ['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield3', 'type' => 'checkbox', + 'configdata' => ['checkbydefault' => 1]]); + + $this->courses[1] = $this->getDataGenerator()->create_course(); + $this->courses[2] = $this->getDataGenerator()->create_course(); + $this->courses[3] = $this->getDataGenerator()->create_course(); + + $this->cfdata[1] = $this->get_generator()->add_instance_data($this->cfields[1], $this->courses[1]->id, 1); + $this->cfdata[2] = $this->get_generator()->add_instance_data($this->cfields[1], $this->courses[2]->id, 1); + + $this->setUser($this->getDataGenerator()->create_user()); + } + + /** + * Get generator + * @return core_customfield_generator + */ + protected function get_generator(): core_customfield_generator { + return $this->getDataGenerator()->get_plugin_generator('core_customfield'); + } + + /** + * Test for initialising field and data controllers + */ + public function test_initialise() { + $f = \core_customfield\field_controller::create($this->cfields[1]->get('id')); + $this->assertTrue($f instanceof field_controller); + + $f = \core_customfield\field_controller::create(0, (object)['type' => 'checkbox'], $this->cfcat); + $this->assertTrue($f instanceof field_controller); + + $d = \core_customfield\data_controller::create($this->cfdata[1]->get('id')); + $this->assertTrue($d instanceof data_controller); + + $d = \core_customfield\data_controller::create(0, null, $this->cfields[1]); + $this->assertTrue($d instanceof data_controller); + } + + /** + * Test for configuration form functions + * + * Create a configuration form and submit it with the same values as in the field + */ + public function test_config_form() { + $submitdata = (array)$this->cfields[1]->to_record(); + $submitdata['configdata'] = $this->cfields[1]->get('configdata'); + + \core_customfield\field_config_form::mock_submit($submitdata, []); + $handler = $this->cfcat->get_handler(); + $form = $handler->get_field_config_form($this->cfields[1]); + $this->assertTrue($form->is_validated()); + $data = $form->get_data(); + $handler->save_field_configuration($this->cfields[1], $data); + + // Try submitting with 'unique values' checked. + $submitdata['configdata']['uniquevalues'] = 1; + \core_customfield\field_config_form::mock_submit($submitdata, []); + $handler = $this->cfcat->get_handler(); + $form = $handler->get_field_config_form($this->cfields[1]); + $this->assertFalse($form->is_validated()); + } + + /** + * Test for instance form functions + */ + public function test_instance_form() { + global $CFG; + require_once($CFG->dirroot . '/customfield/tests/fixtures/test_instance_form.php'); + $this->setAdminUser(); + $handler = $this->cfcat->get_handler(); + + // First try to submit without required field. + $submitdata = (array)$this->courses[1]; + core_customfield_test_instance_form::mock_submit($submitdata, []); + $form = new core_customfield_test_instance_form('POST', + ['handler' => $handler, 'instance' => $this->courses[1]]); + $this->assertFalse($form->is_validated()); + + // Now with required field. + $submitdata['customfield_myfield2'] = 1; + core_customfield_test_instance_form::mock_submit($submitdata, []); + $form = new core_customfield_test_instance_form('POST', + ['handler' => $handler, 'instance' => $this->courses[1]]); + $this->assertTrue($form->is_validated()); + + $data = $form->get_data(); + $this->assertNotEmpty($data->customfield_myfield1); + $this->assertNotEmpty($data->customfield_myfield2); + $handler->instance_form_save($data); + } + + /** + * Test for data_controller::get_value and export_value + */ + public function test_get_export_value() { + $this->assertEquals(1, $this->cfdata[1]->get_value()); + $this->assertEquals('Yes', $this->cfdata[1]->export_value()); + + // Field without data. + $d = core_customfield\data_controller::create(0, null, $this->cfields[2]); + $this->assertEquals(0, $d->get_value()); + $this->assertEquals('No', $d->export_value()); + + // Field without data that is checked by default. + $d = core_customfield\data_controller::create(0, null, $this->cfields[3]); + $this->assertEquals(1, $d->get_value()); + $this->assertEquals('Yes', $d->export_value()); + } + + /** + * Deleting fields and data + */ + public function test_delete() { + $this->cfcat->get_handler()->delete_all(); + } +} diff --git a/customfield/field/date/tests/plugin_test.php b/customfield/field/date/tests/plugin_test.php new file mode 100644 index 00000000000..95b4876c7a5 --- /dev/null +++ b/customfield/field/date/tests/plugin_test.php @@ -0,0 +1,180 @@ +. + +/** + * Tests for class customfield_date + * + * @package customfield_date + * @copyright 2019 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use customfield_date\field_controller; +use customfield_date\data_controller; + +/** + * Functional test for customfield_date + * + * @package customfield_date + * @copyright 2019 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class customfield_date_plugin_testcase extends advanced_testcase { + + /** @var stdClass[] */ + private $courses = []; + /** @var \core_customfield\category_controller */ + private $cfcat; + /** @var \core_customfield\field_controller[] */ + private $cfields; + /** @var \core_customfield\data_controller[] */ + private $cfdata; + + /** + * Tests set up. + */ + public function setUp() { + $this->resetAfterTest(); + + $this->cfcat = $this->get_generator()->create_category(); + + $this->cfields[1] = $this->get_generator()->create_field( + ['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield1', 'type' => 'date']); + $this->cfields[2] = $this->get_generator()->create_field( + ['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield2', 'type' => 'date', + 'configdata' => ['required' => 1, 'includetime' => 0, 'mindate' => 946684800, 'maxdate' => 1893456000]]); + + $this->courses[1] = $this->getDataGenerator()->create_course(); + $this->courses[2] = $this->getDataGenerator()->create_course(); + $this->courses[3] = $this->getDataGenerator()->create_course(); + + $this->cfdata[1] = $this->get_generator()->add_instance_data($this->cfields[1], $this->courses[1]->id, 1546300800); + $this->cfdata[2] = $this->get_generator()->add_instance_data($this->cfields[1], $this->courses[2]->id, 1546300800); + + $this->setUser($this->getDataGenerator()->create_user()); + } + + /** + * Get generator + * @return core_customfield_generator + */ + protected function get_generator(): core_customfield_generator { + return $this->getDataGenerator()->get_plugin_generator('core_customfield'); + } + + /** + * Test for initialising field and data controllers + */ + public function test_initialise() { + $f = \core_customfield\field_controller::create($this->cfields[1]->get('id')); + $this->assertTrue($f instanceof field_controller); + + $f = \core_customfield\field_controller::create(0, (object)['type' => 'date'], $this->cfcat); + $this->assertTrue($f instanceof field_controller); + + $d = \core_customfield\data_controller::create($this->cfdata[1]->get('id')); + $this->assertTrue($d instanceof data_controller); + + $d = \core_customfield\data_controller::create(0, null, $this->cfields[1]); + $this->assertTrue($d instanceof data_controller); + } + + /** + * Test for configuration form functions + * + * Create a configuration form and submit it with the same values as in the field + */ + public function test_config_form() { + $submitdata = (array)$this->cfields[1]->to_record(); + $submitdata['configdata'] = $this->cfields[1]->get('configdata'); + + \core_customfield\field_config_form::mock_submit($submitdata, []); + $handler = $this->cfcat->get_handler(); + $form = $handler->get_field_config_form($this->cfields[1]); + $this->assertTrue($form->is_validated()); + $data = $form->get_data(); + $handler->save_field_configuration($this->cfields[1], $data); + } + + /** + * Test for instance form functions + */ + public function test_instance_form() { + global $CFG; + require_once($CFG->dirroot . '/customfield/tests/fixtures/test_instance_form.php'); + $this->setAdminUser(); + $handler = $this->cfcat->get_handler(); + + // First try to submit without required field. + $submitdata = (array)$this->courses[1]; + core_customfield_test_instance_form::mock_submit($submitdata, []); + $form = new core_customfield_test_instance_form('POST', + ['handler' => $handler, 'instance' => $this->courses[1]]); + $this->assertFalse($form->is_validated()); + + // Now with required field. + $submitdata['customfield_myfield2'] = time(); + core_customfield_test_instance_form::mock_submit($submitdata, []); + $form = new core_customfield_test_instance_form('POST', + ['handler' => $handler, 'instance' => $this->courses[1]]); + $this->assertTrue($form->is_validated()); + + $data = $form->get_data(); + $this->assertEmpty($data->customfield_myfield1); + $this->assertNotEmpty($data->customfield_myfield2); + $handler->instance_form_save($data); + } + + /** + * Test for min/max date validation + */ + public function test_instance_form_validation() { + $this->setAdminUser(); + $handler = $this->cfcat->get_handler(); + $submitdata = (array)$this->courses[1]; + $data = data_controller::create(0, null, $this->cfields[2]); + + // Submit with date less than mindate. + $submitdata['customfield_myfield2'] = 915148800; + $this->assertNotEmpty($data->instance_form_validation($submitdata, [])); + + // Submit with date more than maxdate. + $submitdata['customfield_myfield2'] = 1893557000; + $this->assertNotEmpty($data->instance_form_validation($submitdata, [])); + } + + /** + * Test for data_controller::get_value and export_value + */ + public function test_get_export_value() { + $this->assertEquals(1546300800, $this->cfdata[1]->get_value()); + $this->assertStringMatchesFormat('%a 1 January 2019%a', $this->cfdata[1]->export_value()); + + // Field without data. + $d = core_customfield\data_controller::create(0, null, $this->cfields[2]); + $this->assertEquals(0, $d->get_value()); + $this->assertEquals(null, $d->export_value()); + } + + /** + * Deleting fields and data + */ + public function test_delete() { + $this->cfcat->get_handler()->delete_all(); + } +} diff --git a/customfield/field/select/tests/plugin_test.php b/customfield/field/select/tests/plugin_test.php new file mode 100644 index 00000000000..de630d5e1d4 --- /dev/null +++ b/customfield/field/select/tests/plugin_test.php @@ -0,0 +1,166 @@ +. + +/** + * Tests for class customfield_select + * + * @package customfield_select + * @copyright 2019 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use customfield_select\field_controller; +use customfield_select\data_controller; + +/** + * Functional test for customfield_select + * + * @package customfield_select + * @copyright 2019 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class customfield_select_plugin_testcase extends advanced_testcase { + + /** @var stdClass[] */ + private $courses = []; + /** @var \core_customfield\category_controller */ + private $cfcat; + /** @var \core_customfield\field_controller[] */ + private $cfields; + /** @var \core_customfield\data_controller[] */ + private $cfdata; + + /** + * Tests set up. + */ + public function setUp() { + $this->resetAfterTest(); + + $this->cfcat = $this->get_generator()->create_category(); + + $this->cfields[1] = $this->get_generator()->create_field( + ['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield1', 'type' => 'select', + 'configdata' => ['options' => "a\nb\nc"]]); + $this->cfields[2] = $this->get_generator()->create_field( + ['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield2', 'type' => 'select', + 'configdata' => ['required' => 1, 'options' => "a\nb\nc"]]); + $this->cfields[3] = $this->get_generator()->create_field( + ['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield3', 'type' => 'select', + 'configdata' => ['defaultvalue' => 'b', 'options' => "a\nb\nc"]]); + + $this->courses[1] = $this->getDataGenerator()->create_course(); + $this->courses[2] = $this->getDataGenerator()->create_course(); + $this->courses[3] = $this->getDataGenerator()->create_course(); + + $this->cfdata[1] = $this->get_generator()->add_instance_data($this->cfields[1], $this->courses[1]->id, 1); + $this->cfdata[2] = $this->get_generator()->add_instance_data($this->cfields[1], $this->courses[2]->id, 1); + + $this->setUser($this->getDataGenerator()->create_user()); + } + + /** + * Get generator + * @return core_customfield_generator + */ + protected function get_generator(): core_customfield_generator { + return $this->getDataGenerator()->get_plugin_generator('core_customfield'); + } + + /** + * Test for initialising field and data controllers + */ + public function test_initialise() { + $f = \core_customfield\field_controller::create($this->cfields[1]->get('id')); + $this->assertTrue($f instanceof field_controller); + + $f = \core_customfield\field_controller::create(0, (object)['type' => 'select'], $this->cfcat); + $this->assertTrue($f instanceof field_controller); + + $d = \core_customfield\data_controller::create($this->cfdata[1]->get('id')); + $this->assertTrue($d instanceof data_controller); + + $d = \core_customfield\data_controller::create(0, null, $this->cfields[1]); + $this->assertTrue($d instanceof data_controller); + } + + /** + * Test for configuration form functions + * + * Create a configuration form and submit it with the same values as in the field + */ + public function test_config_form() { + $submitdata = (array)$this->cfields[1]->to_record(); + $submitdata['configdata'] = $this->cfields[1]->get('configdata'); + + \core_customfield\field_config_form::mock_submit($submitdata, []); + $handler = $this->cfcat->get_handler(); + $form = $handler->get_field_config_form($this->cfields[1]); + $this->assertTrue($form->is_validated()); + $data = $form->get_data(); + $handler->save_field_configuration($this->cfields[1], $data); + } + + /** + * Test for instance form functions + */ + public function test_instance_form() { + global $CFG; + require_once($CFG->dirroot . '/customfield/tests/fixtures/test_instance_form.php'); + $this->setAdminUser(); + $handler = $this->cfcat->get_handler(); + + // First try to submit without required field. + $submitdata = (array)$this->courses[1]; + core_customfield_test_instance_form::mock_submit($submitdata, []); + $form = new core_customfield_test_instance_form('POST', + ['handler' => $handler, 'instance' => $this->courses[1]]); + $this->assertFalse($form->is_validated()); + + // Now with required field. + $submitdata['customfield_myfield2'] = 1; + core_customfield_test_instance_form::mock_submit($submitdata, []); + $form = new core_customfield_test_instance_form('POST', + ['handler' => $handler, 'instance' => $this->courses[1]]); + $this->assertTrue($form->is_validated()); + + $data = $form->get_data(); + $this->assertNotEmpty($data->customfield_myfield1); + $this->assertNotEmpty($data->customfield_myfield2); + $handler->instance_form_save($data); + } + + /** + * Test for data_controller::get_value and export_value + */ + public function test_get_export_value() { + $this->assertEquals(1, $this->cfdata[1]->get_value()); + $this->assertEquals('a', $this->cfdata[1]->export_value()); + + // Field without data but with a default value. + $d = core_customfield\data_controller::create(0, null, $this->cfields[3]); + $this->assertEquals(2, $d->get_value()); + $this->assertEquals('b', $d->export_value()); + } + + /** + * Deleting fields and data + */ + public function test_delete() { + $this->cfcat->get_handler()->delete_all(); + } +} diff --git a/customfield/field/text/tests/plugin_test.php b/customfield/field/text/tests/plugin_test.php new file mode 100644 index 00000000000..7a2379fd6e6 --- /dev/null +++ b/customfield/field/text/tests/plugin_test.php @@ -0,0 +1,176 @@ +. + +/** + * Tests for class customfield_text + * + * @package customfield_text + * @copyright 2019 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use customfield_text\field_controller; +use customfield_text\data_controller; + +/** + * Functional test for customfield_text + * + * @package customfield_text + * @copyright 2019 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class customfield_text_plugin_testcase extends advanced_testcase { + + /** @var stdClass[] */ + private $courses = []; + /** @var \core_customfield\category_controller */ + private $cfcat; + /** @var \core_customfield\field_controller[] */ + private $cfields; + /** @var \core_customfield\data_controller[] */ + private $cfdata; + + /** + * Tests set up. + */ + public function setUp() { + $this->resetAfterTest(); + + $this->cfcat = $this->get_generator()->create_category(); + + $this->cfields[1] = $this->get_generator()->create_field( + ['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield1', 'type' => 'text', + 'configdata' => ['maxlength' => 30, 'displaysize' => 50]]); + $this->cfields[2] = $this->get_generator()->create_field( + ['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield2', 'type' => 'text', + 'configdata' => ['required' => 1, 'maxlength' => 30, 'displaysize' => 50]]); + $this->cfields[3] = $this->get_generator()->create_field( + ['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield3', 'type' => 'text', + 'configdata' => ['defaultvalue' => 'Defvalue', 'maxlength' => 30, 'displaysize' => 50]]); + $this->cfields[4] = $this->get_generator()->create_field( + ['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield4', 'type' => 'text', + 'configdata' => ['link' => 'https://twitter.com/$$', 'maxlength' => 30, 'displaysize' => 50]]); + + $this->courses[1] = $this->getDataGenerator()->create_course(); + $this->courses[2] = $this->getDataGenerator()->create_course(); + $this->courses[3] = $this->getDataGenerator()->create_course(); + + $this->cfdata[1] = $this->get_generator()->add_instance_data($this->cfields[1], $this->courses[1]->id, + 'Value1'); + $this->cfdata[2] = $this->get_generator()->add_instance_data($this->cfields[1], $this->courses[2]->id, + 'Value2'); + + $this->setUser($this->getDataGenerator()->create_user()); + } + + /** + * Get generator + * @return core_customfield_generator + */ + protected function get_generator(): core_customfield_generator { + return $this->getDataGenerator()->get_plugin_generator('core_customfield'); + } + + /** + * Test for initialising field and data controllers + */ + public function test_initialise() { + $f = \core_customfield\field_controller::create($this->cfields[1]->get('id')); + $this->assertTrue($f instanceof field_controller); + + $f = \core_customfield\field_controller::create(0, (object)['type' => 'text'], $this->cfcat); + $this->assertTrue($f instanceof field_controller); + + $d = \core_customfield\data_controller::create($this->cfdata[1]->get('id')); + $this->assertTrue($d instanceof data_controller); + + $d = \core_customfield\data_controller::create(0, null, $this->cfields[1]); + $this->assertTrue($d instanceof data_controller); + } + + /** + * Test for configuration form functions + * + * Create a configuration form and submit it with the same values as in the field + */ + public function test_config_form() { + $submitdata = (array)$this->cfields[1]->to_record(); + $submitdata['configdata'] = $this->cfields[1]->get('configdata'); + + \core_customfield\field_config_form::mock_submit($submitdata, []); + $handler = $this->cfcat->get_handler(); + $form = $handler->get_field_config_form($this->cfields[1]); + $this->assertTrue($form->is_validated()); + $data = $form->get_data(); + $handler->save_field_configuration($this->cfields[1], $data); + } + + /** + * Test for instance form functions + */ + public function test_instance_form() { + global $CFG; + require_once($CFG->dirroot . '/customfield/tests/fixtures/test_instance_form.php'); + $this->setAdminUser(); + $handler = $this->cfcat->get_handler(); + + // First try to submit without required field. + $submitdata = (array)$this->courses[1]; + core_customfield_test_instance_form::mock_submit($submitdata, []); + $form = new core_customfield_test_instance_form('POST', + ['handler' => $handler, 'instance' => $this->courses[1]]); + $this->assertFalse($form->is_validated()); + + // Now with required field. + $submitdata['customfield_myfield2'] = 'Some text'; + core_customfield_test_instance_form::mock_submit($submitdata, []); + $form = new core_customfield_test_instance_form('POST', + ['handler' => $handler, 'instance' => $this->courses[1]]); + $this->assertTrue($form->is_validated()); + + $data = $form->get_data(); + $this->assertNotEmpty($data->customfield_myfield1); + $this->assertNotEmpty($data->customfield_myfield2); + $handler->instance_form_save($data); + } + + /** + * Test for data_controller::get_value and export_value + */ + public function test_get_export_value() { + $this->assertEquals('Value1', $this->cfdata[1]->get_value()); + $this->assertEquals('Value1', $this->cfdata[1]->export_value()); + + // Field without data but with a default value. + $d = core_customfield\data_controller::create(0, null, $this->cfields[3]); + $this->assertEquals('Defvalue', $d->get_value()); + $this->assertEquals('Defvalue', $d->export_value()); + + // Field with a link. + $d = $this->get_generator()->add_instance_data($this->cfields[4], $this->courses[1]->id, 'mynickname'); + $this->assertEquals('mynickname', $d->get_value()); + $this->assertEquals('mynickname', $d->export_value()); + } + + /** + * Deleting fields and data + */ + public function test_delete() { + $this->cfcat->get_handler()->delete_all(); + } +} diff --git a/customfield/field/textarea/lib.php b/customfield/field/textarea/lib.php new file mode 100644 index 00000000000..83389f37dad --- /dev/null +++ b/customfield/field/textarea/lib.php @@ -0,0 +1,76 @@ +. + +/** + * Callbacks + * + * @package customfield_textarea + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +/** + * Serve the files from the customfield_textarea file areas + * + * @param stdClass $course the course object + * @param stdClass $cm the course module object + * @param context $context the context + * @param string $filearea the name of the file area + * @param array $args extra arguments (itemid, path) + * @param bool $forcedownload whether or not force download + * @param array $options additional options affecting the file serving + * @return bool false if the file not found, just send the file otherwise and do not return + */ +function customfield_textarea_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) { + global $DB; + + $itemid = array_shift($args); + if ($filearea === 'value') { + // Value of the data, itemid = id in data table. + $datarecord = $DB->get_record(\core_customfield\data::TABLE, ['id' => $itemid], '*', MUST_EXIST); + $field = \core_customfield\field_controller::create($datarecord->fieldid); + $data = \core_customfield\data_controller::create(0, $datarecord, $field); + $handler = $field->get_handler(); + if ($field->get('type') !== 'textarea' || !$handler->can_view($field, $data->get('instanceid')) + || $data->get_context()->id != $context->id) { + send_file_not_found(); + } + } else if ($filearea === 'defaultvalue') { + // Default value of the field, itemid = id in the field table. + $field = \core_customfield\field_controller::create($itemid); + $handler = $field->get_handler(); + if ($field->get('type') !== 'textarea' || $handler->get_configuration_context()->id != $context->id) { + send_file_not_found(); + } + } else { + send_file_not_found(); + } + + $filename = array_pop($args); // The last item in the $args array. + $filepath = '/' . ($args ? implode('/', $args) . '/' : ''); + + // Retrieve the file from the Files API. + $fs = get_file_storage(); + $file = $fs->get_file($context->id, 'customfield_textarea', $filearea, $itemid, $filepath, $filename); + if (!$file) { + send_file_not_found(); + } + + // We can now send the file back to the browser - in this case with a cache lifetime of 1 day and no filtering. + send_file($file, 86400, 0, $forcedownload, $options); +} diff --git a/customfield/field/textarea/tests/plugin_test.php b/customfield/field/textarea/tests/plugin_test.php new file mode 100644 index 00000000000..2e8619d7ce2 --- /dev/null +++ b/customfield/field/textarea/tests/plugin_test.php @@ -0,0 +1,167 @@ +. + +/** + * Tests for class customfield_textarea + * + * @package customfield_textarea + * @copyright 2019 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use customfield_textarea\field_controller; +use customfield_textarea\data_controller; + +/** + * Functional test for customfield_textarea + * + * @package customfield_textarea + * @copyright 2019 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class customfield_textarea_plugin_testcase extends advanced_testcase { + + /** @var stdClass[] */ + private $courses = []; + /** @var \core_customfield\category_controller */ + private $cfcat; + /** @var \core_customfield\field_controller[] */ + private $cfields; + /** @var \core_customfield\data_controller[] */ + private $cfdata; + + /** + * Tests set up. + */ + public function setUp() { + $this->resetAfterTest(); + + $this->cfcat = $this->get_generator()->create_category(); + + $this->cfields[1] = $this->get_generator()->create_field( + ['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield1', 'type' => 'textarea']); + $this->cfields[2] = $this->get_generator()->create_field( + ['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield2', 'type' => 'textarea', + 'configdata' => ['required' => 1]]); + $this->cfields[3] = $this->get_generator()->create_field( + ['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield3', 'type' => 'textarea', + 'configdata' => ['defaultvalue' => 'Value3', 'defaultvalueformat' => FORMAT_MOODLE]]); + + $this->courses[1] = $this->getDataGenerator()->create_course(); + $this->courses[2] = $this->getDataGenerator()->create_course(); + $this->courses[3] = $this->getDataGenerator()->create_course(); + + $this->cfdata[1] = $this->get_generator()->add_instance_data($this->cfields[1], $this->courses[1]->id, + ['text' => 'Value1', 'format' => FORMAT_MOODLE]); + $this->cfdata[2] = $this->get_generator()->add_instance_data($this->cfields[1], $this->courses[2]->id, + ['text' => 'Value2', 'format' => FORMAT_MOODLE]); + + $this->setUser($this->getDataGenerator()->create_user()); + } + + /** + * Get generator + * @return core_customfield_generator + */ + protected function get_generator(): core_customfield_generator { + return $this->getDataGenerator()->get_plugin_generator('core_customfield'); + } + + /** + * Test for initialising field and data controllers + */ + public function test_initialise() { + $f = \core_customfield\field_controller::create($this->cfields[1]->get('id')); + $this->assertTrue($f instanceof field_controller); + + $f = \core_customfield\field_controller::create(0, (object)['type' => 'textarea'], $this->cfcat); + $this->assertTrue($f instanceof field_controller); + + $d = \core_customfield\data_controller::create($this->cfdata[1]->get('id')); + $this->assertTrue($d instanceof data_controller); + + $d = \core_customfield\data_controller::create(0, null, $this->cfields[1]); + $this->assertTrue($d instanceof data_controller); + } + + /** + * Test for configuration form functions + * + * Create a configuration form and submit it with the same values as in the field + */ + public function test_config_form() { + $submitdata = (array)$this->cfields[3]->to_record(); + $submitdata['configdata'] = $this->cfields[3]->get('configdata'); + + \core_customfield\field_config_form::mock_submit($submitdata, []); + $handler = $this->cfcat->get_handler(); + $form = $handler->get_field_config_form($this->cfields[3]); + $this->assertTrue($form->is_validated()); + $data = $form->get_data(); + $handler->save_field_configuration($this->cfields[3], $data); + } + + /** + * Test for instance form functions + */ + public function test_instance_form() { + global $CFG; + require_once($CFG->dirroot . '/customfield/tests/fixtures/test_instance_form.php'); + $this->setAdminUser(); + $handler = $this->cfcat->get_handler(); + + // First try to submit without required field. + $submitdata = (array)$this->courses[1]; + core_customfield_test_instance_form::mock_submit($submitdata, []); + $form = new core_customfield_test_instance_form('POST', + ['handler' => $handler, 'instance' => $this->courses[1]]); + $this->assertFalse($form->is_validated()); + + // Now with required field. + $submitdata['customfield_myfield2_editor'] = ['text' => 'Some text', 'format' => FORMAT_HTML]; + core_customfield_test_instance_form::mock_submit($submitdata, []); + $form = new core_customfield_test_instance_form('POST', + ['handler' => $handler, 'instance' => $this->courses[1]]); + $this->assertTrue($form->is_validated()); + + $data = $form->get_data(); + $this->assertNotEmpty($data->customfield_myfield1_editor); + $this->assertNotEmpty($data->customfield_myfield2_editor); + $handler->instance_form_save($data); + } + + /** + * Test for data_controller::get_value and export_value + */ + public function test_get_export_value() { + $this->assertEquals('Value1', $this->cfdata[1]->get_value()); + $this->assertEquals('
Hi there
', 'format' => FORMAT_HTML]); + + $this->get_generator()->add_instance_data($this->cffields[21], $this->courses[1]->id, 'hihi1'); + + $this->get_generator()->add_instance_data($this->cffields[14], $this->courses[2]->id, 'Hello2'); + + $this->get_generator()->add_instance_data($this->cffields[21], $this->courses[2]->id, 'hihi2'); + + $this->setUser($this->getDataGenerator()->create_user()); + } + + /** + * Get generator + * @return core_customfield_generator + */ + protected function get_generator(): core_customfield_generator { + return $this->getDataGenerator()->get_plugin_generator('core_customfield'); + } + + /** + * Test for provider::get_metadata() + */ + public function test_get_metadata() { + $collection = new \core_privacy\local\metadata\collection('core_customfield'); + $collection = provider::get_metadata($collection); + $this->assertNotEmpty($collection); + } + + /** + * Test for provider::get_customfields_data_contexts + */ + public function test_get_customfields_data_contexts() { + global $DB; + list($sql, $params) = $DB->get_in_or_equal([$this->courses[1]->id, $this->courses[2]->id], SQL_PARAMS_NAMED); + $r = provider::get_customfields_data_contexts('core_course', 'course', '=0', + $sql, $params); + $this->assertEquals([context_course::instance($this->courses[1]->id)->id, + context_course::instance($this->courses[2]->id)->id], + $r->get_contextids(), '', 0, 10, true); + } + + /** + * Test for provider::get_customfields_configuration_contexts() + */ + public function test_get_customfields_configuration_contexts() { + $r = provider::get_customfields_configuration_contexts('core_course', 'course'); + $this->assertEquals([context_system::instance()->id], $r->get_contextids()); + } + + /** + * Test for provider::export_customfields_data() + */ + public function test_export_customfields_data() { + global $USER, $DB; + // Hack one of the fields so it has an invalid field type. + $invalidfieldid = $this->cffields[21]->get('id'); + $DB->update_record('customfield_field', ['id' => $invalidfieldid, 'type' => 'invalid']); + + $context = context_course::instance($this->courses[1]->id); + $contextlist = new approved_contextlist($USER, 'core_customfield', [$context->id]); + provider::export_customfields_data($contextlist, 'core_course', 'course', '=0', '=:i', ['i' => $this->courses[1]->id]); + /** @var core_privacy\tests\request\content_writer $writer */ + $writer = writer::with_context($context); + + // Make sure that all and only data for the course1 was exported. + // There is no way to fetch all data from writer as array so we need to fetch one-by-one for each data id. + $invaldfieldischecked = false; + foreach ($DB->get_records('customfield_data', []) as $dbrecord) { + $data = $writer->get_data(['Custom fields data', $dbrecord->id]); + if ($dbrecord->instanceid == $this->courses[1]->id) { + $this->assertEquals($dbrecord->fieldid, $data->fieldid); + $this->assertNotEmpty($data->fieldtype); + $this->assertNotEmpty($data->fieldshortname); + $this->assertNotEmpty($data->fieldname); + $invaldfieldischecked = $invaldfieldischecked ?: ($data->fieldid == $invalidfieldid); + } else { + $this->assertEmpty($data); + } + } + + // Make sure field with was checked in this test. + $this->assertTrue($invaldfieldischecked); + } + + /** + * Test for provider::delete_customfields_data() + */ + public function test_delete_customfields_data() { + global $USER, $DB; + $approvedcontexts = new approved_contextlist($USER, 'core_course', [context_course::instance($this->courses[1]->id)->id]); + provider::delete_customfields_data($approvedcontexts, 'core_course', 'course'); + $this->assertEmpty($DB->get_records('customfield_data', ['instanceid' => $this->courses[1]->id])); + $this->assertNotEmpty($DB->get_records('customfield_data', ['instanceid' => $this->courses[2]->id])); + } + + /** + * Test for provider::delete_customfields_configuration() + */ + public function test_delete_customfields_configuration() { + global $USER, $DB; + // Remember the list of fields in the category 2 before we delete it. + $catid1 = $this->cfcats[1]->get('id'); + $catid2 = $this->cfcats[2]->get('id'); + $fids2 = $DB->get_fieldset_select('customfield_field', 'id', 'categoryid=?', [$catid2]); + $this->assertNotEmpty($fids2); + list($fsql, $fparams) = $DB->get_in_or_equal($fids2, SQL_PARAMS_NAMED); + $this->assertNotEmpty($DB->get_records_select('customfield_data', 'fieldid ' . $fsql, $fparams)); + + // A little hack here, modify customfields configuration so they have different itemids. + $DB->update_record('customfield_category', ['id' => $catid2, 'itemid' => 1]); + $contextlist = new approved_contextlist($USER, 'core_course', [context_system::instance()->id]); + provider::delete_customfields_configuration($contextlist, 'core_course', 'course', '=:i', ['i' => 1]); + + // Make sure everything for category $catid2 is gone but present for $catid1. + $this->assertEmpty($DB->get_records('customfield_category', ['id' => $catid2])); + $this->assertEmpty($DB->get_records_select('customfield_field', 'id ' . $fsql, $fparams)); + $this->assertEmpty($DB->get_records_select('customfield_data', 'fieldid ' . $fsql, $fparams)); + + $this->assertNotEmpty($DB->get_records('customfield_category', ['id' => $catid1])); + $fids1 = $DB->get_fieldset_select('customfield_field', 'id', 'categoryid=?', [$catid1]); + list($fsql1, $fparams1) = $DB->get_in_or_equal($fids1, SQL_PARAMS_NAMED); + $this->assertNotEmpty($DB->get_records_select('customfield_field', 'id ' . $fsql1, $fparams1)); + $this->assertNotEmpty($DB->get_records_select('customfield_data', 'fieldid ' . $fsql1, $fparams1)); + } + + /** + * Test for provider::delete_customfields_configuration_for_context() + */ + public function test_delete_customfields_configuration_for_context() { + global $USER, $DB; + // Remember the list of fields in the category 2 before we delete it. + $catid1 = $this->cfcats[1]->get('id'); + $catid2 = $this->cfcats[2]->get('id'); + $fids2 = $DB->get_fieldset_select('customfield_field', 'id', 'categoryid=?', [$catid2]); + $this->assertNotEmpty($fids2); + list($fsql, $fparams) = $DB->get_in_or_equal($fids2, SQL_PARAMS_NAMED); + $this->assertNotEmpty($DB->get_records_select('customfield_data', 'fieldid ' . $fsql, $fparams)); + + // A little hack here, modify customfields configuration so they have different contexts. + $context = context_user::instance($USER->id); + $DB->update_record('customfield_category', ['id' => $catid2, 'contextid' => $context->id]); + provider::delete_customfields_configuration_for_context('core_course', 'course', $context); + + // Make sure everything for category $catid2 is gone but present for $catid1. + $this->assertEmpty($DB->get_records('customfield_category', ['id' => $catid2])); + $this->assertEmpty($DB->get_records_select('customfield_field', 'id ' . $fsql, $fparams)); + $this->assertEmpty($DB->get_records_select('customfield_data', 'fieldid ' . $fsql, $fparams)); + + $this->assertNotEmpty($DB->get_records('customfield_category', ['id' => $catid1])); + $fids1 = $DB->get_fieldset_select('customfield_field', 'id', 'categoryid=?', [$catid1]); + list($fsql1, $fparams1) = $DB->get_in_or_equal($fids1, SQL_PARAMS_NAMED); + $this->assertNotEmpty($DB->get_records_select('customfield_field', 'id ' . $fsql1, $fparams1)); + $this->assertNotEmpty($DB->get_records_select('customfield_data', 'fieldid ' . $fsql1, $fparams1)); + } + + /** + * Test for provider::delete_customfields_data_for_context() + */ + public function test_delete_customfields_data_for_context() { + global $DB; + provider::delete_customfields_data_for_context('core_course', 'course', + context_course::instance($this->courses[1]->id)); + $fids2 = $DB->get_fieldset_select('customfield_field', 'id', '1=1', []); + list($fsql, $fparams) = $DB->get_in_or_equal($fids2, SQL_PARAMS_NAMED); + $fparams['course1'] = $this->courses[1]->id; + $fparams['course2'] = $this->courses[2]->id; + $this->assertEmpty($DB->get_records_select('customfield_data', 'instanceid = :course1 AND fieldid ' . $fsql, $fparams)); + $this->assertNotEmpty($DB->get_records_select('customfield_data', 'instanceid = :course2 AND fieldid ' . $fsql, $fparams)); + } +} diff --git a/lang/en/admin.php b/lang/en/admin.php index dbb7577c9c7..c2e304a7486 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -729,6 +729,7 @@ $string['managecontextlocklocked'] = '{$a->contextname} and any lower contexts are now frozen.'; $string['managecontextlockunlocked'] = '{$a->contextname} and any lower contexts are now unfrozen.'; $string['managecontextunlock'] = 'Unfreeze this context'; +$string['managecustomfields'] = 'Manage custom field types'; $string['manageformats'] = 'Manage course formats'; $string['manageformatsgotosettings'] = 'Default format can be changed in {$a}'; $string['managelang'] = 'Manage'; @@ -1384,3 +1385,4 @@ $string['hubs'] = 'Hubs'; $string['configloginhttps'] = 'Turning this on will make Moodle use a secure https connection just for the login page (providing a secure login), and then afterwards revert back to the normal http URL for general speed. CAUTION: this setting REQUIRES https to be specifically enabled on the web server - if it is not then YOU COULD LOCK YOURSELF OUT OF YOUR SITE.'; $string['loginhttps'] = 'Use HTTPS for logins'; +$string['course_customfield'] = 'Course custom fields'; diff --git a/lib/setuplib.php b/lib/setuplib.php index 68c283a2832..b9ab72f8683 100644 --- a/lib/setuplib.php +++ b/lib/setuplib.php @@ -1407,7 +1407,7 @@ function disable_output_buffering() { */ function is_major_upgrade_required() { global $CFG; - $lastmajordbchanges = 2018111301.00; + $lastmajordbchanges = 2019011101.00; $required = empty($CFG->version); $required = $required || (float)$CFG->version < $lastmajordbchanges; diff --git a/lib/tests/behat/behat_general.php b/lib/tests/behat/behat_general.php index 13ac090a8a1..7adcfd9e1c8 100644 --- a/lib/tests/behat/behat_general.php +++ b/lib/tests/behat/behat_general.php @@ -1446,6 +1446,46 @@ function($context, $args) { } } + /** + * Checks that the image on the page is the same as one of the fixture files + * + * @Then /^the image at "(?P