MDL-57898 core_customfield: Custom fields API
authorMarina Glancy <marina@moodle.com>
Fri, 11 Jan 2019 10:36:40 +0000 (11:36 +0100)
committerMarina Glancy <marina@moodle.com>
Fri, 18 Jan 2019 13:28:18 +0000 (14:28 +0100)
This commit is part of work on Custom fields API,
to minimize commit history in moodle core the work of a team of developers was split
into several commits with different authors but the authorship of individual
lines of code may be different from the commit author.

14 files changed:
customfield/classes/privacy/customfield_provider.php [new file with mode: 0644]
customfield/field/checkbox/tests/plugin_test.php [new file with mode: 0644]
customfield/field/date/tests/plugin_test.php [new file with mode: 0644]
customfield/field/select/tests/plugin_test.php [new file with mode: 0644]
customfield/field/text/tests/plugin_test.php [new file with mode: 0644]
customfield/field/textarea/lib.php [new file with mode: 0644]
customfield/field/textarea/tests/plugin_test.php [new file with mode: 0644]
customfield/lib.php [new file with mode: 0644]
customfield/tests/fixtures/test_instance_form.php [new file with mode: 0644]
customfield/tests/privacy_test.php [new file with mode: 0644]
lang/en/admin.php
lib/setuplib.php
lib/tests/behat/behat_general.php
phpunit.xml.dist

