MDL-66135 tool_uploadcourse: support custom course fields.
authorPaul Holden <paulh@moodle.com>
Tue, 1 Oct 2019 14:16:13 +0000 (15:16 +0100)
committerPaul Holden <paulh@moodle.com>
Thu, 30 Apr 2020 23:10:15 +0000 (00:10 +0100)
admin/tool/uploadcourse/classes/course.php
admin/tool/uploadcourse/classes/helper.php
admin/tool/uploadcourse/classes/step2_form.php
admin/tool/uploadcourse/index.php
admin/tool/uploadcourse/lang/en/tool_uploadcourse.php
admin/tool/uploadcourse/tests/behat/create.feature
admin/tool/uploadcourse/tests/behat/update.feature
admin/tool/uploadcourse/tests/course_test.php
admin/tool/uploadcourse/tests/fixtures/courses_custom_fields.csv [new file with mode: 0644]
admin/tool/uploadcourse/tests/helper_test.php

index 8ae7a8d..88cd888 100644 (file)
@@ -284,6 +284,15 @@ class tool_uploadcourse_course {
         return $this->errors;
     }
 
+    /**
+     * Return array of valid fields for default values
+     *
+     * @return array
+     */
+    protected function get_valid_fields() {
+        return array_merge(self::$validfields, \tool_uploadcourse_helper::get_custom_course_field_names());
+    }
+
     /**
      * Assemble the course data based on defaults.
      *
@@ -293,7 +302,7 @@ class tool_uploadcourse_course {
      * @return array
      */
     protected function get_final_create_data($data) {
-        foreach (self::$validfields as $field) {
+        foreach ($this->get_valid_fields() as $field) {
             if (!isset($data[$field]) && isset($this->defaults[$field])) {
                 $data[$field] = $this->defaults[$field];
             }
@@ -316,9 +325,9 @@ class tool_uploadcourse_course {
         global $DB;
         $newdata = array();
         $existingdata = $DB->get_record('course', array('shortname' => $this->shortname));
-        foreach (self::$validfields as $field) {
+        foreach ($this->get_valid_fields() as $field) {
             if ($missingonly) {
-                if (!is_null($existingdata->$field) and $existingdata->$field !== '') {
+                if (isset($existingdata->$field) and $existingdata->$field !== '') {
                     continue;
                 }
             }
@@ -699,6 +708,27 @@ class tool_uploadcourse_course {
             $coursedata[$rolekey] = $rolename;
         }
 
+        // Custom fields. If the course already exists and mode isn't set to force creation, we can use its context.
+        if ($exists && $mode !== tool_uploadcourse_processor::MODE_CREATE_ALL) {
+            $context = context_course::instance($coursedata['id']);
+        } else {
+            // The category ID is taken from the defaults if it exists, otherwise from course data.
+            $context = context_coursecat::instance($this->defaults['category'] ?? $coursedata['category']);
+        }
+        $customfielddata = tool_uploadcourse_helper::get_custom_course_field_data($this->rawdata, $this->defaults, $context,
+            $errors);
+        if (!empty($errors)) {
+            foreach ($errors as $key => $message) {
+                $this->error($key, $message);
+            }
+
+            return false;
+        }
+
+        foreach ($customfielddata as $name => $value) {
+            $coursedata[$name] = $value;
+        }
+
         // Some validation.
         if (!empty($coursedata['format']) && !in_array($coursedata['format'], tool_uploadcourse_helper::get_course_formats())) {
             $this->error('invalidcourseformat', new lang_string('invalidcourseformat', 'tool_uploadcourse'));
index 2325c9c..1011c37 100644 (file)
@@ -337,6 +337,103 @@ class tool_uploadcourse_helper {
         return $rolenames;
     }
 
+    /**
+     * Return array of all custom course fields indexed by their shortname
+     *
+     * @return \core_customfield\field_controller[]
+     */
+    public static function get_custom_course_fields(): array {
+        $result = [];
+
+        $fields = \core_course\customfield\course_handler::create()->get_fields();
+        foreach ($fields as $field) {
+            $result[$field->get('shortname')] = $field;
+        }
+
+        return $result;
+    }
+
+    /**
+     * Return array of custom field element names
+     *
+     * @return string[]
+     */
+    public static function get_custom_course_field_names(): array {
+        $result = [];
+
+        $fields = self::get_custom_course_fields();
+        foreach ($fields as $field) {
+            $controller = \core_customfield\data_controller::create(0, null, $field);
+            $result[] = $controller->get_form_element_name();
+        }
+
+        return $result;
+    }
+
+    /**
+     * Return any elements from passed $data whose key matches one of the custom course fields defined for the site
+     *
+     * @param array $data
+     * @param array $defaults
+     * @param context $context
+     * @param array $errors Will be populated with any errors
+     * @return array
+     */
+    public static function get_custom_course_field_data(array $data, array $defaults, context $context,
+            array &$errors = []): array {
+
+        $fields = self::get_custom_course_fields();
+        $result = [];
+
+        $canchangelockedfields = guess_if_creator_will_have_course_capability('moodle/course:changelockedcustomfields', $context);
+
+        foreach ($data as $name => $originalvalue) {
+            if (preg_match('/^customfield_(?<name>.*)?$/', $name, $matches)
+                    && isset($fields[$matches['name']])) {
+
+                $fieldname = $matches['name'];
+                $field = $fields[$fieldname];
+
+                // Skip field if it's locked and user doesn't have capability to change locked fields.
+                if ($field->get_configdata_property('locked') && !$canchangelockedfields) {
+                    continue;
+                }
+
+                // Create field data controller.
+                $controller = \core_customfield\data_controller::create(0, null, $field);
+                $controller->set('id', 1);
+
+                $defaultvalue = $defaults["customfield_{$fieldname}"] ?? $controller->get_default_value();
+                $value = (empty($originalvalue) ? $defaultvalue : $field->parse_value($originalvalue));
+
+                // If we initially had a value, but now don't, then reset it to the default.
+                if (!empty($originalvalue) && empty($value)) {
+                    $value = $defaultvalue;
+                }
+
+                // Validate data with controller.
+                $fieldformdata = [$controller->get_form_element_name() => $value];
+                $validationerrors = $controller->instance_form_validation($fieldformdata, []);
+                if (count($validationerrors) > 0) {
+                    $errors['customfieldinvalid'] = new lang_string('customfieldinvalid', 'tool_uploadcourse',
+                        $field->get_formatted_name());
+
+                    continue;
+                }
+
+                $controller->set($controller->datafield(), $value);
+
+                // Pass an empty object to the data controller, which will transform it to a correct name/value pair.
+                $instance = new stdClass();
+                $controller->instance_form_before_set_data($instance);
+
+                $result = array_merge($result, (array) $instance);
+            }
+        }
+
+        return $result;
+    }
+
     /**
      * Helper to increment an ID number.
      *
@@ -493,5 +590,4 @@ class tool_uploadcourse_helper {
         }
         return $id;
     }
-
-}
+}
\ No newline at end of file
index 58c39d4..8854abb 100644 (file)
@@ -173,6 +173,10 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
             $mform->addHelpButton('defaults[enablecompletion]', 'enablecompletion', 'completion');
         }
 
+        // Add custom fields to the form.
+        $handler = \core_course\customfield\course_handler::create();
+        $handler->instance_form_definition($mform, 0, 'defaultvaluescustomfieldcategory', 'tool_uploadcourse');
+
         // Hidden fields.
         $mform->addElement('hidden', 'importid');
         $mform->setType('importid', PARAM_INT);
@@ -182,6 +186,10 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
 
         $this->add_action_buttons(true, get_string('uploadcourses', 'tool_uploadcourse'));
 
+        // Prepare custom fields data.
+        $data = (object) $data;
+        $handler->instance_form_before_set_data($data);
+
         $this->set_data($data);
     }
 
@@ -219,6 +227,9 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
             $enddate = $format->get_default_course_enddate($mform, array('startdate' => 'defaults[startdate]'));
             $mform->setDefault('defaults[enddate]', $enddate);
         }
+
+        // Tweak the form with values provided by custom fields in use.
+        \core_course\customfield\course_handler::create()->instance_form_definition_after_data($mform);
     }
 
     /**
@@ -237,6 +248,9 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
             $errors['defaults[enddate]'] = get_string($errorcode, 'error');
         }
 
+        // Custom fields validation.
+        array_merge($errors, \core_course\customfield\course_handler::create()->instance_form_validation($data, $files));
+
         return $errors;
     }
 }
index 69f4410..8c2ccfd 100644 (file)
@@ -78,6 +78,12 @@ if ($form2data = $mform2->is_cancelled()) {
     $options = (array) $form2data->options;
     $defaults = (array) $form2data->defaults;
 
+    // Custom field defaults.
+    $customfields = tool_uploadcourse_helper::get_custom_course_field_names();
+    foreach ($customfields as $customfield) {
+        $defaults[$customfield] = $form2data->{$customfield};
+    }
+
     // Restorefile deserves its own logic because formslib does not really appreciate
     // when the name of a filepicker is an array...
     $options['restorefile'] = '';
index 9a0e2c6..b44b4f3 100644 (file)
@@ -75,6 +75,7 @@ $string['csvdelimiter_help'] = 'CSV delimiter of the CSV file.';
 $string['csvfileerror'] = 'There is something wrong with the format of the CSV file. Please check the number of headings and columns match, and that the delimiter and file encoding are correct: {$a}';
 $string['csvline'] = 'Line';
 $string['defaultvalues'] = 'Default course values';
+$string['defaultvaluescustomfieldcategory'] = 'Default values for \'{$a}\'';
 $string['encoding'] = 'Encoding';
 $string['encoding_help'] = 'Encoding of the CSV file.';
 $string['errorwhilerestoringcourse'] = 'Error while restoring the course';
@@ -102,6 +103,7 @@ $string['mode_help'] = 'This allows you to specify if courses can be created and
 $string['nochanges'] = 'No changes';
 $string['pluginname'] = 'Course upload';
 $string['preview'] = 'Preview';
+$string['customfieldinvalid'] = 'Custom field \'{$a}\' is empty or contains invalid data';
 $string['reset'] = 'Reset course after upload';
 $string['reset_help'] = 'Whether to reset the course after creating/updating it.';
 $string['result'] = 'Result';
index 9fbfa01..a17fc2f 100644 (file)
@@ -42,3 +42,66 @@ Feature: An admin can create courses using a CSV file
     And I should see "Course 1"
     And I should see "Course 2"
     And I should see "Course 3"
+
+  @javascript
+  Scenario: Creation of new courses with custom fields
+    Given the following "custom field categories" exist:
+      | name  | component   | area   | itemid |
+      | Other | core_course | course | 0      |
+    And the following "custom fields" exist:
+      | name    | category | type     | shortname | configdata            |
+      | Field 1 | Other    | checkbox | checkbox  |                       |
+      | Field 2 | Other    | date     | date      |                       |
+      | Field 3 | Other    | select   | select    | {"options":"a\nb\nc"} |
+      | Field 4 | Other    | text     | text      |                       |
+      | Field 5 | Other    | textarea | textarea  |                       |
+    When I upload "admin/tool/uploadcourse/tests/fixtures/courses_custom_fields.csv" file to "File" filemanager
+    And I set the field "Upload mode" to "Create new courses only, skip existing ones"
+    And I click on "Preview" "button"
+    And I click on "Upload courses" "button"
+    Then I should see "Course created"
+    And I should see "Courses created: 1"
+    And I am on site homepage
+    And I should see "Course fields 1"
+    And I should see "Field 1: Yes"
+    And I should see "Field 2: Tuesday, 1 October 2019, 2:00"
+    And I should see "Field 3: b"
+    And I should see "Field 4: Hello"
+    And I should see "Field 5: Goodbye"
+
+  @javascript
+  Scenario: Creation of new courses with custom fields using defaults
+    Given the following "custom field categories" exist:
+      | name  | component   | area   | itemid |
+      | Other | core_course | course | 0      |
+    And the following "custom fields" exist:
+      | name    | category | type     | shortname | configdata                                          |
+      | Field 1 | Other    | checkbox | checkbox  | {"checkbydefault":1}                                |
+      | Field 2 | Other    | date     | date      | {"includetime":0}                                   |
+      | Field 3 | Other    | select   | select    | {"options":"a\nb\nc","defaultvalue":"b"}            |
+      | Field 4 | Other    | text     | text      | {"defaultvalue":"Hello"}                            |
+      | Field 5 | Other    | textarea | textarea  | {"defaultvalue":"Some text","defaultvalueformat":1} |
+    When I upload "admin/tool/uploadcourse/tests/fixtures/courses.csv" file to "File" filemanager
+    And I set the field "Upload mode" to "Create all, increment shortname if needed"
+    And I click on "Preview" "button"
+    And I expand all fieldsets
+    And the field "Field 1" matches value "1"
+    And the field "Field 3" matches value "b"
+    And the field "Field 4" matches value "Hello"
+    And the field "Field 5" matches value "Some text"
+    # We have to enable the date field manually.
+    And I set the following fields to these values:
+      | customfield_date[enabled] | 1    |
+      | customfield_date[day]     | 1    |
+      | customfield_date[month]   | June |
+      | customfield_date[year]    | 2020 |
+    And I click on "Upload courses" "button"
+    Then I should see "Course created"
+    And I should see "Courses created: 3"
+    And I am on site homepage
+    And I should see "Course 1"
+    And I should see "Field 1: Yes"
+    And I should see "Field 2: 1 June 2020"
+    And I should see "Field 3: b"
+    And I should see "Field 4: Hello"
+    And I should see "Field 5: Some text"
\ No newline at end of file
index dbdbef5..dbca1f4 100644 (file)
@@ -7,7 +7,8 @@ Feature: An admin can update courses using a CSV file
   Background:
     Given the following "courses" exist:
       | fullname | shortname | category |
-      | Some random name | C1 | 0 |
+      | Some random name | C1  | 0 |
+      | Another course   | CF1 | 0 |
     And I log in as "admin"
     And I navigate to "Courses > Upload courses" in site administration
 
@@ -28,3 +29,31 @@ Feature: An admin can update courses using a CSV file
     And I should see "Course 1"
     And I should not see "Course 2"
     And I should not see "Course 3"
+
+  @javascript
+  Scenario: Updating a course with custom fields
+    Given the following "custom field categories" exist:
+      | name  | component   | area   | itemid |
+      | Other | core_course | course | 0      |
+    And the following "custom fields" exist:
+      | name    | category | type     | shortname | configdata            |
+      | Field 1 | Other    | checkbox | checkbox  |                       |
+      | Field 2 | Other    | date     | date      |                       |
+      | Field 3 | Other    | select   | select    | {"options":"a\nb\nc"} |
+      | Field 4 | Other    | text     | text      |                       |
+      | Field 5 | Other    | textarea | textarea  |                       |
+    When I upload "admin/tool/uploadcourse/tests/fixtures/courses_custom_fields.csv" file to "File" filemanager
+    And I set the following fields to these values:
+      | Upload mode | Only update existing courses |
+      | Update mode | Update with CSV data only    |
+    And I click on "Preview" "button"
+    And I click on "Upload courses" "button"
+    Then I should see "Course updated"
+    And I should see "Courses updated: 1"
+    And I am on site homepage
+    And I should see "Course fields 1"
+    And I should see "Field 1: Yes"
+    And I should see "Field 2: Tuesday, 1 October 2019, 2:00"
+    And I should see "Field 3: b"
+    And I should see "Field 4: Hello"
+    And I should see "Field 5: Goodbye"
\ No newline at end of file
index 0add18e..0af3d93 100644 (file)
@@ -1081,6 +1081,136 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
         $this->assertEquals(strtotime('12th July 2013'), $enroldata['manual']->enrolenddate);
     }
 
+    /**
+     * Test upload processing of course custom fields
+     */
+    public function test_custom_fields_data() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $course = $this->getDataGenerator()->create_course(['shortname' => 'C1']);
+
+        // Create our custom fields.
+        $category = $this->get_customfield_generator()->create_category();
+        $this->create_custom_field($category, 'date', 'mydatefield');
+        $this->create_custom_field($category, 'text', 'mytextfield');
+        $this->create_custom_field($category, 'textarea', 'mytextareafield');
+
+        // Perform upload.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $dataupload = [
+            'shortname' => $course->shortname,
+            'customfield_mydatefield' => '2020-04-01 16:00',
+            'customfield_mytextfield' => 'Hello',
+            'customfield_mytextareafield' => 'Is it me you\'re looking for?',
+        ];
+
+        $uploader = new tool_uploadcourse_course($mode, $updatemode, $dataupload);
+        $this->assertTrue($uploader->prepare());
+        $uploader->proceed();
+
+        // Confirm presence of course custom fields.
+        $data = \core_course\customfield\course_handler::create()->export_instance_data_object($course->id);
+        $this->assertEquals('Wednesday, 1 April 2020, 4:00 PM', $data->mydatefield);
+        $this->assertEquals($dataupload['customfield_mytextfield'], $data->mytextfield);
+        $this->assertContains($dataupload['customfield_mytextareafield'], $data->mytextareafield);
+    }
+
+    /**
+     * Test upload processing of course custom field that is required but empty
+     */
+    public function test_custom_fields_data_required() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $course = $this->getDataGenerator()->create_course(['shortname' => 'C1']);
+
+        // Create our custom field.
+        $category = $this->get_customfield_generator()->create_category();
+        $this->create_custom_field($category, 'select', 'myselect', ['required' => true, 'options' => "Cat\nDog"]);
+
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $dataupload = [
+            'shortname' => $course->shortname,
+            'customfield_myselect' => null,
+        ];
+
+        $uploader = new tool_uploadcourse_course($mode, $updatemode, $dataupload);
+        $this->assertFalse($uploader->prepare());
+        $this->assertArrayHasKey('customfieldinvalid', $uploader->get_errors());
+
+        // Try again with a default value.
+        $defaults = [
+            'customfield_myselect' => 2, // Our second option: Dog.
+        ];
+
+        $uploader = new tool_uploadcourse_course($mode, $updatemode, $dataupload, $defaults);
+        $this->assertTrue($uploader->prepare());
+        $uploader->proceed();
+
+        // Confirm presence of course custom fields.
+        $data = \core_course\customfield\course_handler::create()->export_instance_data_object($course->id);
+        $this->assertEquals('Dog', $data->myselect);
+    }
+
+    /**
+     * Test upload processing of course custom field with an invalid select option
+     */
+    public function test_custom_fields_data_invalid_select_option() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $course = $this->getDataGenerator()->create_course(['shortname' => 'C1']);
+
+        // Create our custom field.
+        $category = $this->get_customfield_generator()->create_category();
+        $this->create_custom_field($category, 'select', 'myselect',
+            ['required' => true, 'options' => "Cat\nDog", 'defaultvalue' => 'Cat']);
+
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $dataupload = [
+            'shortname' => $course->shortname,
+            'customfield_myselect' => 'Fish', // No, invalid.
+        ];
+
+        $uploader = new tool_uploadcourse_course($mode, $updatemode, $dataupload);
+        $this->assertTrue($uploader->prepare());
+        $uploader->proceed();
+
+        // Confirm presence of course custom fields.
+        $data = \core_course\customfield\course_handler::create()->export_instance_data_object($course->id);
+        $this->assertEquals('Cat', $data->myselect);
+    }
+
+    /**
+     * Test upload processing of course custom field with an out of range date
+     */
+    public function test_custom_fields_data_invalid_date() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $course = $this->getDataGenerator()->create_course(['shortname' => 'C1']);
+
+        // Create our custom field.
+        $category = $this->get_customfield_generator()->create_category();
+        $this->create_custom_field($category, 'date', 'mydate',
+            ['mindate' => strtotime('2020-04-01'), 'maxdate' => '2020-04-30']);
+
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $dataupload = [
+            'shortname' => $course->shortname,
+            'customfield_mydate' => '2020-05-06', // Out of range.
+        ];
+
+        $uploader = new tool_uploadcourse_course($mode, $updatemode, $dataupload);
+        $this->assertFalse($uploader->prepare());
+        $this->assertArrayHasKey('customfieldinvalid', $uploader->get_errors());
+    }
+
     public function test_idnumber_problems() {
         $this->resetAfterTest(true);
 
@@ -1224,7 +1354,34 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
         $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
         $this->assertFalse($co->prepare());
         $this->assertArrayHasKey('cannotrenameshortnamealreadyinuse', $co->get_errors());
+    }
 
+    /**
+     * Get custom field plugin generator
+     *
+     * @return core_customfield_generator
+     */
+    protected function get_customfield_generator() : core_customfield_generator {
+        return $this->getDataGenerator()->get_plugin_generator('core_customfield');
     }
 
-}
+    /**
+     * Helper method to create custom course field
+     *
+     * @param \core_customfield\category_controller $category
+     * @param string $type
+     * @param string $shortname
+     * @param array $configdata
+     * @return \core_customfield\field_controller
+     */
+    protected function create_custom_field(\core_customfield\category_controller $category, string $type, string $shortname,
+            array $configdata = []) : \core_customfield\field_controller {
+
+        return $this->get_customfield_generator()->create_field([
+            'categoryid' => $category->get('id'),
+            'type' => $type,
+            'shortname' => $shortname,
+            'configdata' => $configdata,
+        ]);
+    }
+}
\ No newline at end of file
diff --git a/admin/tool/uploadcourse/tests/fixtures/courses_custom_fields.csv b/admin/tool/uploadcourse/tests/fixtures/courses_custom_fields.csv
new file mode 100644 (file)
index 0000000..f949599
--- /dev/null
@@ -0,0 +1,2 @@
+shortname,fullname,summary,category,customfield_checkbox,customfield_date,customfield_select,customfield_text,customfield_textarea
+CF1,Course fields 1,Testing course fields,1,1,2019-10-01 14:00,b,Hello,Goodbye
\ No newline at end of file
index 6768507..feff463 100644 (file)
@@ -250,6 +250,81 @@ class tool_uploadcourse_helper_testcase extends advanced_testcase {
         $this->assertArrayHasKey('invalidroles', $errors);
     }
 
+    /**
+     * Test custom field data processing
+     */
+    public function test_get_custom_course_field_data() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Create all the fields!
+        $category = $this->get_customfield_generator()->create_category();
+
+        $checkboxfield = $this->create_custom_field($category, 'checkbox', 'mycheckbox');
+        $datefield = $this->create_custom_field($category, 'date', 'mydate');
+        $selectfield = $this->create_custom_field($category, 'select', 'myselect', ['options' => "Red\nGreen\nBlue"]);
+        $textfield = $this->create_custom_field($category, 'text', 'mytext', ['locked' => 1]);
+        $textareafield = $this->create_custom_field($category, 'textarea', 'mytextarea');
+
+        $fields = tool_uploadcourse_helper::get_custom_course_fields();
+        $this->assertCount(5, $fields);
+
+        $this->assertArrayHasKey($checkboxfield->get('shortname'), $fields);
+        $this->assertInstanceOf(customfield_checkbox\field_controller::class, $fields[$checkboxfield->get('shortname')]);
+
+        $this->assertArrayHasKey($datefield->get('shortname'), $fields);
+        $this->assertInstanceOf(customfield_date\field_controller::class, $fields[$datefield->get('shortname')]);
+
+        $this->assertArrayHasKey($selectfield->get('shortname'), $fields);
+        $this->assertInstanceOf(customfield_select\field_controller::class, $fields[$selectfield->get('shortname')]);
+
+        $this->assertArrayHasKey($textfield->get('shortname'), $fields);
+        $this->assertInstanceOf(customfield_text\field_controller::class, $fields[$textfield->get('shortname')]);
+
+        $this->assertArrayHasKey($textareafield->get('shortname'), $fields);
+        $this->assertInstanceOf(customfield_textarea\field_controller::class, $fields[$textareafield->get('shortname')]);
+
+        $data = [
+            'customfield_mycheckbox' => '1',
+            'customfield_mydate' => '2019-10-01',
+            'customfield_myselect' => 'Green',
+            'customfield_mytext' => 'Hello',
+            'customfield_myunknownfield' => 'Goodbye',
+        ];
+
+        $expected = [
+            'customfield_mycheckbox' => '1',
+            'customfield_mydate' => strtotime('2019-10-01'),
+            'customfield_myselect' => 2,
+            'customfield_mytext' => 'Hello',
+        ];
+
+        $course = $this->getDataGenerator()->create_course();
+        $user = $this->getDataGenerator()->create_and_enrol($course, 'manager');
+        $this->setUser($user);
+
+        $context = context_course::instance($course->id);
+
+        $this->assertEquals($expected, tool_uploadcourse_helper::get_custom_course_field_data($data, [], $context));
+
+        // Now add our custom textarea field (separately because the value of it's 'itemid' element is unknown).
+        $data['customfield_mytextarea'] = 'Something';
+        $fields = tool_uploadcourse_helper::get_custom_course_field_data($data, [], $context);
+        $this->assertArrayHasKey('customfield_mytextarea_editor', $fields);
+        $this->assertArrayHasKey('text', $fields['customfield_mytextarea_editor']);
+        $this->assertEquals('Something', $fields['customfield_mytextarea_editor']['text']);
+
+        // Now prohibit the capability to change locked fields for the manager role.
+        $managerrole = $DB->get_record('role', ['shortname' => 'manager']);
+        role_change_permission($managerrole->id, $context, 'moodle/course:changelockedcustomfields', CAP_PROHIBIT);
+
+        // The locked 'mytext' custom field should not be returned.
+        $fields = tool_uploadcourse_helper::get_custom_course_field_data($data, [], $context);
+        $this->assertCount(4, $fields);
+        $this->assertArrayNotHasKey('customfield_mytext', $fields);
+    }
+
     public function test_increment_idnumber() {
         $this->resetAfterTest(true);
 
@@ -394,4 +469,33 @@ class tool_uploadcourse_helper_testcase extends advanced_testcase {
         $this->assertEquals($cat3_fakedouble->id, tool_uploadcourse_helper::resolve_category_by_path($path));
         $this->assertEquals($cat3_fakedouble->id, tool_uploadcourse_helper::resolve_category_by_path($path));
     }
-}
+
+    /**
+     * Get custom field plugin generator
+     *
+     * @return core_customfield_generator
+     */
+    protected function get_customfield_generator() : core_customfield_generator {
+        return $this->getDataGenerator()->get_plugin_generator('core_customfield');
+    }
+
+    /**
+     * Helper method to create custom course field
+     *
+     * @param \core_customfield\category_controller $category
+     * @param string $type
+     * @param string $shortname
+     * @param array $configdata
+     * @return \core_customfield\field_controller
+     */
+    protected function create_custom_field(\core_customfield\category_controller $category, string $type, string $shortname,
+            array $configdata = []) : \core_customfield\field_controller {
+
+        return $this->get_customfield_generator()->create_field([
+            'categoryid' => $category->get('id'),
+            'type' => $type,
+            'shortname' => $shortname,
+            'configdata' => $configdata,
+        ]);
+    }
+}
\ No newline at end of file