diff --git a/admin/settings/users.php b/admin/settings/users.php index 2f265ee7efd..aa602290f6e 100644 --- a/admin/settings/users.php +++ b/admin/settings/users.php @@ -213,7 +213,8 @@ if ($hassiteconfig new lang_string('showuseridentity', 'admin'), new lang_string('showuseridentity_desc', 'admin'), ['email' => 1], function() { - global $DB; + global $CFG; + require_once($CFG->dirroot.'/user/profile/lib.php'); // Basic fields available in user table. $fields = [ @@ -229,10 +230,10 @@ if ($hassiteconfig ]; // Custom profile fields. - $profilefields = $DB->get_records('user_info_field', ['datatype' => 'text'], 'sortorder ASC'); - foreach ($profilefields as $key => $field) { - // Only reasonable-length fields can be used as identity fields. - if ($field->param2 > 255) { + $profilefields = profile_get_custom_fields(); + foreach ($profilefields as $field) { + // Only reasonable-length text fields can be used as identity fields. + if ($field->param2 > 255 || $field->datatype != 'text') { continue; } $fields['profile_field_' . $field->shortname] = $field->name . ' *'; diff --git a/admin/tool/uploaduser/classes/process.php b/admin/tool/uploaduser/classes/process.php index cb56918a276..300f5e754b2 100644 --- a/admin/tool/uploaduser/classes/process.php +++ b/admin/tool/uploaduser/classes/process.php @@ -60,7 +60,7 @@ class process { protected $standardfields = []; /** @var array */ protected $profilefields = []; - /** @var array */ + /** @var \profile_field_base[] */ protected $allprofilefields = []; /** @var string|\uu_progress_tracker|null */ protected $progresstrackerclass = null; @@ -161,12 +161,13 @@ class process { * Profile fields */ protected function find_profile_fields(): void { - global $DB; - $this->allprofilefields = $DB->get_records('user_info_field'); + global $CFG; + require_once($CFG->dirroot . '/user/profile/lib.php'); + $this->allprofilefields = profile_get_user_fields_with_data(0); $this->profilefields = []; if ($proffields = $this->allprofilefields) { foreach ($proffields as $key => $proffield) { - $profilefieldname = 'profile_field_'.$proffield->shortname; + $profilefieldname = 'profile_field_'.$proffield->get_shortname(); $this->profilefields[] = $profilefieldname; // Re-index $proffields with key as shortname. This will be // used while checking if profile data is key and needs to be converted (eg. menu profile field). @@ -528,8 +529,7 @@ class process { } } } - $proffields = $this->allprofilefields; - foreach ($this->profilefields as $field) { + foreach ($this->allprofilefields as $field => $profilefield) { if (isset($user->$field)) { continue; } @@ -539,9 +539,6 @@ class process { // Form contains key and later code expects value. // Convert key to value for required profile fields. - require_once($CFG->dirroot.'/user/profile/field/'.$proffields[$field]->datatype.'/field.class.php'); - $profilefieldclass = 'profile_field_'.$proffields[$field]->datatype; - $profilefield = new $profilefieldclass($proffields[$field]->id); if (method_exists($profilefield, 'convert_external_data')) { $user->$field = $profilefield->edit_save_data_preprocess($user->$field, null); } diff --git a/admin/tool/uploaduser/locallib.php b/admin/tool/uploaduser/locallib.php index 68481c77221..88f020c2041 100644 --- a/admin/tool/uploaduser/locallib.php +++ b/admin/tool/uploaduser/locallib.php @@ -415,17 +415,17 @@ function uu_allowed_sysroles_cache() { * @return stdClass pre-processed custom profile data */ function uu_pre_process_custom_profile_data($data) { - global $CFG, $DB; + global $CFG; + require_once($CFG->dirroot . '/user/profile/lib.php'); + $fields = profile_get_user_fields_with_data(0); + // find custom profile fields and check if data needs to converted. foreach ($data as $key => $value) { if (preg_match('/^profile_field_/', $key)) { $shortname = str_replace('profile_field_', '', $key); - if ($fields = $DB->get_records('user_info_field', array('shortname' => $shortname))) { - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $data->id); - if (method_exists($formfield, 'convert_external_data')) { + if ($fields) { + foreach ($fields as $formfield) { + if ($formfield->get_shortname() === $shortname && method_exists($formfield, 'convert_external_data')) { $data->$key = $formfield->convert_external_data($value); } } @@ -443,7 +443,9 @@ function uu_pre_process_custom_profile_data($data) { * @return bool true if no error else false */ function uu_check_custom_profile_data(&$data) { - global $CFG, $DB; + global $CFG; + require_once($CFG->dirroot.'/user/profile/lib.php'); + $noerror = true; $testuserid = null; @@ -452,15 +454,13 @@ function uu_check_custom_profile_data(&$data) { $testuserid = $result[1]; } } + $profilefields = profile_get_user_fields_with_data(0); // Find custom profile fields and check if data needs to converted. foreach ($data as $key => $value) { if (preg_match('/^profile_field_/', $key)) { $shortname = str_replace('profile_field_', '', $key); - if ($fields = $DB->get_records('user_info_field', array('shortname' => $shortname))) { - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, 0); + foreach ($profilefields as $formfield) { + if ($formfield->get_shortname() === $shortname) { if (method_exists($formfield, 'convert_external_data') && is_null($formfield->convert_external_data($value))) { $data['status'][] = get_string('invaliduserfield', 'error', $shortname); diff --git a/admin/tool/uploaduser/tests/behat/upload_users.feature b/admin/tool/uploaduser/tests/behat/upload_users.feature index 4cdfbd29b0f..04fb69b7898 100644 --- a/admin/tool/uploaduser/tests/behat/upload_users.feature +++ b/admin/tool/uploaduser/tests/behat/upload_users.feature @@ -69,7 +69,8 @@ Feature: Upload users # Create user profile field. Given I log in as "admin" And I navigate to "Users > Accounts > User profile fields" in site administration - And I set the field "datatype" to "Text area" + And I click on "Create a new profile field" "link" + And I click on "Text area" "link" And I set the following fields to these values: | Short name | superfield | | Name | Super field | diff --git a/admin/tool/uploaduser/tests/cli_test.php b/admin/tool/uploaduser/tests/cli_test.php index a13c047a841..d2af41ce849 100644 --- a/admin/tool/uploaduser/tests/cli_test.php +++ b/admin/tool/uploaduser/tests/cli_test.php @@ -126,14 +126,13 @@ class tool_uploaduser_cli_testcase extends advanced_testcase { * User upload with user profile fields */ public function test_upload_with_profile_fields() { - global $DB, $CFG; + global $CFG; $this->resetAfterTest(); set_config('passwordpolicy', 0); $this->setAdminUser(); - $categoryid = $DB->insert_record('user_info_category', ['name' => 'Cat 1', 'sortorder' => 1]); - $this->field1 = $DB->insert_record('user_info_field', [ - 'shortname' => 'superfield', 'name' => 'Super field', 'categoryid' => $categoryid, + $this->field1 = $this->getDataGenerator()->create_custom_profile_field([ + 'shortname' => 'superfield', 'name' => 'Super field', 'datatype' => 'text', 'signup' => 1, 'visible' => 1, 'required' => 1, 'sortorder' => 1]); $filepath = $CFG->dirroot.'/lib/tests/fixtures/upload_users_profile.csv'; diff --git a/admin/user/user_bulk_download.php b/admin/user/user_bulk_download.php index ffa3f2335b1..79f3b34c4ca 100644 --- a/admin/user/user_bulk_download.php +++ b/admin/user/user_bulk_download.php @@ -50,11 +50,9 @@ if ($dataformat) { 'city' => 'city', 'country' => 'country'); - if ($extrafields = $DB->get_records('user_info_field')) { - foreach ($extrafields as $n => $field) { - $fields['profile_field_'.$field->shortname] = 'profile_field_'.$field->shortname; - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - } + $extrafields = profile_get_user_fields_with_data(0); + foreach ($extrafields as $formfield) { + $fields['profile_field_'.$formfield->get_shortname()] = 'profile_field_'.$formfield->get_shortname(); } $filename = clean_filename(get_string('users')); @@ -68,11 +66,7 @@ if ($dataformat) { if (!$user = $DB->get_record('user', array('id' => $userid))) { return null; } - foreach ($extrafields as $field) { - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $user->id); - $formfield->edit_load_user_data($user); - } + profile_load_data($user); $userprofiledata = array(); foreach ($fields as $field => $unused) { // Custom user profile textarea fields come in an array diff --git a/auth/db/tests/db_test.php b/auth/db/tests/db_test.php index 1de432c3f17..ce163583dc2 100644 --- a/auth/db/tests/db_test.php +++ b/auth/db/tests/db_test.php @@ -139,8 +139,7 @@ class auth_db_testcase extends advanced_testcase { set_config('field_lock_email', 'unlocked', 'auth_db'); // Create a user profile field and add mapping to it. - $DB->insert_record('user_info_field', ['shortname' => 'pet', 'name' => 'Pet', 'required' => 0, - 'visible' => 1, 'locked' => 0, 'categoryid' => 1, 'datatype' => 'text']); + $this->getDataGenerator()->create_custom_profile_field(['shortname' => 'pet', 'name' => 'Pet', 'datatype' => 'text']); set_config('field_map_profile_field_pet', 'animal', 'auth_db'); set_config('field_updatelocal_profile_field_pet', 'oncreate', 'auth_db'); diff --git a/auth/email/tests/external_test.php b/auth/email/tests/external_test.php index ed1900a6dfd..c04a76a8032 100644 --- a/auth/email/tests/external_test.php +++ b/auth/email/tests/external_test.php @@ -44,18 +44,17 @@ class auth_email_external_testcase extends externallib_advanced_testcase { * Set up for every test */ public function setUp(): void { - global $CFG, $DB; + global $CFG; $this->resetAfterTest(true); $CFG->registerauth = 'email'; - $categoryid = $DB->insert_record('user_info_category', array('name' => 'Cat 1', 'sortorder' => 1)); - $this->field1 = $DB->insert_record('user_info_field', array( - 'shortname' => 'frogname', 'name' => 'Name of frog', 'categoryid' => $categoryid, - 'datatype' => 'text', 'signup' => 1, 'visible' => 1, 'required' => 1, 'sortorder' => 1)); - $this->field2 = $DB->insert_record('user_info_field', array( - 'shortname' => 'sometext', 'name' => 'Some text in textarea', 'categoryid' => $categoryid, - 'datatype' => 'textarea', 'signup' => 1, 'visible' => 1, 'required' => 1, 'sortorder' => 2)); + $this->field1 = $this->getDataGenerator()->create_custom_profile_field(array( + 'shortname' => 'frogname', 'name' => 'Name of frog', + 'datatype' => 'text', 'signup' => 1, 'visible' => 1, 'required' => 1, 'sortorder' => 1))->id; + $this->field2 = $this->getDataGenerator()->create_custom_profile_field(array( + 'shortname' => 'sometext', 'name' => 'Some text in textarea', + 'datatype' => 'textarea', 'signup' => 1, 'visible' => 1, 'required' => 1, 'sortorder' => 2))->id; } public function test_get_signup_settings() { @@ -109,8 +108,8 @@ class auth_email_external_testcase extends externallib_advanced_testcase { // Create category with MathJax and a new field with MathJax. $categoryname = 'Cat $$(a+b)=2$$'; $fieldname = 'Some text $$(a+b)=2$$'; - $categoryid = $DB->insert_record('user_info_category', array('name' => $categoryname, 'sortorder' => 1)); - $field3 = $DB->insert_record('user_info_field', array( + $categoryid = $this->getDataGenerator()->create_custom_profile_field_category(['name' => $categoryname])->id; + $this->getDataGenerator()->create_custom_profile_field(array( 'shortname' => 'mathjaxname', 'name' => $fieldname, 'categoryid' => $categoryid, 'datatype' => 'textarea', 'signup' => 1, 'visible' => 1, 'required' => 1, 'sortorder' => 2)); diff --git a/availability/condition/profile/tests/behat/availability_profile.feature b/availability/condition/profile/tests/behat/availability_profile.feature index 816e1fe73a8..690bcd8dc27 100644 --- a/availability/condition/profile/tests/behat/availability_profile.feature +++ b/availability/condition/profile/tests/behat/availability_profile.feature @@ -65,7 +65,8 @@ Feature: availability_profile # Add custom field. Given I log in as "admin" And I navigate to "Users > Accounts > User profile fields" in site administration - And I set the field "datatype" to "Text input" + And I click on "Create a new profile field" "link" + And I click on "Text input" "link" And I set the following fields to these values: | Short name | superfield | | Name | Super field | diff --git a/availability/condition/profile/tests/condition_test.php b/availability/condition/profile/tests/condition_test.php index 7e3074c7906..12d44772e57 100644 --- a/availability/condition/profile/tests/condition_test.php +++ b/availability/condition/profile/tests/condition_test.php @@ -50,14 +50,10 @@ class availability_profile_condition_testcase extends advanced_testcase { $this->resetAfterTest(); - // Add a custom profile field type. The API for doing this is indescribably - // horrid and tightly intertwined with the form UI, so it's best to add - // it directly in database. - $DB->insert_record('user_info_field', array( - 'shortname' => 'frogtype', 'name' => 'Type of frog', 'categoryid' => 1, + // Add a custom profile field type. + $this->profilefield = $this->getDataGenerator()->create_custom_profile_field(array( + 'shortname' => 'frogtype', 'name' => 'Type of frog', 'datatype' => 'text')); - $this->profilefield = $DB->get_record('user_info_field', - array('shortname' => 'frogtype')); // Clear static cache. \availability_profile\condition::wipe_static_cache(); @@ -333,11 +329,9 @@ class availability_profile_condition_testcase extends advanced_testcase { $info = new \core_availability\mock_info(); // Add custom textarea type. - $DB->insert_record('user_info_field', array( - 'shortname' => 'longtext', 'name' => 'Long text', 'categoryid' => 1, + $customfield = $this->getDataGenerator()->create_custom_profile_field(array( + 'shortname' => 'longtext', 'name' => 'Long text', 'datatype' => 'textarea')); - $customfield = $DB->get_record('user_info_field', - array('shortname' => 'longtext')); // The list of fields should include the text field added in setUp(), // but should not include the textarea field added just now. @@ -465,11 +459,9 @@ class availability_profile_condition_testcase extends advanced_testcase { condition::wipe_static_cache(); // For testing, make another info field with default value. - $DB->insert_record('user_info_field', array( - 'shortname' => 'tonguestyle', 'name' => 'Tongue style', 'categoryid' => 1, + $otherprofilefield = $this->getDataGenerator()->create_custom_profile_field(array( + 'shortname' => 'tonguestyle', 'name' => 'Tongue style', 'datatype' => 'text', 'defaultdata' => 'Slimy')); - $otherprofilefield = $DB->get_record('user_info_field', - array('shortname' => 'tonguestyle')); // Make a test course and some users. $generator = $this->getDataGenerator(); diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index 7e51a1f9d85..abbc17983d2 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -1691,7 +1691,9 @@ class restore_section_structure_step extends restore_structure_step { * @param stdClass $data Record data */ public function process_availability_field($data) { - global $DB; + global $DB, $CFG; + require_once($CFG->dirroot.'/user/profile/lib.php'); + $data = (object)$data; // Mark it is as passed by default $passed = true; @@ -1704,9 +1706,8 @@ class restore_section_structure_step extends restore_structure_step { // If one is null but the other isn't something clearly went wrong and we'll skip this condition. $passed = false; } else if (!is_null($data->customfield)) { - $params = array('shortname' => $data->customfield, 'datatype' => $data->customfieldtype); - $customfieldid = $DB->get_field('user_info_field', 'id', $params); - $passed = ($customfieldid !== false); + $field = profile_get_custom_field_data_by_shortname($data->customfield); + $passed = $field && $field->datatype == $data->customfieldtype; } if ($passed) { @@ -4534,7 +4535,9 @@ class restore_module_structure_step extends restore_structure_step { * @param stdClass $data Record data */ protected function process_availability_field($data) { - global $DB; + global $DB, $CFG; + require_once($CFG->dirroot.'/user/profile/lib.php'); + $data = (object)$data; // Mark it is as passed by default $passed = true; @@ -4547,9 +4550,8 @@ class restore_module_structure_step extends restore_structure_step { // If one is null but the other isn't something clearly went wrong and we'll skip this condition. $passed = false; } else if (!empty($data->customfield)) { - $params = array('shortname' => $data->customfield, 'datatype' => $data->customfieldtype); - $customfieldid = $DB->get_field('user_info_field', 'id', $params); - $passed = ($customfieldid !== false); + $field = profile_get_custom_field_data_by_shortname($data->customfield); + $passed = $field && $field->datatype == $data->customfieldtype; } if ($passed) { diff --git a/backup/util/dbops/restore_dbops.class.php b/backup/util/dbops/restore_dbops.class.php index 3da5cfe1b3c..91be3c13619 100644 --- a/backup/util/dbops/restore_dbops.class.php +++ b/backup/util/dbops/restore_dbops.class.php @@ -1151,6 +1151,7 @@ abstract class restore_dbops { public static function create_included_users($basepath, $restoreid, $userid, \core\progress\base $progress) { global $CFG, $DB; + require_once($CFG->dirroot.'/user/profile/lib.php'); $progress->start_progress('Creating included users'); $authcache = array(); // Cache to get some bits from authentication plugins @@ -1257,8 +1258,9 @@ abstract class restore_dbops { $udata = (object)$udata; // If the profile field has data and the profile shortname-datatype is defined in server if ($udata->field_data) { - if ($field = $DB->get_record('user_info_field', array('shortname'=>$udata->field_name, 'datatype'=>$udata->field_type))) { - /// Insert the user_custom_profile_field + $field = profile_get_custom_field_data_by_shortname($udata->field_name); + if ($field && $field->datatype === $udata->field_type) { + // Insert the user_custom_profile_field. $rec = new stdClass(); $rec->userid = $newuserid; $rec->fieldid = $field->id; diff --git a/badges/criteria/award_criteria_profile.php b/badges/criteria/award_criteria_profile.php index 9c02a19c6f4..b014cf5a92b 100644 --- a/badges/criteria/award_criteria_profile.php +++ b/badges/criteria/award_criteria_profile.php @@ -44,7 +44,8 @@ class award_criteria_profile extends award_criteria { * */ public function get_options(&$mform) { - global $DB; + global $CFG, $DB; + require_once($CFG->dirroot . '/user/profile/lib.php'); $none = true; $existing = array(); @@ -54,17 +55,11 @@ class award_criteria_profile extends award_criteria { $dfields = array('firstname', 'lastname', 'email', 'address', 'phone1', 'phone2', 'department', 'institution', 'description', 'picture', 'city', 'country'); - $sql = "SELECT uf.id as fieldid, uf.name as name, ic.id as categoryid, ic.name as categoryname, uf.datatype - FROM {user_info_field} uf - JOIN {user_info_category} ic - ON uf.categoryid = ic.id AND uf.visible <> 0 - ORDER BY ic.sortorder ASC, uf.sortorder ASC"; - // Get custom fields. - $cfields = $DB->get_records_sql($sql); - $cfids = array_map(function($o) { - return $o->fieldid; - }, $cfields); + $cfields = array_filter(profile_get_custom_fields(), function($field) { + return $field->visible <> 0; + }); + $cfids = array_keys($cfields); if ($this->id !== 0) { $existing = array_keys($this->params); @@ -98,13 +93,14 @@ class award_criteria_profile extends award_criteria { foreach ($cfields as $field) { if (!isset($currentcat) || $currentcat != $field->categoryid) { $currentcat = $field->categoryid; - $mform->addElement('header', 'category_' . $currentcat, format_string($field->categoryname)); + $categoryname = $DB->get_field('user_info_category', 'name', ['id' => $field->categoryid]); + $mform->addElement('header', 'category_' . $currentcat, format_string($categoryname)); } $checked = false; - if (in_array($field->fieldid, $existing)) { + if (in_array($field->id, $existing)) { $checked = true; } - $this->config_options($mform, array('id' => $field->fieldid, 'checked' => $checked, 'name' => $field->name, 'error' => false)); + $this->config_options($mform, array('id' => $field->id, 'checked' => $checked, 'name' => $field->name, 'error' => false)); $none = false; } } @@ -133,11 +129,16 @@ class award_criteria_profile extends award_criteria { * @return string */ public function get_details($short = '') { - global $DB, $OUTPUT; + global $OUTPUT, $CFG; + require_once($CFG->dirroot.'/user/profile/lib.php'); + $output = array(); foreach ($this->params as $p) { if (is_numeric($p['field'])) { - $str = $DB->get_field('user_info_field', 'name', array('id' => $p['field'])); + $fields = profile_get_custom_fields(); + // Get formatted field name if such field exists. + $str = isset($fields[$p['field']]->name) ? + format_string($fields[$p['field']]->name) : null; } else { $str = \core_user\fields::get_display_name($p['field']); } diff --git a/badges/tests/badgeslib_test.php b/badges/tests/badgeslib_test.php index b44d2f59a52..2be62c01178 100644 --- a/badges/tests/badgeslib_test.php +++ b/badges/tests/badgeslib_test.php @@ -592,9 +592,9 @@ class badgeslib_test extends advanced_testcase { require_once($CFG->dirroot.'/user/profile/lib.php'); // Add a custom field of textarea type. - $customprofileid = $DB->insert_record('user_info_field', array( - 'shortname' => 'newfield', 'name' => 'Description of new field', 'categoryid' => 1, - 'datatype' => 'textarea')); + $customprofileid = $this->getDataGenerator()->create_custom_profile_field(array( + 'shortname' => 'newfield', 'name' => 'Description of new field', + 'datatype' => 'textarea'))->id; $this->preventResetByRollback(); // Messaging is not compatible with transactions. $badge = new badge($this->coursebadge); diff --git a/calendar/tests/calendartype_test.php b/calendar/tests/calendartype_test.php index 487010b2869..4e6e5dfc08c 100644 --- a/calendar/tests/calendartype_test.php +++ b/calendar/tests/calendartype_test.php @@ -36,7 +36,6 @@ require_once($CFG->libdir . '/form/datetimeselector.php'); // Used to test the user datetime profile field. require_once($CFG->dirroot . '/user/profile/lib.php'); require_once($CFG->dirroot . '/user/profile/definelib.php'); -require_once($CFG->dirroot . '/user/profile/index_field_form.php'); /** * Unit tests for the calendar type system. @@ -273,12 +272,13 @@ class core_calendar_type_testcase extends advanced_testcase { $formdata['name'] = 'Name'; $formdata['param1'] = $date['inputminyear']; $formdata['param2'] = $date['inputmaxyear']; + $formdata['datatype'] = 'datetime'; // Mock submitting this. - field_form::mock_submit($formdata); + \core_user\form\profile_field_form::mock_submit($formdata); // Create the user datetime form. - $form = new field_form(null, 'datetime'); + $form = new \core_user\form\profile_field_form(); // Get the data from the submission. $submissiondata = $form->get_data(); diff --git a/grade/lib.php b/grade/lib.php index 2e8c8d33d1b..0ae85d9b9af 100644 --- a/grade/lib.php +++ b/grade/lib.php @@ -3330,14 +3330,10 @@ abstract class grade_helper { // Sets the list of custom profile fields $customprofilefields = array_map('trim', explode(',', $CFG->grade_export_customprofilefields)); if ($includecustomfields && !empty($customprofilefields)) { - list($wherefields, $whereparams) = $DB->get_in_or_equal($customprofilefields); - $customfields = $DB->get_records_sql("SELECT f.* - FROM {user_info_field} f - JOIN {user_info_category} c ON f.categoryid=c.id - WHERE f.shortname $wherefields - ORDER BY c.sortorder ASC, f.sortorder ASC", $whereparams); + $customfields = profile_get_user_fields_with_data(0); - foreach ($customfields as $field) { + foreach ($customfields as $fieldobj) { + $field = (object)$fieldobj->get_field_config_for_external(); // Make sure we can display this custom field if (!in_array($field->shortname, $customprofilefields)) { continue; diff --git a/lang/en/admin.php b/lang/en/admin.php index ae5cb2f0d63..9f2e48fe760 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -989,7 +989,7 @@ $string['profilecommonsettings'] = 'Common settings'; $string['profileconfirmcategorydeletion'] = 'There is/are {$a} field/s in this category which will be moved into the category above (or below if in the top category).
Do you still wish to delete this category?'; $string['profileconfirmfielddeletion'] = 'There is/are {$a} user record/s for this field which will be deleted.
Do you still wish to delete this field?'; $string['profilecreatecategory'] = 'Create a new profile category'; -$string['profilecreatefield'] = 'Create a new profile field:'; +$string['profilecreatefield'] = 'Create a new profile field'; $string['profilecreatenewcategory'] = 'Creating a new category'; $string['profilecreatenewfield'] = 'Creating a new \'{$a}\' profile field'; $string['profiledefaultcategory'] = 'Other fields'; diff --git a/lib/authlib.php b/lib/authlib.php index b81c6ab4b5e..057bd81f02d 100644 --- a/lib/authlib.php +++ b/lib/authlib.php @@ -598,14 +598,16 @@ class auth_plugin_base { * @return array list of custom fields. */ public function get_custom_user_profile_fields() { - global $DB; + global $CFG; + require_once($CFG->dirroot . '/user/profile/lib.php'); + // If already retrieved then return. if (!is_null($this->customfields)) { return $this->customfields; } $this->customfields = array(); - if ($proffields = $DB->get_records('user_info_field')) { + if ($proffields = profile_get_custom_fields()) { foreach ($proffields as $proffield) { $this->customfields[] = 'profile_field_'.$proffield->shortname; } @@ -1159,7 +1161,8 @@ function signup_is_enabled() { * @since Moodle 3.3 */ function display_auth_lock_options($settings, $auth, $userfields, $helptext, $mapremotefields, $updateremotefields, $customfields = array()) { - global $DB; + global $CFG; + require_once($CFG->dirroot . '/user/profile/lib.php'); // Introductory explanation and help text. if ($mapremotefields) { @@ -1180,7 +1183,8 @@ function display_auth_lock_options($settings, $auth, $userfields, $helptext, $ma // Generate the list of profile fields to allow updates / lock. if (!empty($customfields)) { $userfields = array_merge($userfields, $customfields); - $customfieldname = $DB->get_records('user_info_field', null, '', 'shortname, name'); + $allcustomfields = profile_get_custom_fields(); + $customfieldname = array_combine(array_column($allcustomfields, 'shortname'), $allcustomfields); } foreach ($userfields as $field) { diff --git a/lib/testing/generator/data_generator.php b/lib/testing/generator/data_generator.php index 29c81681464..9eb9914009c 100644 --- a/lib/testing/generator/data_generator.php +++ b/lib/testing/generator/data_generator.php @@ -1289,7 +1289,7 @@ EOD; 'defaultdata' => 0 ] ]; - foreach ($typedefaults[$data['datatype']] as $field => $value) { + foreach ($typedefaults[$data['datatype']] ?? [] as $field => $value) { $defaults[$field] = $value; } diff --git a/lib/tests/behat/showuseridentity.feature b/lib/tests/behat/showuseridentity.feature index a475ec67f19..53446924a3d 100644 --- a/lib/tests/behat/showuseridentity.feature +++ b/lib/tests/behat/showuseridentity.feature @@ -22,7 +22,7 @@ Feature: Select user identity fields | user1 | C1 | manager | | user2 | C1 | manager | - Scenario: The admin settings screen should show text custom fields (and let you choose them) + Scenario: The admin settings screen should show text custom fields of certain length (and let you choose them) When I log in as "admin" And I navigate to "Users > Permissions > User policies" in site administration Then I should see "Speciality" in the "#admin-showuseridentity" "css_element" diff --git a/lib/tests/event_profile_field_test.php b/lib/tests/event_profile_field_test.php index 82154e7dcae..d2b5b42b6c8 100644 --- a/lib/tests/event_profile_field_test.php +++ b/lib/tests/event_profile_field_test.php @@ -50,13 +50,8 @@ class core_event_profile_field_testcase extends advanced_testcase { * Test that triggering the user_info_category_created event works as expected. */ public function test_user_info_category_created_event() { - global $DB; - // Create a new profile category. - $cat1 = new stdClass(); - $cat1->name = 'Example category'; - $cat1->sortorder = $DB->count_records('user_info_category') + 1; - $cat1->id = $DB->insert_record('user_info_category', $cat1); + $cat1 = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Example category']); // Trigger the event. $sink = $this->redirectEvents(); @@ -81,15 +76,8 @@ class core_event_profile_field_testcase extends advanced_testcase { global $DB; // Create new profile categories. - $cat1 = new stdClass(); - $cat1->name = 'Example category'; - $cat1->sortorder = $DB->count_records('user_info_category') + 1; - $cat1->id = $DB->insert_record('user_info_category', $cat1); - - $cat2 = new stdClass(); - $cat2->name = 'Example category 2'; - $cat2->sortorder = $DB->count_records('user_info_category') + 1; - $cat2->id = $DB->insert_record('user_info_category', $cat2); + $cat1 = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Example category']); + $cat2 = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Example category 2']); // Trigger the events. $sink = $this->redirectEvents(); @@ -116,18 +104,9 @@ class core_event_profile_field_testcase extends advanced_testcase { * Test that deleting a user info category triggers a delete event. */ public function test_user_info_category_deleted_event() { - global $DB; - // Create new profile categories. - $cat1 = new stdClass(); - $cat1->name = 'Example category'; - $cat1->sortorder = $DB->count_records('user_info_category') + 1; - $cat1->id = $DB->insert_record('user_info_category', $cat1); - - $cat2 = new stdClass(); - $cat2->name = 'Example category 2'; - $cat2->sortorder = $DB->count_records('user_info_category') + 1; - $cat2->id = $DB->insert_record('user_info_category', $cat2); + $cat1 = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Example category']); + $cat2 = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Example category 2']); // Trigger the event. $sink = $this->redirectEvents(); @@ -152,10 +131,7 @@ class core_event_profile_field_testcase extends advanced_testcase { global $DB; // Create a new profile category. - $cat1 = new stdClass(); - $cat1->name = 'Example category'; - $cat1->sortorder = $DB->count_records('user_info_category') + 1; - $cat1->id = $DB->insert_record('user_info_category', $cat1); + $cat1 = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Example category']); // Create a new profile field. $data = new stdClass(); @@ -196,27 +172,17 @@ class core_event_profile_field_testcase extends advanced_testcase { * Test that updating a user info field triggers an update event. */ public function test_user_info_field_updated_event() { - global $DB; - // Create a new profile category. - $cat1 = new stdClass(); - $cat1->name = 'Example category'; - $cat1->sortorder = $DB->count_records('user_info_category') + 1; - $cat1->id = $DB->insert_record('user_info_category', $cat1); + $cat1 = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Example category']); // Create a new profile field. - $data = new stdClass(); - $data->datatype = 'text'; - $data->shortname = 'example'; - $data->name = 'Example field'; - $data->description = 'Hello this is an example.'; - $data->required = false; - $data->locked = false; - $data->forceunique = false; - $data->signup = false; - $data->visible = '0'; - $data->categoryid = $cat1->id; - $data->id = $DB->insert_record('user_info_field', $data); + $data = $this->getDataGenerator()->create_custom_profile_field([ + 'datatype' => 'text', + 'shortname' => 'example', + 'name' => 'Example field', + 'description' => 'Hello this is an example.', + 'categoryid' => $cat1->id, + ]); // Trigger the event. $sink = $this->redirectEvents(); @@ -241,36 +207,25 @@ class core_event_profile_field_testcase extends advanced_testcase { * Test that moving a field triggers update events. */ public function test_user_info_field_updated_event_move_field() { - global $DB; - // Create a new profile category. - $cat1 = new stdClass(); - $cat1->name = 'Example category'; - $cat1->sortorder = $DB->count_records('user_info_category') + 1; - $cat1->id = $DB->insert_record('user_info_category', $cat1); + $cat1 = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Example category']); // Create a new profile field. - $field1 = new stdClass(); - $field1->datatype = 'text'; - $field1->shortname = 'example'; - $field1->name = 'Example field'; - $field1->description = 'Hello this is an example.'; - $field1->required = false; - $field1->locked = false; - $field1->forceunique = false; - $field1->signup = false; - $field1->visible = '0'; - $field1->categoryid = $cat1->id; - $field1->sortorder = $DB->count_records('user_info_field') + 1; - $field1->id = $DB->insert_record('user_info_field', $field1); + $field1 = $this->getDataGenerator()->create_custom_profile_field([ + 'datatype' => 'text', + 'shortname' => 'example', + 'name' => 'Example field', + 'description' => 'Hello this is an example.', + 'categoryid' => $cat1->id, + ]); // Create another that we will be moving. - $field2 = clone $field1; - $field2->datatype = 'text'; - $field2->shortname = 'example2'; - $field2->name = 'Example field 2'; - $field2->sortorder = $DB->count_records('user_info_field') + 1; - $field2->id = $DB->insert_record('user_info_field', $field2); + $field2 = $this->getDataGenerator()->create_custom_profile_field([ + 'datatype' => 'text', + 'shortname' => 'example2', + 'name' => 'Example field 2', + 'categoryid' => $cat1->id, + ]); // Trigger the events. $sink = $this->redirectEvents(); @@ -302,32 +257,18 @@ class core_event_profile_field_testcase extends advanced_testcase { * another category triggers an update event. */ public function test_user_info_field_updated_event_delete_category() { - global $DB; - - // Create a new profile category. - $cat1 = new stdClass(); - $cat1->name = 'Example category'; - $cat1->sortorder = $DB->count_records('user_info_category') + 1; - $cat1->id = $DB->insert_record('user_info_category', $cat1); - - $cat2 = new stdClass(); - $cat2->name = 'Example category'; - $cat2->sortorder = $DB->count_records('user_info_category') + 1; - $cat2->id = $DB->insert_record('user_info_category', $cat2); + // Create profile categories. + $cat1 = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Example category']); + $cat2 = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Example category']); // Create a new profile field. - $field = new stdClass(); - $field->datatype = 'text'; - $field->shortname = 'example'; - $field->name = 'Example field'; - $field->description = 'Hello this is an example.'; - $field->required = false; - $field->locked = false; - $field->forceunique = false; - $field->signup = false; - $field->visible = '0'; - $field->categoryid = $cat1->id; - $field->id = $DB->insert_record('user_info_field', $field); + $field = $this->getDataGenerator()->create_custom_profile_field([ + 'datatype' => 'text', + 'shortname' => 'example', + 'name' => 'Example field', + 'description' => 'Hello this is an example.', + 'categoryid' => $cat1->id, + ]); // Trigger the event. $sink = $this->redirectEvents(); @@ -351,27 +292,17 @@ class core_event_profile_field_testcase extends advanced_testcase { * Test that deleting a user info field triggers a delete event. */ public function test_user_info_field_deleted_event() { - global $DB; - // Create a new profile category. - $cat1 = new stdClass(); - $cat1->name = 'Example category'; - $cat1->sortorder = $DB->count_records('user_info_category') + 1; - $cat1->id = $DB->insert_record('user_info_category', $cat1); + $cat1 = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Example category']); // Create a new profile field. - $data = new stdClass(); - $data->datatype = 'text'; - $data->shortname = 'delete'; - $data->name = 'Example field for delete'; - $data->description = 'Hello this is an example.'; - $data->required = false; - $data->locked = false; - $data->forceunique = false; - $data->signup = false; - $data->visible = '0'; - $data->categoryid = $cat1->id; - $data->id = $DB->insert_record('user_info_field', $data, true); + $data = $this->getDataGenerator()->create_custom_profile_field([ + 'datatype' => 'text', + 'shortname' => 'delete', + 'name' => 'Example field for delete', + 'description' => 'Hello this is an example.', + 'categoryid' => $cat1->id, + ]); // Trigger the event. $sink = $this->redirectEvents(); diff --git a/user/amd/build/edit_profile_fields.min.js b/user/amd/build/edit_profile_fields.min.js new file mode 100644 index 00000000000..50d9ec4d57b --- /dev/null +++ b/user/amd/build/edit_profile_fields.min.js @@ -0,0 +1,2 @@ +define ("core_user/edit_profile_fields",["exports","core_form/modalform","core/str"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);var d={actions:{editCategory:"[data-action=\"editcategory\"]",editField:"[data-action=\"editfield\"]",createField:"[data-action=\"createfield\"]"}};a.init=function init(){document.addEventListener("click",function(a){var e=a.target.closest(d.actions.editCategory);if(e){a.preventDefault();var f=e.getAttribute("data-id")?(0,c.get_string)("profileeditcategory","admin",e.getAttribute("data-name")):(0,c.get_string)("profilecreatenewcategory","admin"),g=new b.default({formClass:"core_user\\form\\profile_category_form",args:{id:e.getAttribute("data-id")},modalConfig:{title:f},returnFocus:e});g.addEventListener(g.events.FORM_SUBMITTED,function(){return window.location.reload()});g.show()}e=a.target.closest(d.actions.editField);if(e){a.preventDefault();var h=new b.default({formClass:"core_user\\form\\profile_field_form",args:{id:e.getAttribute("data-id")},modalConfig:{title:(0,c.get_string)("profileeditfield","admin",e.getAttribute("data-name"))},returnFocus:e});h.addEventListener(h.events.FORM_SUBMITTED,function(){return window.location.reload()});h.show()}e=a.target.closest(d.actions.createField);if(e){a.preventDefault();var i=new b.default({formClass:"core_user\\form\\profile_field_form",args:{datatype:e.getAttribute("data-datatype"),categoryid:e.getAttribute("data-categoryid")},modalConfig:{title:(0,c.get_string)("profilecreatenewfield","admin",e.getAttribute("data-datatypename"))},returnFocus:e});i.addEventListener(i.events.FORM_SUBMITTED,function(){return window.location.reload()});i.show()}})}}); +//# sourceMappingURL=edit_profile_fields.min.js.map diff --git a/user/amd/build/edit_profile_fields.min.js.map b/user/amd/build/edit_profile_fields.min.js.map new file mode 100644 index 00000000000..1140d959d1a --- /dev/null +++ b/user/amd/build/edit_profile_fields.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/edit_profile_fields.js"],"names":["Selectors","actions","editCategory","editField","createField","init","document","addEventListener","e","element","target","closest","preventDefault","title","getAttribute","form","ModalForm","formClass","args","id","modalConfig","returnFocus","events","FORM_SUBMITTED","window","location","reload","show","datatype","categoryid"],"mappings":"iLAeA,uD,GAYMA,CAAAA,CAAS,CAAG,CACdC,OAAO,CAAE,CACLC,YAAY,CAAE,gCADT,CAELC,SAAS,CAAE,6BAFN,CAGLC,WAAW,CAAE,+BAHR,CADK,C,QAQE,QAAPC,CAAAA,IAAO,EAAM,CACtBC,QAAQ,CAACC,gBAAT,CAA0B,OAA1B,CAAmC,SAASC,CAAT,CAAY,CAC3C,GAAIC,CAAAA,CAAO,CAAGD,CAAC,CAACE,MAAF,CAASC,OAAT,CAAiBX,CAAS,CAACC,OAAV,CAAkBC,YAAnC,CAAd,CACA,GAAIO,CAAJ,CAAa,CACTD,CAAC,CAACI,cAAF,GADS,GAEHC,CAAAA,CAAK,CAAGJ,CAAO,CAACK,YAAR,CAAqB,SAArB,EACV,iBAAU,qBAAV,CAAiC,OAAjC,CAA0CL,CAAO,CAACK,YAAR,CAAqB,WAArB,CAA1C,CADU,CAEV,iBAAU,0BAAV,CAAsC,OAAtC,CAJK,CAKHC,CAAI,CAAG,GAAIC,UAAJ,CAAc,CACvBC,SAAS,CAAE,wCADY,CAEvBC,IAAI,CAAE,CAACC,EAAE,CAAEV,CAAO,CAACK,YAAR,CAAqB,SAArB,CAAL,CAFiB,CAGvBM,WAAW,CAAE,CAACP,KAAK,CAALA,CAAD,CAHU,CAIvBQ,WAAW,CAAEZ,CAJU,CAAd,CALJ,CAWTM,CAAI,CAACR,gBAAL,CAAsBQ,CAAI,CAACO,MAAL,CAAYC,cAAlC,CAAkD,iBAAMC,CAAAA,MAAM,CAACC,QAAP,CAAgBC,MAAhB,EAAN,CAAlD,EACAX,CAAI,CAACY,IAAL,EACH,CAEDlB,CAAO,CAAGD,CAAC,CAACE,MAAF,CAASC,OAAT,CAAiBX,CAAS,CAACC,OAAV,CAAkBE,SAAnC,CAAV,CACA,GAAIM,CAAJ,CAAa,CACTD,CAAC,CAACI,cAAF,GACA,GAAMG,CAAAA,CAAI,CAAG,GAAIC,UAAJ,CAAc,CACvBC,SAAS,CAAE,qCADY,CAEvBC,IAAI,CAAE,CAACC,EAAE,CAAEV,CAAO,CAACK,YAAR,CAAqB,SAArB,CAAL,CAFiB,CAGvBM,WAAW,CAAE,CAACP,KAAK,CAAE,iBAAU,kBAAV,CAA8B,OAA9B,CAAuCJ,CAAO,CAACK,YAAR,CAAqB,WAArB,CAAvC,CAAR,CAHU,CAIvBO,WAAW,CAAEZ,CAJU,CAAd,CAAb,CAMAM,CAAI,CAACR,gBAAL,CAAsBQ,CAAI,CAACO,MAAL,CAAYC,cAAlC,CAAkD,iBAAMC,CAAAA,MAAM,CAACC,QAAP,CAAgBC,MAAhB,EAAN,CAAlD,EACAX,CAAI,CAACY,IAAL,EACH,CAEDlB,CAAO,CAAGD,CAAC,CAACE,MAAF,CAASC,OAAT,CAAiBX,CAAS,CAACC,OAAV,CAAkBG,WAAnC,CAAV,CACA,GAAIK,CAAJ,CAAa,CACTD,CAAC,CAACI,cAAF,GACA,GAAMG,CAAAA,CAAI,CAAG,GAAIC,UAAJ,CAAc,CACvBC,SAAS,CAAE,qCADY,CAEvBC,IAAI,CAAE,CAACU,QAAQ,CAAEnB,CAAO,CAACK,YAAR,CAAqB,eAArB,CAAX,CAAkDe,UAAU,CAAEpB,CAAO,CAACK,YAAR,CAAqB,iBAArB,CAA9D,CAFiB,CAGvBM,WAAW,CAAE,CAACP,KAAK,CAAE,iBAAU,uBAAV,CAAmC,OAAnC,CAA4CJ,CAAO,CAACK,YAAR,CAAqB,mBAArB,CAA5C,CAAR,CAHU,CAIvBO,WAAW,CAAEZ,CAJU,CAAd,CAAb,CAMAM,CAAI,CAACR,gBAAL,CAAsBQ,CAAI,CAACO,MAAL,CAAYC,cAAlC,CAAkD,iBAAMC,CAAAA,MAAM,CAACC,QAAP,CAAgBC,MAAhB,EAAN,CAAlD,EACAX,CAAI,CAACY,IAAL,EACH,CACJ,CA1CD,CA2CH,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport ModalForm from 'core_form/modalform';\nimport {get_string as getString} from 'core/str';\n\n/**\n * User profile fields editor\n *\n * @module core_user/edit_profile_fields\n * @package core_user\n * @copyright 2021 Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst Selectors = {\n actions: {\n editCategory: '[data-action=\"editcategory\"]',\n editField: '[data-action=\"editfield\"]',\n createField: '[data-action=\"createfield\"]',\n },\n};\n\nexport const init = () => {\n document.addEventListener('click', function(e) {\n let element = e.target.closest(Selectors.actions.editCategory);\n if (element) {\n e.preventDefault();\n const title = element.getAttribute('data-id') ?\n getString('profileeditcategory', 'admin', element.getAttribute('data-name')) :\n getString('profilecreatenewcategory', 'admin');\n const form = new ModalForm({\n formClass: 'core_user\\\\form\\\\profile_category_form',\n args: {id: element.getAttribute('data-id')},\n modalConfig: {title},\n returnFocus: element,\n });\n form.addEventListener(form.events.FORM_SUBMITTED, () => window.location.reload());\n form.show();\n }\n\n element = e.target.closest(Selectors.actions.editField);\n if (element) {\n e.preventDefault();\n const form = new ModalForm({\n formClass: 'core_user\\\\form\\\\profile_field_form',\n args: {id: element.getAttribute('data-id')},\n modalConfig: {title: getString('profileeditfield', 'admin', element.getAttribute('data-name'))},\n returnFocus: element,\n });\n form.addEventListener(form.events.FORM_SUBMITTED, () => window.location.reload());\n form.show();\n }\n\n element = e.target.closest(Selectors.actions.createField);\n if (element) {\n e.preventDefault();\n const form = new ModalForm({\n formClass: 'core_user\\\\form\\\\profile_field_form',\n args: {datatype: element.getAttribute('data-datatype'), categoryid: element.getAttribute('data-categoryid')},\n modalConfig: {title: getString('profilecreatenewfield', 'admin', element.getAttribute('data-datatypename'))},\n returnFocus: element,\n });\n form.addEventListener(form.events.FORM_SUBMITTED, () => window.location.reload());\n form.show();\n }\n });\n};"],"file":"edit_profile_fields.min.js"} \ No newline at end of file diff --git a/user/amd/src/edit_profile_fields.js b/user/amd/src/edit_profile_fields.js new file mode 100644 index 00000000000..04916db4825 --- /dev/null +++ b/user/amd/src/edit_profile_fields.js @@ -0,0 +1,80 @@ +// 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 . + +import ModalForm from 'core_form/modalform'; +import {get_string as getString} from 'core/str'; + +/** + * User profile fields editor + * + * @module core_user/edit_profile_fields + * @package core_user + * @copyright 2021 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +const Selectors = { + actions: { + editCategory: '[data-action="editcategory"]', + editField: '[data-action="editfield"]', + createField: '[data-action="createfield"]', + }, +}; + +export const init = () => { + document.addEventListener('click', function(e) { + let element = e.target.closest(Selectors.actions.editCategory); + if (element) { + e.preventDefault(); + const title = element.getAttribute('data-id') ? + getString('profileeditcategory', 'admin', element.getAttribute('data-name')) : + getString('profilecreatenewcategory', 'admin'); + const form = new ModalForm({ + formClass: 'core_user\\form\\profile_category_form', + args: {id: element.getAttribute('data-id')}, + modalConfig: {title}, + returnFocus: element, + }); + form.addEventListener(form.events.FORM_SUBMITTED, () => window.location.reload()); + form.show(); + } + + element = e.target.closest(Selectors.actions.editField); + if (element) { + e.preventDefault(); + const form = new ModalForm({ + formClass: 'core_user\\form\\profile_field_form', + args: {id: element.getAttribute('data-id')}, + modalConfig: {title: getString('profileeditfield', 'admin', element.getAttribute('data-name'))}, + returnFocus: element, + }); + form.addEventListener(form.events.FORM_SUBMITTED, () => window.location.reload()); + form.show(); + } + + element = e.target.closest(Selectors.actions.createField); + if (element) { + e.preventDefault(); + const form = new ModalForm({ + formClass: 'core_user\\form\\profile_field_form', + args: {datatype: element.getAttribute('data-datatype'), categoryid: element.getAttribute('data-categoryid')}, + modalConfig: {title: getString('profilecreatenewfield', 'admin', element.getAttribute('data-datatypename'))}, + returnFocus: element, + }); + form.addEventListener(form.events.FORM_SUBMITTED, () => window.location.reload()); + form.show(); + } + }); +}; \ No newline at end of file diff --git a/user/classes/fields.php b/user/classes/fields.php index d5621957d02..4e5dbfe9e80 100644 --- a/user/classes/fields.php +++ b/user/classes/fields.php @@ -371,10 +371,11 @@ class fields { // or if the user doesn't have access to see them). foreach ($extra as $key => $field) { if (preg_match(self::PROFILE_FIELD_REGEX, $field, $matches)) { + $allowed = false; if ($allowcustom) { require_once($CFG->dirroot . '/user/profile/lib.php'); $fieldinfo = profile_get_custom_field_data_by_shortname($matches[1]); - switch ($fieldinfo['visible']) { + switch ($fieldinfo->visible ?? -1) { case PROFILE_VISIBLE_NONE: case PROFILE_VISIBLE_PRIVATE: $allowed = !$context || has_capability('moodle/user:viewalldetails', $context); @@ -383,8 +384,6 @@ class fields { $allowed = true; break; } - } else { - $allowed = false; } if (!$allowed) { unset($extra[$key]); @@ -586,7 +585,7 @@ class fields { require_once($CFG->dirroot . '/user/profile/lib.php'); $fieldinfo = profile_get_custom_field_data_by_shortname($matches[1]); // Use format_string so it can be translated with multilang filter if necessary. - return format_string($fieldinfo['name']); + return $fieldinfo ? format_string($fieldinfo->name) : $field; } // Some fields have language strings which are not the same as field name. diff --git a/user/classes/form/profile_category_form.php b/user/classes/form/profile_category_form.php new file mode 100644 index 00000000000..e69695fa03f --- /dev/null +++ b/user/classes/form/profile_category_form.php @@ -0,0 +1,125 @@ +. + +namespace core_user\form; + +use context; +use core_form\dynamic_form; +use moodle_url; + +/** + * Modal form to edit profile category + * + * @package core_user + * @copyright 2021 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class profile_category_form extends dynamic_form { + + /** + * Form definition + */ + protected function definition() { + $mform = $this->_form; + + $strrequired = get_string('required'); + + // Add some extra hidden fields. + $mform->addElement('hidden', 'id'); + $mform->setType('id', PARAM_INT); + $mform->addElement('hidden', 'action', 'editcategory'); + $mform->setType('action', PARAM_ALPHANUMEXT); + + $mform->addElement('text', 'name', get_string('profilecategoryname', 'admin'), 'maxlength="255" size="30"'); + $mform->setType('name', PARAM_TEXT); + $mform->addRule('name', $strrequired, 'required', null, 'client'); + } + + /** + * Perform some moodle validation. + * + * @param array $data + * @param array $files + * @return array + */ + public function validation($data, $files) { + global $DB; + $errors = parent::validation($data, $files); + + $duplicate = $DB->get_field('user_info_category', 'id', ['name' => $data['name']]); + + // Check the name is unique. + if (!empty($data['id'])) { // We are editing an existing record. + $olddata = $DB->get_record('user_info_category', ['id' => $data['id']]); + // Name has changed, new name in use, new name in use by another record. + $dupfound = (($olddata->name !== $data['name']) && $duplicate && ($data['id'] != $duplicate)); + } else { // New profile category. + $dupfound = $duplicate; + } + + if ($dupfound ) { + $errors['name'] = get_string('profilecategorynamenotunique', 'admin'); + } + + return $errors; + } + + /** + * Returns context where this form is used + * + * @return context + */ + protected function get_context_for_dynamic_submission(): context { + return \context_system::instance(); + } + + /** + * Checks if current user has access to this form, otherwise throws exception + */ + protected function check_access_for_dynamic_submission(): void { + require_capability('moodle/site:config', $this->get_context_for_dynamic_submission()); + } + + /** + * Process the form submission, used if form was submitted via AJAX + */ + public function process_dynamic_submission() { + global $CFG; + require_once($CFG->dirroot.'/user/profile/definelib.php'); + profile_save_category($this->get_data()); + } + + /** + * Load in existing data as form defaults + */ + public function set_data_for_dynamic_submission(): void { + global $DB; + if ($id = $this->optional_param('id', 0, PARAM_INT)) { + $this->set_data($DB->get_record('user_info_category', ['id' => $id], '*', MUST_EXIST)); + } + } + + /** + * Returns url to set in $PAGE->set_url() when form is being rendered or submitted via AJAX + * + * @return moodle_url + */ + protected function get_page_url_for_dynamic_submission(): moodle_url { + $id = $this->optional_param('id', 0, PARAM_INT); + return new moodle_url('/user/profile/index.php', + ['action' => 'editcategory', 'id' => $id]); + } +} \ No newline at end of file diff --git a/user/classes/form/profile_field_form.php b/user/classes/form/profile_field_form.php new file mode 100644 index 00000000000..4dd6017bf58 --- /dev/null +++ b/user/classes/form/profile_field_form.php @@ -0,0 +1,183 @@ +. + +namespace core_user\form; + +use context; +use core_form\dynamic_form; +use moodle_url; +use profile_define_base; + +/** + * Class field_form used for profile fields. + * + * @package core_user + * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class profile_field_form extends dynamic_form { + + /** @var profile_define_base $field */ + public $field; + /** @var \stdClass */ + protected $fieldrecord; + + /** + * Define the form + */ + public function definition () { + global $CFG; + require_once($CFG->dirroot.'/user/profile/definelib.php'); + + $mform = $this->_form; + + // Everything else is dependant on the data type. + $datatype = $this->get_field_record()->datatype; + require_once($CFG->dirroot.'/user/profile/field/'.$datatype.'/define.class.php'); + $newfield = 'profile_define_'.$datatype; + $this->field = new $newfield(); + + // Add some extra hidden fields. + $mform->addElement('hidden', 'id'); + $mform->setType('id', PARAM_INT); + $mform->addElement('hidden', 'action', 'editfield'); + $mform->setType('action', PARAM_ALPHANUMEXT); + $mform->addElement('hidden', 'datatype', $datatype); + $mform->setType('datatype', PARAM_ALPHA); + + $this->field->define_form($mform); + } + + + /** + * Alter definition based on existing or submitted data + */ + public function definition_after_data () { + $mform = $this->_form; + $this->field->define_after_data($mform); + } + + + /** + * Perform some moodle validation. + * @param array $data + * @param array $files + * @return array + */ + public function validation($data, $files) { + return $this->field->define_validate($data, $files); + } + + /** + * Returns the defined editors for the field. + * @return array + */ + public function editors(): array { + $editors = $this->field->define_editors(); + return is_array($editors) ? $editors : []; + } + + /** + * Returns context where this form is used + * + * @return context + */ + protected function get_context_for_dynamic_submission(): context { + return \context_system::instance(); + } + + /** + * Checks if current user has access to this form, otherwise throws exception + */ + protected function check_access_for_dynamic_submission(): void { + require_capability('moodle/site:config', $this->get_context_for_dynamic_submission()); + } + + /** + * Process the form submission, used if form was submitted via AJAX + */ + public function process_dynamic_submission() { + global $CFG; + require_once($CFG->dirroot.'/user/profile/definelib.php'); + profile_save_field($this->get_data(), $this->editors()); + } + + /** + * Load in existing data as form defaults + */ + public function set_data_for_dynamic_submission(): void { + $field = $this->get_field_record(); + + // Clean and prepare description for the editor. + $description = clean_text($field->description, $field->descriptionformat); + $field->description = ['text' => $description, 'format' => $field->descriptionformat, 'itemid' => 0]; + // Convert the data format for. + if (is_array($this->editors())) { + foreach ($this->editors() as $editor) { + if (isset($field->$editor)) { + $editordesc = clean_text($field->$editor, $field->{$editor.'format'}); + $field->$editor = ['text' => $editordesc, 'format' => $field->{$editor.'format'}, 'itemid' => 0]; + } + } + } + + $this->set_data($field); + } + + /** + * Returns url to set in $PAGE->set_url() when form is being rendered or submitted via AJAX + * + * @return moodle_url + */ + protected function get_page_url_for_dynamic_submission(): moodle_url { + $id = $this->optional_param('id', 0, PARAM_INT); + $datatype = $this->optional_param('datatype', 'text', PARAM_PLUGIN); + return new moodle_url('/user/profile/index.php', + ['action' => 'editfield', 'id' => $id, 'datatype' => $id ? null : $datatype]); + } + + /** + * Record for the field from the database (or generic record for a new field) + * + * @return false|mixed|\stdClass + * @throws \coding_exception + * @throws \dml_exception + */ + public function get_field_record() { + global $DB; + + if (!$this->fieldrecord) { + $id = $this->optional_param('id', 0, PARAM_INT); + if (!$id || !($this->fieldrecord = $DB->get_record('user_info_field', ['id' => $id]))) { + $datatype = $this->optional_param('datatype', 'text', PARAM_PLUGIN); + $this->fieldrecord = new \stdClass(); + $this->fieldrecord->datatype = $datatype; + $this->fieldrecord->description = ''; + $this->fieldrecord->descriptionformat = FORMAT_HTML; + $this->fieldrecord->defaultdata = ''; + $this->fieldrecord->defaultdataformat = FORMAT_HTML; + $this->fieldrecord->categoryid = $this->optional_param('categoryid', 0, PARAM_INT); + } + if (!\core_component::get_component_directory('profilefield_'.$this->fieldrecord->datatype)) { + throw new \moodle_exception('fieldnotfound', 'customfield'); + } + } + + return $this->fieldrecord; + } +} + + diff --git a/user/filters/profilefield.php b/user/filters/profilefield.php index a46030e05cf..328ad70c0bc 100644 --- a/user/filters/profilefield.php +++ b/user/filters/profilefield.php @@ -73,11 +73,12 @@ class user_filter_profilefield extends user_filter_type { * @return array of profile fields */ public function get_profile_fields() { - global $DB; - $order = $DB->sql_order_by_text('name'); - if (!$fields = $DB->get_records_menu('user_info_field', null, $order, 'id, name')) { - return null; - } + global $CFG; + require_once($CFG->dirroot . '/user/profile/lib.php'); + + $fieldrecords = profile_get_custom_fields(); + $fields = array_combine(array_keys($fieldrecords), array_column($fieldrecords, 'name')); + core_collator::asort($fields); $res = array(0 => get_string('anyfield', 'filters')); return $res + $fields; diff --git a/user/profile/definelib.php b/user/profile/definelib.php index 22d37ee2011..bf57b78b743 100644 --- a/user/profile/definelib.php +++ b/user/profile/definelib.php @@ -32,7 +32,7 @@ class profile_define_base { /** * Prints out the form snippet for creating or editing a profile field - * @param moodleform $form instance of the moodleform class + * @param MoodleQuickForm $form instance of the moodleform class */ public function define_form(&$form) { $form->addElement('header', '_commonsettings', get_string('profilecommonsettings', 'admin')); @@ -45,7 +45,7 @@ class profile_define_base { /** * Prints out the form snippet for the part of creating or editing a profile field common to all data types. * - * @param moodleform $form instance of the moodleform class + * @param MoodleQuickForm $form instance of the moodleform class */ public function define_form_common(&$form) { @@ -88,7 +88,7 @@ class profile_define_base { /** * Prints out the form snippet for the part of creating or editing a profile field specific to the current data type. - * @param moodleform $form instance of the moodleform class + * @param MoodleQuickForm $form instance of the moodleform class */ public function define_form_specific($form) { // Do nothing - overwrite if necessary. @@ -138,7 +138,7 @@ class profile_define_base { $err['shortname'] = get_string('profileshortnameinvalid', 'admin'); } else { // Fetch field-record from DB. - $field = $DB->get_record('user_info_field', array('shortname' => $data->shortname)); + $field = profile_get_custom_field_data_by_shortname($data->shortname); // Check the shortname is unique. if ($field and $field->id <> $data->id) { $err['shortname'] = get_string('profileshortnamenotunique', 'admin'); @@ -166,7 +166,7 @@ class profile_define_base { /** * Alter form based on submitted or existing data - * @param moodleform $mform + * @param MoodleQuickForm $mform */ public function define_after_data(&$mform) { // Do nothing - overwrite if necessary. @@ -204,6 +204,7 @@ class profile_define_base { } else { \core\event\user_info_field_created::create_from_field($field)->trigger(); } + profile_purge_user_fields_cache(); } /** @@ -251,6 +252,7 @@ function profile_reorder_fields() { } } } + profile_purge_user_fields_cache(); } } @@ -268,6 +270,7 @@ function profile_reorder_categories() { $c->sortorder = $i++; $DB->update_record('user_info_category', $c); } + profile_purge_user_fields_cache(); } } @@ -326,6 +329,7 @@ function profile_delete_category($id) { profile_reorder_categories(); \core\event\user_info_category_deleted::create_from_category($category)->trigger(); + profile_purge_user_fields_cache(); return true; } @@ -355,6 +359,7 @@ function profile_delete_field($id) { $DB->delete_records('user_info_field', array('id' => $id)); \core\event\user_info_field_deleted::create_from_field($field)->trigger(); + profile_purge_user_fields_cache(); // Reorder the remaining fields in the same category. profile_reorder_fields(); @@ -445,6 +450,7 @@ function profile_move_category($id, $move) { \core\event\user_info_category_updated::create_from_category($category)->trigger(); \core\event\user_info_category_updated::create_from_category($swapcategory)->trigger(); + profile_purge_user_fields_cache(); return true; } @@ -478,18 +484,48 @@ function profile_list_categories() { return array_map('format_string', $categories); } +/** + * Create or update a profile category + * + * @param stdClass $data + */ +function profile_save_category(stdClass $data): void { + global $DB; + + if (empty($data->id)) { + unset($data->id); + $data->sortorder = $DB->count_records('user_info_category') + 1; + $data->id = $DB->insert_record('user_info_category', $data, true); + + $createdcategory = $DB->get_record('user_info_category', array('id' => $data->id)); + \core\event\user_info_category_created::create_from_category($createdcategory)->trigger(); + } else { + $DB->update_record('user_info_category', $data); + + $updatedcateogry = $DB->get_record('user_info_category', array('id' => $data->id)); + \core\event\user_info_category_updated::create_from_category($updatedcateogry)->trigger(); + } + profile_reorder_categories(); + profile_purge_user_fields_cache(); +} /** * Edit a category * + * @deprecated since Moodle 3.11 MDL-71051 - please do not use this function any more. + * @todo MDL-71413 This will be deleted in Moodle 4.3. + * @see profile_save_category() + * * @param int $id * @param string $redirect */ function profile_edit_category($id, $redirect) { global $DB, $OUTPUT, $CFG; - require_once($CFG->dirroot.'/user/profile/index_category_form.php'); - $categoryform = new category_form(); + debugging('Function profile_edit_category() is deprecated without replacement, see also profile_save_category()', + DEBUG_DEVELOPER); + + $categoryform = new \core_user\form\profile_category_form(); if ($category = $DB->get_record('user_info_category', array('id' => $id))) { $categoryform->set_data($category); @@ -499,22 +535,8 @@ function profile_edit_category($id, $redirect) { redirect($redirect); } else { if ($data = $categoryform->get_data()) { - if (empty($data->id)) { - unset($data->id); - $data->sortorder = $DB->count_records('user_info_category') + 1; - $data->id = $DB->insert_record('user_info_category', $data, true); - - $createdcategory = $DB->get_record('user_info_category', array('id' => $data->id)); - \core\event\user_info_category_created::create_from_category($createdcategory)->trigger(); - } else { - $DB->update_record('user_info_category', $data); - - $updatedcateogry = $DB->get_record('user_info_category', array('id' => $data->id)); - \core\event\user_info_category_updated::create_from_category($updatedcateogry)->trigger(); - } - profile_reorder_categories(); + profile_save_category($data); redirect($redirect); - } if (empty($id)) { @@ -533,78 +555,73 @@ function profile_edit_category($id, $redirect) { } +/** + * Save updated field definition or create a new field + * + * @param stdClass $data data from the form profile_field_form + * @param array $editors editors for this form field type + */ +function profile_save_field(stdClass $data, array $editors): void { + global $CFG; + + require_once($CFG->dirroot.'/user/profile/field/'.$data->datatype.'/define.class.php'); + $newfield = 'profile_define_'.$data->datatype; + /** @var profile_define_base $formfield */ + $formfield = new $newfield(); + + // Collect the description and format back into the proper data structure from the editor. + // Note: This field will ALWAYS be an editor. + $data->descriptionformat = $data->description['format']; + $data->description = $data->description['text']; + + // Check whether the default data is an editor, this is (currently) only the textarea field type. + if (is_array($data->defaultdata) && array_key_exists('text', $data->defaultdata)) { + // Collect the default data and format back into the proper data structure from the editor. + $data->defaultdataformat = $data->defaultdata['format']; + $data->defaultdata = $data->defaultdata['text']; + } + + // Convert the data format for. + if (is_array($editors)) { + foreach ($editors as $editor) { + if (isset($field->$editor)) { + $field->{$editor.'format'} = $field->{$editor}['format']; + $field->$editor = $field->{$editor}['text']; + } + } + } + + $formfield->define_save($data); + profile_reorder_fields(); + profile_reorder_categories(); +} + /** * Edit a profile field. * + * @deprecated since Moodle 3.11 MDL-71051 - please do not use this function any more. + * @todo MDL-71413 This will be deleted in Moodle 4.3. + * @see profile_save_field() + * * @param int $id * @param string $datatype * @param string $redirect */ function profile_edit_field($id, $datatype, $redirect) { - global $CFG, $DB, $OUTPUT, $PAGE; + global $OUTPUT, $PAGE; - if (!$field = $DB->get_record('user_info_field', array('id' => $id))) { - $field = new stdClass(); - $field->datatype = $datatype; - $field->description = ''; - $field->descriptionformat = FORMAT_HTML; - $field->defaultdata = ''; - $field->defaultdataformat = FORMAT_HTML; - } + debugging('Function profile_edit_field() is deprecated without replacement, see also profile_save_field()', + DEBUG_DEVELOPER); - // Clean and prepare description for the editor. - $field->description = clean_text($field->description, $field->descriptionformat); - $field->description = array('text' => $field->description, 'format' => $field->descriptionformat, 'itemid' => 0); - - require_once($CFG->dirroot.'/user/profile/index_field_form.php'); - $fieldform = new field_form(null, $field->datatype); - - // Convert the data format for. - if (is_array($fieldform->editors())) { - foreach ($fieldform->editors() as $editor) { - if (isset($field->$editor)) { - $field->$editor = clean_text($field->$editor, $field->{$editor.'format'}); - $field->$editor = array('text' => $field->$editor, 'format' => $field->{$editor.'format'}, 'itemid' => 0); - } - } - } - - $fieldform->set_data($field); + $fieldform = new \core_user\form\profile_field_form(); + $fieldform->set_data_for_dynamic_submission(); if ($fieldform->is_cancelled()) { redirect($redirect); } else { if ($data = $fieldform->get_data()) { - require_once($CFG->dirroot.'/user/profile/field/'.$datatype.'/define.class.php'); - $newfield = 'profile_define_'.$datatype; - $formfield = new $newfield(); - - // Collect the description and format back into the proper data structure from the editor. - // Note: This field will ALWAYS be an editor. - $data->descriptionformat = $data->description['format']; - $data->description = $data->description['text']; - - // Check whether the default data is an editor, this is (currently) only the textarea field type. - if (is_array($data->defaultdata) && array_key_exists('text', $data->defaultdata)) { - // Collect the default data and format back into the proper data structure from the editor. - $data->defaultdataformat = $data->defaultdata['format']; - $data->defaultdata = $data->defaultdata['text']; - } - - // Convert the data format for. - if (is_array($fieldform->editors())) { - foreach ($fieldform->editors() as $editor) { - if (isset($field->$editor)) { - $field->{$editor.'format'} = $field->{$editor}['format']; - $field->$editor = $field->{$editor}['text']; - } - } - } - - $formfield->define_save($data); - profile_reorder_fields(); - profile_reorder_categories(); + profile_save_field($data, $fieldform->editors()); redirect($redirect); } @@ -613,7 +630,7 @@ function profile_edit_field($id, $datatype, $redirect) { if (empty($id)) { $strheading = get_string('profilecreatenewfield', 'admin', $datatypes[$datatype]); } else { - $strheading = get_string('profileeditfield', 'admin', format_string($field->name)); + $strheading = get_string('profileeditfield', 'admin', format_string($fieldform->get_field_record()->name)); } // Print the page. @@ -626,4 +643,11 @@ function profile_edit_field($id, $datatype, $redirect) { } } - +/** + * Purge the cache for the user profile fields + */ +function profile_purge_user_fields_cache() { + $cache = \cache::make_from_params(cache_store::MODE_REQUEST, 'core_profile', 'customfields', + [], ['simplekeys' => true, 'simpledata' => true]); + $cache->purge(); +} diff --git a/user/profile/field/checkbox/tests/privacy_test.php b/user/profile/field/checkbox/tests/privacy_test.php index 9f4862e84bb..17d618c9767 100644 --- a/user/profile/field/checkbox/tests/privacy_test.php +++ b/user/profile/field/checkbox/tests/privacy_test.php @@ -276,13 +276,8 @@ class profilefield_checkbox_testcase extends provider_testcase { * @return int The ID of the profile category */ private function add_profile_category() { - global $DB; - // Create a new profile category. - $cat = new stdClass(); - $cat->name = 'Test category'; - $cat->sortorder = 1; - - return $DB->insert_record('user_info_category', $cat); + $cat = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Test category']); + return $cat->id; } /** @@ -293,20 +288,13 @@ class profilefield_checkbox_testcase extends provider_testcase { * @return int The ID of the profile field */ private function add_profile_field($categoryid, $datatype) { - global $DB; - // Create a new profile field. - $data = new stdClass(); - $data->datatype = $datatype; - $data->shortname = 'tstField'; - $data->name = 'Test field'; - $data->description = 'This is a test.'; - $data->required = false; - $data->locked = false; - $data->forceunique = false; - $data->signup = false; - $data->visible = '0'; - $data->categoryid = $categoryid; - - return $DB->insert_record('user_info_field', $data); + $data = $this->getDataGenerator()->create_custom_profile_field([ + 'datatype' => $datatype, + 'shortname' => 'tstField', + 'name' => 'Test field', + 'description' => 'This is a test.', + 'categoryid' => $categoryid, + ]); + return $data->id; } } diff --git a/user/profile/field/datetime/tests/privacy_test.php b/user/profile/field/datetime/tests/privacy_test.php index d18026e044a..eaa1921495a 100644 --- a/user/profile/field/datetime/tests/privacy_test.php +++ b/user/profile/field/datetime/tests/privacy_test.php @@ -280,13 +280,8 @@ class profilefield_datetime_testcase extends provider_testcase { * @return int The ID of the profile category */ private function add_profile_category() { - global $DB; - // Create a new profile category. - $cat = new stdClass(); - $cat->name = 'Test category'; - $cat->sortorder = 1; - - return $DB->insert_record('user_info_category', $cat); + $cat = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Test category']); + return $cat->id; } /** @@ -297,20 +292,13 @@ class profilefield_datetime_testcase extends provider_testcase { * @return int The ID of the profile field */ private function add_profile_field($categoryid, $datatype) { - global $DB; - // Create a new profile field. - $data = new stdClass(); - $data->datatype = $datatype; - $data->shortname = 'tstField'; - $data->name = 'Test field'; - $data->description = 'This is a test.'; - $data->required = false; - $data->locked = false; - $data->forceunique = false; - $data->signup = false; - $data->visible = '0'; - $data->categoryid = $categoryid; - - return $DB->insert_record('user_info_field', $data); + $data = $this->getDataGenerator()->create_custom_profile_field([ + 'datatype' => $datatype, + 'shortname' => 'tstField', + 'name' => 'Test field', + 'description' => 'This is a test.', + 'categoryid' => $categoryid, + ]); + return $data->id; } } diff --git a/user/profile/field/menu/tests/privacy_test.php b/user/profile/field/menu/tests/privacy_test.php index ff399ebb7d6..647f40fe13f 100644 --- a/user/profile/field/menu/tests/privacy_test.php +++ b/user/profile/field/menu/tests/privacy_test.php @@ -277,13 +277,8 @@ class profilefield_menu_testcase extends provider_testcase { * @return int The ID of the profile category */ private function add_profile_category() { - global $DB; - // Create a new profile category. - $cat = new stdClass(); - $cat->name = 'Test category'; - $cat->sortorder = 1; - - return $DB->insert_record('user_info_category', $cat); + $cat = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Test category']); + return $cat->id; } /** @@ -294,20 +289,13 @@ class profilefield_menu_testcase extends provider_testcase { * @return int The ID of the profile field */ private function add_profile_field($categoryid, $datatype) { - global $DB; - // Create a new profile field. - $data = new stdClass(); - $data->datatype = $datatype; - $data->shortname = 'tstField'; - $data->name = 'Test field'; - $data->description = 'This is a test.'; - $data->required = false; - $data->locked = false; - $data->forceunique = false; - $data->signup = false; - $data->visible = '0'; - $data->categoryid = $categoryid; - - return $DB->insert_record('user_info_field', $data); + $data = $this->getDataGenerator()->create_custom_profile_field([ + 'datatype' => $datatype, + 'shortname' => 'tstField', + 'name' => 'Test field', + 'description' => 'This is a test.', + 'categoryid' => $categoryid, + ]); + return $data->id; } } diff --git a/user/profile/field/social/tests/behat/social_profile_field.feature b/user/profile/field/social/tests/behat/social_profile_field.feature index 8abf51d8cc5..572cf386566 100644 --- a/user/profile/field/social/tests/behat/social_profile_field.feature +++ b/user/profile/field/social/tests/behat/social_profile_field.feature @@ -8,14 +8,16 @@ Feature: Social profile fields can not have a duplicate shortname. Scenario: Verify you can edit social profile fields. Given I log in as "admin" When I navigate to "Users > Accounts > User profile fields" in site administration - And I set the field "datatype" to "Social" + And I click on "Create a new profile field" "link" + And I click on "Social" "link" And I set the following fields to these values: - | Short name | yahoo | | Networktype | Yahoo ID | + | Short name | yahoo | And I click on "Save changes" "button" - And I set the field "datatype" to "Social" + And I click on "Create a new profile field" "link" + And I click on "Social" "link" And I set the following fields to these values: - | Short name | yahoo | | Networktype | Yahoo ID | + | Short name | yahoo | And I click on "Save changes" "button" Then I should see "This short name is already in use" diff --git a/user/profile/field/social/tests/privacy_test.php b/user/profile/field/social/tests/privacy_test.php index 4c336f04b32..38bd287fa42 100644 --- a/user/profile/field/social/tests/privacy_test.php +++ b/user/profile/field/social/tests/privacy_test.php @@ -276,13 +276,8 @@ class profilefield_social_testcase extends provider_testcase { * @return int The ID of the profile category */ private function add_profile_category() { - global $DB; - // Create a new profile category. - $cat = new stdClass(); - $cat->name = 'Test category'; - $cat->sortorder = 1; - - return $DB->insert_record('user_info_category', $cat); + $cat = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Test category']); + return $cat->id; } /** @@ -293,20 +288,13 @@ class profilefield_social_testcase extends provider_testcase { * @return int The ID of the profile field */ private function add_profile_field($categoryid, $datatype) { - global $DB; - // Create a new profile field. - $data = new stdClass(); - $data->datatype = $datatype; - $data->shortname = 'icq'; - $data->name = 'icq'; - $data->description = ''; - $data->required = false; - $data->locked = false; - $data->forceunique = false; - $data->signup = false; - $data->visible = '0'; - $data->categoryid = $categoryid; - - return $DB->insert_record('user_info_field', $data); + $data = $this->getDataGenerator()->create_custom_profile_field([ + 'datatype' => $datatype, + 'shortname' => 'icq', + 'name' => 'icq', + 'description' => '', + 'categoryid' => $categoryid, + ]); + return $data->id; } } diff --git a/user/profile/field/text/tests/privacy_test.php b/user/profile/field/text/tests/privacy_test.php index a5c35d5ff1c..a3e2bcc60be 100644 --- a/user/profile/field/text/tests/privacy_test.php +++ b/user/profile/field/text/tests/privacy_test.php @@ -277,13 +277,8 @@ class profilefield_text_testcase extends provider_testcase { * @return int The ID of the profile category */ private function add_profile_category() { - global $DB; - // Create a new profile category. - $cat = new stdClass(); - $cat->name = 'Test category'; - $cat->sortorder = 1; - - return $DB->insert_record('user_info_category', $cat); + $cat = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Test category']); + return $cat->id; } /** @@ -294,20 +289,13 @@ class profilefield_text_testcase extends provider_testcase { * @return int The ID of the profile field */ private function add_profile_field($categoryid, $datatype) { - global $DB; - // Create a new profile field. - $data = new stdClass(); - $data->datatype = $datatype; - $data->shortname = 'tstField'; - $data->name = 'Test field'; - $data->description = 'This is a test.'; - $data->required = false; - $data->locked = false; - $data->forceunique = false; - $data->signup = false; - $data->visible = '0'; - $data->categoryid = $categoryid; - - return $DB->insert_record('user_info_field', $data); + $data = $this->getDataGenerator()->create_custom_profile_field([ + 'datatype' => $datatype, + 'shortname' => 'tstField', + 'name' => 'Test field', + 'description' => 'This is a test.', + 'categoryid' => $categoryid, + ]); + return $data->id; } } diff --git a/user/profile/field/textarea/tests/privacy_test.php b/user/profile/field/textarea/tests/privacy_test.php index 5ed8bb49b61..1e47d57cb3e 100644 --- a/user/profile/field/textarea/tests/privacy_test.php +++ b/user/profile/field/textarea/tests/privacy_test.php @@ -276,13 +276,8 @@ class profilefield_textarea_testcase extends provider_testcase { * @return int The ID of the profile category */ private function add_profile_category() { - global $DB; - // Create a new profile category. - $cat = new stdClass(); - $cat->name = 'Test category'; - $cat->sortorder = 1; - - return $DB->insert_record('user_info_category', $cat); + $cat = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Test category']); + return $cat->id; } /** @@ -293,20 +288,13 @@ class profilefield_textarea_testcase extends provider_testcase { * @return int The ID of the profile field */ private function add_profile_field($categoryid, $datatype) { - global $DB; - // Create a new profile field. - $data = new stdClass(); - $data->datatype = $datatype; - $data->shortname = 'tstField'; - $data->name = 'Test field'; - $data->description = 'This is a test.'; - $data->required = false; - $data->locked = false; - $data->forceunique = false; - $data->signup = false; - $data->visible = '0'; - $data->categoryid = $categoryid; - - return $DB->insert_record('user_info_field', $data); + $data = $this->getDataGenerator()->create_custom_profile_field([ + 'datatype' => $datatype, + 'shortname' => 'tstField', + 'name' => 'Test field', + 'description' => 'This is a test.', + 'categoryid' => $categoryid, + ]); + return $data->id; } } diff --git a/user/profile/index.php b/user/profile/index.php index ae55294ed12..c2806e64fbf 100644 --- a/user/profile/index.php +++ b/user/profile/index.php @@ -32,10 +32,7 @@ $action = optional_param('action', '', PARAM_ALPHA); $redirect = $CFG->wwwroot.'/user/profile/index.php'; -$strchangessaved = get_string('changessaved'); -$strcancelled = get_string('cancelled'); $strdefaultcategory = get_string('profiledefaultcategory', 'admin'); -$strnofields = get_string('profilenofieldsdefined', 'admin'); $strcreatefield = get_string('profilecreatefield', 'admin'); @@ -91,19 +88,6 @@ switch ($action) { echo $OUTPUT->footer(); die; break; - case 'editfield': - $id = optional_param('id', 0, PARAM_INT); - $datatype = optional_param('datatype', '', PARAM_ALPHA); - - profile_edit_field($id, $datatype, $redirect); - die; - break; - case 'editcategory': - $id = optional_param('id', 0, PARAM_INT); - - profile_edit_category($id, $redirect); - die; - break; default: // Normal form. } @@ -124,14 +108,12 @@ if (empty($categories)) { echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('profilefields', 'admin')); -foreach ($categories as $category) { - $table = new html_table(); - $table->head = array(get_string('profilefield', 'admin'), get_string('edit')); - $table->align = array('left', 'right'); - $table->width = '95%'; - $table->attributes['class'] = 'generaltable profilefield'; - $table->data = array(); +$outputcategories = []; +$options = profile_list_datatypes(); +foreach ($categories as $category) { + // Category fields. + $outputfields = []; if ($fields = $DB->get_records('user_info_field', array('categoryid' => $category->id), 'sortorder ASC')) { foreach ($fields as $field) { $fieldname = format_string($field->name); @@ -140,139 +122,46 @@ foreach ($categories as $category) { if (class_exists($classname) && method_exists($classname, 'get_fieldname')) { $fieldname = $classname::get_fieldname($field->name); } - $table->data[] = array($fieldname, profile_field_icons($field)); + $outputfields[] = [ + 'id' => $field->id, + 'shortname' => $field->shortname, + 'datatype' => $field->datatype, + 'name' => $fieldname, + 'isfirst' => !count($outputfields), + 'islast' => count($outputfields) == count($fields) - 1, + ]; } } - echo $OUTPUT->heading(format_string($category->name) .' '.profile_category_icons($category)); - if (count($table->data)) { - echo html_writer::table($table); - } else { - echo $OUTPUT->notification($strnofields); + // Add new field menu. + $menu = new \action_menu(); + $menu->set_alignment(\action_menu::BL, \action_menu::BL); + $menu->set_menu_trigger($strcreatefield); + foreach ($options as $type => $fieldname) { + $action = new \action_menu_link_secondary(new \moodle_url('#'), null, $fieldname, + ['data-action' => 'createfield', 'data-categoryid' => $category->id, 'data-datatype' => $type, + 'data-datatypename' => $fieldname]); + $menu->add($action); } + $menu->attributes['class'] .= ' float-left mr-1'; -} // End of $categories foreach. + // Add category information to the template. + $outputcategories[] = [ + 'id' => $category->id, + 'name' => format_string($category->name), + 'fields' => $outputfields, + 'hasfields' => count($outputfields), + 'isfirst' => !count($outputcategories), + 'islast' => count($outputcategories) == count($categories) - 1, + 'candelete' => count($categories) > 1, + 'addfieldmenu' => $menu->export_for_template($OUTPUT), + ]; +} -echo '
'; -echo '
'; - -// Create a new field link. -$options = profile_list_datatypes(); -$popupurl = new moodle_url('/user/profile/index.php?id=0&action=editfield'); -echo $OUTPUT->single_select($popupurl, 'datatype', $options, '', array('' => get_string('choosedots')), 'newfieldform', array('label' => $strcreatefield)); - -// Add a div with a class so themers can hide, style or reposition the text. -html_writer::start_tag('div', array('class' => 'adminuseractionhint')); -echo get_string('or', 'lesson'); -html_writer::end_tag('div'); - -// Create a new category link. -$options = array('action' => 'editcategory'); -echo $OUTPUT->single_button(new moodle_url('index.php', $options), get_string('profilecreatecategory', 'admin')); - -echo '
'; +echo $OUTPUT->render_from_template('core_user/edit_profile_fields', [ + 'categories' => $outputcategories, + 'sesskey' => sesskey(), + 'baseurl' => (new moodle_url('/user/profile/index.php'))->out(false) +]); echo $OUTPUT->footer(); -die; - - -/***** Some functions relevant to this script *****/ - -/** - * Create a string containing the editing icons for the user profile categories - * @param stdClass $category the category object - * @return string the icon string - */ -function profile_category_icons($category) { - global $CFG, $USER, $DB, $OUTPUT; - - $strdelete = get_string('delete'); - $strmoveup = get_string('moveup'); - $strmovedown = get_string('movedown'); - $stredit = get_string('edit'); - - $categorycount = $DB->count_records('user_info_category'); - $fieldcount = $DB->count_records('user_info_field', array('categoryid' => $category->id)); - - // Edit. - $editstr = '' . - $OUTPUT->pix_icon('t/edit', $stredit) .' '; - - // Delete. - // Can only delete the last category if there are no fields in it. - if (($categorycount > 1) or ($fieldcount == 0)) { - $editstr .= 'id.'&action=deletecategory&sesskey='.sesskey() . '">'; - $editstr .= $OUTPUT->pix_icon('t/delete', $strdelete).' '; - } else { - $editstr .= $OUTPUT->spacer() . ' '; - } - - // Move up. - if ($category->sortorder > 1) { - $editstr .= 'id.'&action=movecategory&dir=up&sesskey='.sesskey().'">'; - $editstr .= $OUTPUT->pix_icon('t/up', $strmoveup) . ' '; - } else { - $editstr .= $OUTPUT->spacer() . ' '; - } - - // Move down. - if ($category->sortorder < $categorycount) { - $editstr .= 'id.'&action=movecategory&dir=down&sesskey='.sesskey().'">'; - $editstr .= $OUTPUT->pix_icon('t/down', $strmovedown) . ' '; - } else { - $editstr .= $OUTPUT->spacer() . ' '; - } - - return $editstr; -} - -/** - * Create a string containing the editing icons for the user profile fields - * @param stdClass $field the field object - * @return string the icon string - */ -function profile_field_icons($field) { - global $CFG, $USER, $DB, $OUTPUT; - - $strdelete = get_string('delete'); - $strmoveup = get_string('moveup'); - $strmovedown = get_string('movedown'); - $stredit = get_string('edit'); - - $fieldcount = $DB->count_records('user_info_field', array('categoryid' => $field->categoryid)); - $datacount = $DB->count_records('user_info_data', array('fieldid' => $field->id)); - - // Edit. - $editstr = ''; - $editstr .= $OUTPUT->pix_icon('t/edit', $stredit) . ' '; - - // Delete. - $editstr .= ''; - $editstr .= $OUTPUT->pix_icon('t/delete', $strdelete) . ' '; - - // Move up. - if ($field->sortorder > 1) { - $editstr .= 'id.'&action=movefield&dir=up&sesskey='.sesskey().'">'; - $editstr .= $OUTPUT->pix_icon('t/up', $strmoveup) . ' '; - } else { - $editstr .= $OUTPUT->spacer() . ' '; - } - - // Move down. - if ($field->sortorder < $fieldcount) { - $editstr .= 'id.'&action=movefield&dir=down&sesskey='.sesskey().'">'; - $editstr .= $OUTPUT->pix_icon('t/down', $strmovedown) . ' '; - } else { - $editstr .= $OUTPUT->spacer() . ' '; - } - - return $editstr; -} - - - diff --git a/user/profile/index_category_form.php b/user/profile/index_category_form.php deleted file mode 100644 index 551e7fb2879..00000000000 --- a/user/profile/index_category_form.php +++ /dev/null @@ -1,95 +0,0 @@ -. - -/** - * This file contains the profile field category form. - * - * @package core_user - * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -if (!defined('MOODLE_INTERNAL')) { - die('Direct access to this script is forbidden.'); // It must be included from a Moodle page. -} - -require_once($CFG->dirroot.'/lib/formslib.php'); - -/** - * Class category_form - * - * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class category_form extends moodleform { - - /** - * Define the form. - */ - public function definition () { - global $USER, $CFG; - - $mform = $this->_form; - - $strrequired = get_string('required'); - - // Add some extra hidden fields. - $mform->addElement('hidden', 'id'); - $mform->setType('id', PARAM_INT); - $mform->addElement('hidden', 'action', 'editcategory'); - $mform->setType('action', PARAM_ALPHANUMEXT); - - $mform->addElement('text', 'name', get_string('profilecategoryname', 'admin'), 'maxlength="255" size="30"'); - $mform->setType('name', PARAM_TEXT); - $mform->addRule('name', $strrequired, 'required', null, 'client'); - - $this->add_action_buttons(true); - - } - - /** - * Perform some moodle validation. - * - * @param array $data - * @param array $files - * @return array - */ - public function validation($data, $files) { - global $CFG, $DB; - $errors = parent::validation($data, $files); - - $data = (object)$data; - - $duplicate = $DB->get_field('user_info_category', 'id', array('name' => $data->name)); - - // Check the name is unique. - if (!empty($data->id)) { // We are editing an existing record. - $olddata = $DB->get_record('user_info_category', array('id' => $data->id)); - // Name has changed, new name in use, new name in use by another record. - $dupfound = (($olddata->name !== $data->name) && $duplicate && ($data->id != $duplicate)); - } else { // New profile category. - $dupfound = $duplicate; - } - - if ($dupfound ) { - $errors['name'] = get_string('profilecategorynamenotunique', 'admin'); - } - - return $errors; - } -} - - diff --git a/user/profile/index_field_form.php b/user/profile/index_field_form.php deleted file mode 100644 index 39d168c07f3..00000000000 --- a/user/profile/index_field_form.php +++ /dev/null @@ -1,100 +0,0 @@ -. - -/** - * This file contains the Field Form used for profile fields. - * - * @package core_user - * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -if (!defined('MOODLE_INTERNAL')) { - die('Direct access to this script is forbidden.'); // It must be included from a Moodle page. -} - -require_once($CFG->dirroot.'/lib/formslib.php'); - -/** - * Class field_form - * - * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class field_form extends moodleform { - - /** @var profile_define_base $field */ - public $field; - - /** - * Define the form - */ - public function definition () { - global $CFG; - - $mform = $this->_form; - - // Everything else is dependant on the data type. - $datatype = $this->_customdata; - require_once($CFG->dirroot.'/user/profile/field/'.$datatype.'/define.class.php'); - $newfield = 'profile_define_'.$datatype; - $this->field = new $newfield(); - - $strrequired = get_string('required'); - - // Add some extra hidden fields. - $mform->addElement('hidden', 'id'); - $mform->setType('id', PARAM_INT); - $mform->addElement('hidden', 'action', 'editfield'); - $mform->setType('action', PARAM_ALPHANUMEXT); - $mform->addElement('hidden', 'datatype', $datatype); - $mform->setType('datatype', PARAM_ALPHA); - - $this->field->define_form($mform); - - $this->add_action_buttons(true); - } - - - /** - * Alter definition based on existing or submitted data - */ - public function definition_after_data () { - $mform = $this->_form; - $this->field->define_after_data($mform); - } - - - /** - * Perform some moodle validation. - * @param array $data - * @param array $files - * @return array - */ - public function validation($data, $files) { - return $this->field->define_validate($data, $files); - } - - /** - * Returns the defined editors for the field. - * @return mixed - */ - public function editors() { - return $this->field->define_editors(); - } -} - - diff --git a/user/profile/lib.php b/user/profile/lib.php index cd74a4d35a2..d4b59d44e86 100644 --- a/user/profile/lib.php +++ b/user/profile/lib.php @@ -84,7 +84,7 @@ class profile_field_base { * Constructor method. * @param int $fieldid id of the profile from the user_info_field table * @param int $userid id of the user for whom we are displaying data - * @param object $fielddata optional data for the field object plus additional fields 'hasuserdata', 'data' and 'dataformat' + * @param stdClass $fielddata optional data for the field object plus additional fields 'hasuserdata', 'data' and 'dataformat' * with user data. (If $fielddata->hasuserdata is empty, user data is not available and we should use default data). * If this parameter is passed, constructor will not call load_data() at all. */ @@ -130,7 +130,7 @@ class profile_field_base { /** * Abstract method: Adds the profile field to the moodle form class * @abstract The following methods must be overwritten by child classes - * @param moodleform $mform instance of the moodleform class + * @param MoodleQuickForm $mform instance of the moodleform class */ public function edit_field_add($mform) { print_error('mustbeoveride', 'debug', '', 'edit_field_add'); @@ -148,7 +148,7 @@ class profile_field_base { /** * Print out the form field in the edit profile page - * @param moodleform $mform instance of the moodleform class + * @param MoodleQuickForm $mform instance of the moodleform class * @return bool */ public function edit_field($mform) { @@ -164,7 +164,7 @@ class profile_field_base { /** * Tweaks the edit form - * @param moodleform $mform instance of the moodleform class + * @param MoodleQuickForm $mform instance of the moodleform class * @return bool */ public function edit_after_data($mform) { @@ -179,7 +179,6 @@ class profile_field_base { /** * Saves the data coming from form * @param stdClass $usernew data coming from the form - * @return mixed returns data id if success of db insert/update, false on fail, 0 if not permitted */ public function edit_save_data($usernew) { global $DB; @@ -213,7 +212,7 @@ class profile_field_base { * Validate the form field from profile page * * @param stdClass $usernew - * @return string contains error message otherwise null + * @return array error messages for the form validation */ public function edit_validate_field($usernew) { global $DB; @@ -256,7 +255,7 @@ class profile_field_base { /** * Sets the default data for the field in the form object - * @param moodleform $mform instance of the moodleform class + * @param MoodleQuickForm $mform instance of the moodleform class */ public function edit_field_set_default($mform) { if (!empty($this->field->defaultdata)) { @@ -267,7 +266,7 @@ class profile_field_base { /** * Sets the required flag for the field in the form object * - * @param moodleform $mform instance of the moodleform class + * @param MoodleQuickForm $mform instance of the moodleform class */ public function edit_field_set_required($mform) { global $USER; @@ -278,7 +277,7 @@ class profile_field_base { /** * HardFreeze the field if locked. - * @param moodleform $mform instance of the moodleform class + * @param MoodleQuickForm $mform instance of the moodleform class */ public function edit_field_set_locked($mform) { if (!$mform->elementExists($this->inputname)) { @@ -389,6 +388,15 @@ class profile_field_base { $this->categoryname = $categoryname; } + /** + * Return field short name + * + * @return string + */ + public function get_shortname(): string { + return $this->field->shortname; + } + /** * Returns the name of the profile category where this field is * @@ -567,7 +575,7 @@ class profile_field_base { * @param int $userid * @return profile_field_base[] */ -function profile_get_user_fields_with_data($userid) { +function profile_get_user_fields_with_data(int $userid): array { global $DB, $CFG; // Join any user info data present with each user info field for the user object. @@ -601,7 +609,7 @@ function profile_get_user_fields_with_data($userid) { * @param int $userid * @return profile_field_base[][] */ -function profile_get_user_fields_with_data_by_category($userid) { +function profile_get_user_fields_with_data_by_category(int $userid): array { $fields = profile_get_user_fields_with_data($userid); $data = []; foreach ($fields as $field) { @@ -614,9 +622,7 @@ function profile_get_user_fields_with_data_by_category($userid) { * Loads user profile field data into the user object. * @param stdClass $user */ -function profile_load_data($user) { - global $CFG; - +function profile_load_data(stdClass $user): void { $fields = profile_get_user_fields_with_data($user->id); foreach ($fields as $formfield) { $formfield->edit_load_user_data($user); @@ -626,10 +632,10 @@ function profile_load_data($user) { /** * Print out the customisable categories and fields for a users profile * - * @param moodleform $mform instance of the moodleform class - * @param int $userid id of user whose profile is being edited. + * @param MoodleQuickForm $mform instance of the moodleform class + * @param int $userid id of user whose profile is being edited or 0 for the new user */ -function profile_definition($mform, $userid = 0) { +function profile_definition(MoodleQuickForm $mform, int $userid = 0): void { $categories = profile_get_user_fields_with_data_by_category($userid); foreach ($categories as $categoryid => $fields) { // Check first if *any* fields will be displayed. @@ -655,12 +661,10 @@ function profile_definition($mform, $userid = 0) { /** * Adds profile fields to user edit forms. - * @param moodleform $mform + * @param MoodleQuickForm $mform * @param int $userid */ -function profile_definition_after_data($mform, $userid) { - global $CFG; - +function profile_definition_after_data(MoodleQuickForm $mform, int $userid): void { $userid = ($userid < 0) ? 0 : (int)$userid; $fields = profile_get_user_fields_with_data($userid); @@ -673,11 +677,9 @@ function profile_definition_after_data($mform, $userid) { * Validates profile data. * @param stdClass $usernew * @param array $files - * @return array + * @return array array of errors, same as in {@see moodleform::validation()} */ -function profile_validation($usernew, $files) { - global $CFG; - +function profile_validation(stdClass $usernew, array $files): array { $err = array(); $fields = profile_get_user_fields_with_data($usernew->id); foreach ($fields as $formfield) { @@ -690,7 +692,7 @@ function profile_validation($usernew, $files) { * Saves profile data for a user. * @param stdClass $usernew */ -function profile_save_data($usernew) { +function profile_save_data(stdClass $usernew): void { global $CFG; $fields = profile_get_user_fields_with_data($usernew->id); @@ -701,10 +703,15 @@ function profile_save_data($usernew) { /** * Display profile fields. + * + * @deprecated since Moodle 3.11 MDL-71051 - please do not use this function any more. + * @todo MDL-71413 This will be deleted in Moodle 4.3. + * * @param int $userid */ function profile_display_fields($userid) { - global $CFG, $USER, $DB; + debugging('Function profile_display_fields() is deprecated because it is no longer used and will be '. + 'removed in future versions of Moodle', DEBUG_DEVELOPER); $categories = profile_get_user_fields_with_data_by_category($userid); foreach ($categories as $categoryid => $fields) { @@ -723,28 +730,16 @@ function profile_display_fields($userid) { * @return array list of profile fields info * @since Moodle 3.2 */ -function profile_get_signup_fields() { - global $CFG, $DB; - +function profile_get_signup_fields(): array { $profilefields = array(); - // Only retrieve required custom fields (with category information) - // results are sort by categories, then by fields. - $sql = "SELECT uf.id as fieldid, ic.id as categoryid, ic.name as categoryname, uf.datatype - FROM {user_info_field} uf - JOIN {user_info_category} ic - ON uf.categoryid = ic.id AND uf.signup = 1 AND uf.visible<>0 - ORDER BY ic.sortorder ASC, uf.sortorder ASC"; - - if ($fields = $DB->get_records_sql($sql)) { - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $fieldobject = new $newfield($field->fieldid); - + $fieldobjects = profile_get_user_fields_with_data(0); + foreach ($fieldobjects as $fieldobject) { + $field = (object)$fieldobject->get_field_config_for_external(); + if ($fieldobject->get_category_name() !== null && $fieldobject->is_signup_field() && $field->visible <> 0) { $profilefields[] = (object) array( 'categoryid' => $field->categoryid, - 'categoryname' => $field->categoryname, - 'fieldid' => $field->fieldid, + 'categoryname' => $fieldobject->get_category_name(), + 'fieldid' => $field->id, 'datatype' => $field->datatype, 'object' => $fieldobject ); @@ -756,9 +751,9 @@ function profile_get_signup_fields() { /** * Adds code snippet to a moodle form object for custom profile fields that * should appear on the signup page - * @param moodleform $mform moodle form object + * @param MoodleQuickForm $mform moodle form object */ -function profile_signup_fields($mform) { +function profile_signup_fields(MoodleQuickForm $mform): void { if ($fields = profile_get_signup_fields()) { foreach ($fields as $field) { @@ -774,13 +769,11 @@ function profile_signup_fields($mform) { /** * Returns an object with the custom profile fields set for the given user - * @param integer $userid + * @param int $userid * @param bool $onlyinuserobject True if you only want the ones in $USER. - * @return stdClass + * @return stdClass object where properties names are shortnames of custom profile fields */ -function profile_user_record($userid, $onlyinuserobject = true) { - global $CFG; - +function profile_user_record(int $userid, bool $onlyinuserobject = true): stdClass { $usercustomfields = new stdClass(); $fields = profile_get_user_fields_with_data($userid); @@ -807,33 +800,15 @@ function profile_user_record($userid, $onlyinuserobject = true) { * @return array Array of field objects from database (indexed by id) * @since Moodle 2.7.1 */ -function profile_get_custom_fields($onlyinuserobject = false) { - global $DB, $CFG; - - // Get all the fields. - $fields = $DB->get_records('user_info_field', null, 'id ASC'); - - // If only doing the user object ones, unset the rest. - if ($onlyinuserobject) { - foreach ($fields as $id => $field) { - require_once($CFG->dirroot . '/user/profile/field/' . - $field->datatype . '/field.class.php'); - $newfield = 'profile_field_' . $field->datatype; - $formfield = new $newfield(); - if (!$formfield->is_user_object_data()) { - unset($fields[$id]); - } +function profile_get_custom_fields(bool $onlyinuserobject = false): array { + $fieldobjects = profile_get_user_fields_with_data(0); + $fields = []; + foreach ($fieldobjects as $fieldobject) { + if (!$onlyinuserobject || $fieldobject->is_user_object_data()) { + $fields[$fieldobject->fieldid] = (object)$fieldobject->get_field_config_for_external(); } } - - foreach ($fields as $index => $field) { - $component = 'profilefield_' . $field->datatype; - $classname = "\\$component\\helper"; - if (class_exists($classname) && method_exists($classname, 'get_fieldname')) { - $fields[$index]->name = $classname::get_fieldname($field->name); - } - } - + ksort($fields); return $fields; } @@ -855,8 +830,10 @@ function profile_load_custom_fields($user) { function profile_save_custom_fields($userid, $profilefields) { global $DB; - if ($fields = $DB->get_records('user_info_field')) { - foreach ($fields as $field) { + $fields = profile_get_user_fields_with_data(0); + if ($fields) { + foreach ($fields as $fieldobject) { + $field = (object)$fieldobject->get_field_config_for_external(); if (isset($profilefields[$field->shortname])) { $conditions = array('fieldid' => $field->id, 'userid' => $userid); $id = $DB->get_field('user_info_data', 'id', $conditions); @@ -877,26 +854,22 @@ function profile_save_custom_fields($userid, $profilefields) { * current request for all fields so that it can be used quickly. * * @param string $shortname Shortname of custom profile field - * @return array Array with id, name, and visible fields + * @return stdClass|null Object with properties id, shortname, name, visible, datatype, categoryid, etc */ -function profile_get_custom_field_data_by_shortname(string $shortname): array { - global $DB; - +function profile_get_custom_field_data_by_shortname(string $shortname): ?stdClass { $cache = \cache::make_from_params(cache_store::MODE_REQUEST, 'core_profile', 'customfields', [], ['simplekeys' => true, 'simpledata' => true]); $data = $cache->get($shortname); - if (!$data) { + if ($data === false) { // If we don't have data, we get and cache it for all fields to avoid multiple DB requests. - $fields = $DB->get_records('user_info_field', null, '', 'id, shortname, name, visible'); + $fields = profile_get_custom_fields(); + $data = null; foreach ($fields as $field) { - $cache->set($field->shortname, (array)$field); + $cache->set($field->shortname, $field); if ($field->shortname === $shortname) { - $data = (array)$field; + $data = $field; } } - if (!$data) { - throw new \coding_exception('Unknown custom field: ' . $shortname); - } } return $data; @@ -946,15 +919,12 @@ function profile_view($user, $context, $course = null) { * @return bool */ function profile_has_required_custom_fields_set($userid) { - global $DB; - - $sql = "SELECT f.id - FROM {user_info_field} f - LEFT JOIN {user_info_data} d ON (d.fieldid = f.id AND d.userid = ?) - WHERE f.required = 1 AND f.visible > 0 AND f.locked = 0 AND d.id IS NULL"; - - if ($DB->record_exists_sql($sql, [$userid])) { - return false; + $profilefields = profile_get_user_fields_with_data($userid); + foreach ($profilefields as $profilefield) { + if ($profilefield->is_required() && !$profilefield->is_locked() && + $profilefield->is_empty() && $profilefield->get_field_config_for_external()['visible']) { + return false; + } } return true; diff --git a/user/templates/edit_profile_fields.mustache b/user/templates/edit_profile_fields.mustache new file mode 100644 index 00000000000..97faa9e4b8b --- /dev/null +++ b/user/templates/edit_profile_fields.mustache @@ -0,0 +1,141 @@ +{{! + 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 . +}} +{{! + @template core_user/edit_profile_fields + + UI for editing profile fields + + Example context (json): + { + "baseurl": "index.php", + "sesskey": "12345", + "categories": [ + { + "id": 1, + "name": "Cat1", + "fields": [ + {"id": 1, "name": "Field1", "isfirst": true, "islast": false}, + {"id": 2, "name": "Field2", "isfirst": false, "islast": false}, + {"id": 3, "name": "Field3", "isfirst": false, "islast": true} + ], + "hasfields": true, + "isfirst": true, + "candelete": true + }, + { + "id": 2, + "name": "Cat2", + "candelete": true + }, + { + "id": 3, + "name": "Cat3", + "islast": true, + "candelete": true + } + ] + } +}} + + + +
+{{#categories}} +
+
+
+

+ {{{name}}} + + {{#pix}}t/edit, core, {{#str}}edit{{/str}}{{/pix}} + {{#candelete}} + + {{#pix}}t/delete, core, {{#str}}delete{{/str}}{{/pix}} + {{/candelete}} + {{^isfirst}} + + {{#pix}}t/up, core, {{#str}}moveup{{/str}}{{/pix}} + {{/isfirst}} + {{#isfirst}}{{#pix}}spacer, moodle{{/pix}}{{/isfirst}} + {{^islast}} + + {{#pix}}t/down, core, {{#str}}movedown{{/str}}{{/pix}} + {{/islast}} +

+
+
+ {{#addfieldmenu}}{{> core/action_menu}}{{/addfieldmenu}} +
+
+ + + {{#hasfields}} + + + + + + + + {{#fields}} + + + + + {{/fields}} + + {{/hasfields}} + {{^hasfields}} + + + + + + {{/hasfields}} +
{{#str}}profilefield, admin{{/str}}{{#str}}edit{{/str}}
+ {{{name}}} + + + {{#pix}}t/edit, core, {{#str}}edit{{/str}}{{/pix}} + + {{#pix}}t/delete, core, {{#str}}delete{{/str}}{{/pix}} + {{^isfirst}} + + {{#pix}}t/up, core, {{#str}}moveup{{/str}}{{/pix}} + {{/isfirst}} + {{#isfirst}}{{#pix}}spacer, moodle{{/pix}}{{/isfirst}} + {{^islast}} + + {{#pix}}t/down, core, {{#str}}movedown{{/str}}{{/pix}} + {{/islast}} + {{#islast}}{{#pix}}spacer, moodle{{/pix}}{{/islast}} +
+ {{#str}}profilenofieldsdefined, admin{{/str}} +
+
+{{/categories}} +
+ +{{#js}} + require(['core_user/edit_profile_fields'], function(s) { + s.init(); + }); +{{/js}} diff --git a/user/tests/behat/custom_profile_fields.feature b/user/tests/behat/custom_profile_fields.feature index a17a063c470..31475fc59b7 100644 --- a/user/tests/behat/custom_profile_fields.feature +++ b/user/tests/behat/custom_profile_fields.feature @@ -17,7 +17,8 @@ Feature: Custom profile fields should be visible and editable by those with the And I log in as "admin" And I navigate to "Users > Accounts > User profile fields" in site administration - And I set the field "datatype" to "Text input" + And I click on "Create a new profile field" "link" + And I click on "Text input" "link" And I set the following fields to these values: | Short name | notvisible_field | | Name | notvisible_field | @@ -25,7 +26,8 @@ Feature: Custom profile fields should be visible and editable by those with the | Who is this field visible to? | Not visible | And I click on "Save changes" "button" - And I set the field "datatype" to "Text input" + And I click on "Create a new profile field" "link" + And I click on "Text input" "link" And I set the following fields to these values: | Short name | uservisible_field | | Name | uservisible_field | @@ -33,7 +35,8 @@ Feature: Custom profile fields should be visible and editable by those with the | Who is this field visible to? | Visible to user | And I click on "Save changes" "button" - And I set the field "datatype" to "Text input" + And I click on "Create a new profile field" "link" + And I click on "Text input" "link" And I set the following fields to these values: | Short name | everyonevisible_field | | Name | everyonevisible_field | @@ -41,7 +44,8 @@ Feature: Custom profile fields should be visible and editable by those with the | Who is this field visible to? | Visible to everyone | And I click on "Save changes" "button" - And I set the field "datatype" to "Text input" + And I click on "Create a new profile field" "link" + And I click on "Text input" "link" And I set the following fields to these values: | Short name | teachervisible_field | | Name | teachervisible_field | diff --git a/user/tests/profilelib_test.php b/user/tests/profilelib_test.php index d598a1d4b4f..77da39879f1 100644 --- a/user/tests/profilelib_test.php +++ b/user/tests/profilelib_test.php @@ -39,16 +39,16 @@ class core_user_profilelib_testcase extends advanced_testcase { * with profile_user_record. */ public function test_get_custom_fields() { - global $DB, $CFG; + global $CFG; require_once($CFG->dirroot . '/user/profile/lib.php'); $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); // Add a custom field of textarea type. - $id1 = $DB->insert_record('user_info_field', array( - 'shortname' => 'frogdesc', 'name' => 'Description of frog', 'categoryid' => 1, - 'datatype' => 'textarea')); + $id1 = $this->getDataGenerator()->create_custom_profile_field([ + 'shortname' => 'frogdesc', 'name' => 'Description of frog', + 'datatype' => 'textarea'])->id; // Check the field is returned. $result = profile_get_custom_fields(); @@ -66,9 +66,9 @@ class core_user_profilelib_testcase extends advanced_testcase { $this->assertObjectHasAttribute('frogdesc', profile_user_record($user->id, false)); // Add another custom field, this time of normal text type. - $id2 = $DB->insert_record('user_info_field', array( - 'shortname' => 'frogname', 'name' => 'Name of frog', 'categoryid' => 1, - 'datatype' => 'text')); + $id2 = $this->getDataGenerator()->create_custom_profile_field(array( + 'shortname' => 'frogname', 'name' => 'Name of frog', + 'datatype' => 'text'))->id; // Check both are returned using normal option. $result = profile_get_custom_fields(); @@ -147,26 +147,26 @@ class core_user_profilelib_testcase extends advanced_testcase { * Test that {@link user_not_fully_set_up()} takes required custom fields into account. */ public function test_profile_has_required_custom_fields_set() { - global $CFG, $DB; + global $CFG; require_once($CFG->dirroot.'/mnet/lib.php'); $this->resetAfterTest(); // Add a required, visible, unlocked custom field. - $DB->insert_record('user_info_field', ['shortname' => 'house', 'name' => 'House', 'required' => 1, - 'visible' => 1, 'locked' => 0, 'categoryid' => 1, 'datatype' => 'text']); + $this->getDataGenerator()->create_custom_profile_field(['shortname' => 'house', 'name' => 'House', 'required' => 1, + 'visible' => 1, 'locked' => 0, 'datatype' => 'text']); // Add an optional, visible, unlocked custom field. - $DB->insert_record('user_info_field', ['shortname' => 'pet', 'name' => 'Pet', 'required' => 0, - 'visible' => 1, 'locked' => 0, 'categoryid' => 1, 'datatype' => 'text']); + $this->getDataGenerator()->create_custom_profile_field(['shortname' => 'pet', 'name' => 'Pet', 'required' => 0, + 'visible' => 1, 'locked' => 0, 'datatype' => 'text']); // Add required but invisible custom field. - $DB->insert_record('user_info_field', ['shortname' => 'secretid', 'name' => 'Secret ID', 'required' => 1, - 'visible' => 0, 'locked' => 0, 'categoryid' => 1, 'datatype' => 'text']); + $this->getDataGenerator()->create_custom_profile_field(['shortname' => 'secretid', 'name' => 'Secret ID', + 'required' => 1, 'visible' => 0, 'locked' => 0, 'datatype' => 'text']); // Add required but locked custom field. - $DB->insert_record('user_info_field', ['shortname' => 'muggleborn', 'name' => 'Muggle-born', 'required' => 1, - 'visible' => 1, 'locked' => 1, 'categoryid' => 1, 'datatype' => 'checkbox']); + $this->getDataGenerator()->create_custom_profile_field(['shortname' => 'muggleborn', 'name' => 'Muggle-born', + 'required' => 1, 'visible' => 1, 'locked' => 1, 'datatype' => 'checkbox']); // Create some student accounts. $hermione = $this->getDataGenerator()->create_user(); @@ -215,14 +215,14 @@ class core_user_profilelib_testcase extends advanced_testcase { * Test that user generator sets the custom profile fields */ public function test_profile_fields_in_generator() { - global $CFG, $DB; + global $CFG; require_once($CFG->dirroot.'/mnet/lib.php'); $this->resetAfterTest(); // Add a required, visible, unlocked custom field. - $DB->insert_record('user_info_field', ['shortname' => 'house', 'name' => 'House', 'required' => 1, - 'visible' => 1, 'locked' => 0, 'categoryid' => 1, 'datatype' => 'text']); + $this->getDataGenerator()->create_custom_profile_field(['shortname' => 'house', 'name' => 'House', 'required' => 1, + 'visible' => 1, 'locked' => 0, 'datatype' => 'text', 'defaultdata' => null]); // Create some student accounts. $hermione = $this->getDataGenerator()->create_user(['profile_field_house' => 'Gryffindor']); @@ -261,17 +261,17 @@ class core_user_profilelib_testcase extends advanced_testcase { // Get the first field data and check it is correct. $data = profile_get_custom_field_data_by_shortname('speciality'); - $this->assertEquals('Speciality', $data['name']); - $this->assertEquals(PROFILE_VISIBLE_ALL, $data['visible']); - $this->assertEquals($field1->id, $data['id']); + $this->assertEquals('Speciality', $data->name); + $this->assertEquals(PROFILE_VISIBLE_ALL, $data->visible); + $this->assertEquals($field1->id, $data->id); // Get the second field data, checking there is no database query this time. $before = $DB->perf_get_queries(); $data = profile_get_custom_field_data_by_shortname('veggie'); $this->assertEquals($before, $DB->perf_get_queries()); - $this->assertEquals('Vegetarian', $data['name']); - $this->assertEquals(PROFILE_VISIBLE_PRIVATE, $data['visible']); - $this->assertEquals($field2->id, $data['id']); + $this->assertEquals('Vegetarian', $data->name); + $this->assertEquals(PROFILE_VISIBLE_PRIVATE, $data->visible); + $this->assertEquals($field2->id, $data->id); } /** @@ -281,7 +281,6 @@ class core_user_profilelib_testcase extends advanced_testcase { global $CFG; require_once($CFG->dirroot . '/user/profile/lib.php'); - $this->expectExceptionMessage('Unknown custom field: speciality'); - profile_get_custom_field_data_by_shortname('speciality'); + $this->assertNull(profile_get_custom_field_data_by_shortname('speciality')); } }