diff --git a/customfield/classes/privacy/customfield_provider.php b/customfield/classes/privacy/customfield_provider.php
new file mode 100644 (file)
index 0000000..8d05a5b
--- /dev/null
@@ -0,0 +1,84 @@
+<?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/>.
+
+/**
+ * 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 (file)
index 0000000..6d11750
--- /dev/null
@@ -0,0 +1,177 @@
+<?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/>.
+
+/**
+ * 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 (file)
index 0000000..95b4876
--- /dev/null
@@ -0,0 +1,180 @@
+<?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/>.
+
+/**
+ * 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 (file)
index 0000000..de630d5
--- /dev/null
@@ -0,0 +1,166 @@
+<?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/>.
+
+/**
+ * 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 (file)
index 0000000..7a2379f
--- /dev/null
@@ -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/>.
+
+/**
+ * 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('<a href="https://twitter.com/mynickname">mynickname</a>', $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 (file)
index 0000000..83389f3
--- /dev/null
@@ -0,0 +1,76 @@
+<?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/>.
+
+/**
+ * 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 (file)
index 0000000..2e8619d
--- /dev/null
@@ -0,0 +1,167 @@
+<?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/>.
+
+/**
+ * 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('<div class="text_to_html">Value1</div>', $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('Value3', $d->get_value());
+        $this->assertEquals('<div class="text_to_html">Value3</div>', $d->export_value());
+    }
+
+    /**
+     * Deleting fields and data
+     */
+    public function test_delete() {
+        $this->cfcat->get_handler()->delete_all();
+    }
+}
diff --git a/customfield/lib.php b/customfield/lib.php
new file mode 100644 (file)
index 0000000..b3ec2d6
--- /dev/null
@@ -0,0 +1,85 @@
+<?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/>.
+
+/**
+ * Callbacks
+ *
+ * @package   core_customfield
+ * @copyright 2018 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Edit customfield elements inplace
+ *
+ * @param string $itemtype
+ * @param int    $itemid
+ * @param string $newvalue
+ * @return \core\output\inplace_editable
+ */
+function core_customfield_inplace_editable($itemtype, $itemid, $newvalue) {
+    if ($itemtype === 'category') {
+        $category = core_customfield\category_controller::create($itemid);
+        $handler = $category->get_handler();
+        \external_api::validate_context($handler->get_configuration_context());
+        if (!$handler->can_configure()) {
+            throw new moodle_exception('nopermissionconfigure', 'core_customfield');
+        }
+        $newvalue = clean_param($newvalue, PARAM_NOTAGS);
+        $handler->rename_category($category, $newvalue);
+        return \core_customfield\api::get_category_inplace_editable($category, true);
+    }
+}
+
+/**
+ * Serve the files from the core_customfield 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 core_customfield_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
+    if ($filearea !== 'description') {
+        return false;
+    }
+
+    $itemid = array_shift($args);
+    $filename = array_pop($args); // The last item in the $args array.
+
+    $field = \core_customfield\field_controller::create($itemid);
+    $handler = $field->get_handler();
+    if ($handler->get_configuration_context()->id != $context->id) {
+        return false;
+    }
+
+    // Retrieve the file from the Files API.
+    $fs = get_file_storage();
+    $file = $fs->get_file($context->id, 'core_customfield', $filearea, $itemid, '/', $filename);
+    if (!$file) {
+        return false; // The file does not exist.
+    }
+
+    // We can now send the file back to the browser - in this case with a cache lifetime of 1 day and no filtering.
+    // From Moodle 2.3, use send_stored_file instead.
+    send_file($file, 86400, 0, $forcedownload, $options);
+}
diff --git a/customfield/tests/fixtures/test_instance_form.php b/customfield/tests/fixtures/test_instance_form.php
new file mode 100644 (file)
index 0000000..97fce04
--- /dev/null
@@ -0,0 +1,79 @@
+<?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/>.
+
+/**
+ * Class core_customfield_test_instance_form
+ *
+ * @package     core_customfield
+ * @copyright   2019 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/formslib.php');
+
+/**
+ * Class core_customfield_test_instance_form
+ *
+ * @package     core_customfield
+ * @copyright   2019 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_customfield_test_instance_form extends moodleform {
+    /** @var \core_customfield\handler */
+    protected $handler;
+
+    /** @var stdClass */
+    protected $instance;
+
+    /**
+     * Form definition
+     */
+    public function definition() {
+        $this->handler = $this->_customdata['handler'];
+        $this->instance = $this->_customdata['instance'];
+
+        $this->_form->addElement('hidden', 'id');
+        $this->_form->setType('id', PARAM_INT);
+
+        $this->handler->instance_form_definition($this->_form, $this->instance->id);
+
+        $this->add_action_buttons();
+
+        $this->handler->instance_form_before_set_data($this->instance);
+        $this->set_data($this->instance);
+    }
+
+    /**
+     * Definition after data
+     */
+    public function definition_after_data() {
+        $this->handler->instance_form_definition_after_data($this->_form, $this->instance->id);
+    }
+
+    /**
+     * Form validation
+     *
+     * @param array $data
+     * @param array $files
+     * @return array
+     */
+    public function validation($data, $files) {
+        return $this->handler->instance_form_validation($data, $files);
+    }
+}
diff --git a/customfield/tests/privacy_test.php b/customfield/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..8dfdb41
--- /dev/null
@@ -0,0 +1,250 @@
+<?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/>.
+
+/**
+ * Class provider_test
+ *
+ * @package     core_customfield
+ * @copyright   2019 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use core_privacy\tests\provider_testcase;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\writer;
+use core_customfield\privacy\provider;
+
+/**
+ * Class provider_test
+ *
+ * @package     core_customfield
+ * @copyright   2019 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_customfield_privacy_testcase extends provider_testcase {
+
+    /** @var stdClass[]  */
+    private $courses = [];
+    /** @var \core_customfield\category_controller[] */
+    private $cfcats = [];
+    /** @var \core_customfield\field_controller[] */
+    private $cffields = [];
+
+    /**
+     * Set up
+     */
+    public function setUp() {
+        $this->resetAfterTest();
+
+        $this->cfcats[1] = $this->get_generator()->create_category();
+        $this->cfcats[2] = $this->get_generator()->create_category();
+        $this->cffields[11] = $this->get_generator()->create_field(
+            ['categoryid' => $this->cfcats[1]->get('id'), 'type' => 'checkbox']);
+        $this->cffields[12] = $this->get_generator()->create_field(
+            ['categoryid' => $this->cfcats[1]->get('id'), 'type' => 'date']);
+        $this->cffields[13] = $this->get_generator()->create_field(
+            ['categoryid' => $this->cfcats[1]->get('id'),
+            'type' => 'select', 'configdata' => ['options' => "a\nb\nc"]]);
+        $this->cffields[14] = $this->get_generator()->create_field(
+            ['categoryid' => $this->cfcats[1]->get('id'), 'type' => 'text']);
+        $this->cffields[15] = $this->get_generator()->create_field(
+            ['categoryid' => $this->cfcats[1]->get('id'), 'type' => 'textarea']);
+        $this->cffields[21] = $this->get_generator()->create_field(
+            ['categoryid' => $this->cfcats[2]->get('id')]);
+        $this->cffields[22] = $this->get_generator()->create_field(
+            ['categoryid' => $this->cfcats[2]->get('id')]);
+
+        $this->courses[1] = $this->getDataGenerator()->create_course();
+        $this->courses[2] = $this->getDataGenerator()->create_course();
+        $this->courses[3] = $this->getDataGenerator()->create_course();
+
+        $this->get_generator()->add_instance_data($this->cffields[11], $this->courses[1]->id, 1);
+        $this->get_generator()->add_instance_data($this->cffields[12], $this->courses[1]->id, 1546300800);
+        $this->get_generator()->add_instance_data($this->cffields[13], $this->courses[1]->id, 2);
+        $this->get_generator()->add_instance_data($this->cffields[14], $this->courses[1]->id, 'Hello1');
+        $this->get_generator()->add_instance_data($this->cffields[15], $this->courses[1]->id,
+            ['text' => '<p>Hi there</p>', '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));
+    }
+}
index dbb7577..c2e304a 100644 (file)
@@ -729,6 +729,7 @@ $string['managecontextlock'] = 'Freeze this context';
 $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['moodleorghubname'] = 'Moodle.net';
 $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';
index 68c283a..b9ab72f 100644 (file)
@@ -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;
index 13ac090..7adcfd9 100644 (file)
@@ -1446,6 +1446,46 @@ class behat_general extends behat_base {
         }
     }
 
+    /**
+     * Checks that the image on the page is the same as one of the fixture files
+     *
+     * @Then /^the image at "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should be identical to "(?P<filepath_string>(?:[^"]|\\")*)"$/
+     * @throws ExpectationException
+     * @param string $element The locator of the image
+     * @param string $selectortype The selector type
+     * @param string $filepath path to the fixture file
+     */
+    public function the_image_at_should_be_identical_to($element, $selectortype, $filepath) {
+        global $CFG;
+
+        // Get the container node (exception if it doesn't exist).
+        $containernode = $this->get_selected_node($selectortype, $element);
+        $url = $containernode->getAttribute('src');
+        if ($url == null) {
+            throw new ExpectationException('Element does not have src attribute',
+                $this->getSession());
+        }
+        $session = $this->getSession()->getCookie('MoodleSession');
+        $content = download_file_content($url, array('Cookie' => 'MoodleSession=' . $session));
+
+        // Get the content of the fixture file.
+        // Replace 'admin/' if it is in start of path with $CFG->admin .
+        if (substr($filepath, 0, 6) === 'admin/') {
+            $filepath = $CFG->admin . DIRECTORY_SEPARATOR . substr($filepath, 6);
+        }
+        $filepath = str_replace('/', DIRECTORY_SEPARATOR, $filepath);
+        $filepath = $CFG->dirroot . DIRECTORY_SEPARATOR . $filepath;
+        if (!is_readable($filepath)) {
+            throw new ExpectationException('The file to compare to does not exist.', $this->getSession());
+        }
+        $expectedcontent = file_get_contents($filepath);
+
+        if ($content !== $expectedcontent) {
+            throw new ExpectationException('Image is not identical to the fixture. Received ' .
+            strlen($content) . ' bytes and expected ' . strlen($expectedcontent) . ' bytes');
+        }
+    }
+
     /**
      * Prepare to detect whether or not a new page has loaded (or the same page reloaded) some time in the future.
      *
index 60af5a0..075bd95 100644 (file)
@@ -92,6 +92,9 @@
         <testsuite name="core_blog_testsuite">
             <directory suffix="_test.php">blog/tests</directory>
         </testsuite>
+        <testsuite name="core_customfield_testsuite">
+            <directory suffix="_test.php">customfield/tests</directory>
+        </testsuite>
         <testsuite name="core_iplookup_testsuite">
             <directory suffix="_test.php">iplookup/tests</directory>
         </testsuite>