Index: includes/theme.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/theme.inc,v retrieving revision 1.422 diff -u -r1.422 theme.inc --- includes/theme.inc 6 May 2008 12:18:45 -0000 1.422 +++ includes/theme.inc 9 May 2008 16:50:47 -0000 @@ -1534,42 +1534,66 @@ } /** - * Format a username. + * Format the user's name for use on a rendered page title, node, or comment. + * + * Use $user->name, or $user->displayed_name, depending on admin settings. * * @param $object * The user object to format, usually returned from user_load(). * @return - * A string containing an HTML link to the user's page if the passed object - * suggests that this is a site user. Otherwise, only the username is returned. + * A string containing $user->name, or $user->displayed_name, depending on + * admin settings. This string is formatted as an an HTML link to the user's + * profile page if the passed object suggests that this is a site user. + * If not a site user, only the string is returned. */ -function theme_username($object) { - - if ($object->uid && $object->name) { - // Shorten the name when it is too long or it will break many tables. - if (drupal_strlen($object->name) > 20) { - $name = drupal_substr($object->name, 0, 15) . '...'; - } - else { - $name = $object->name; +function theme_username($object, $displayname = FALSE, $truncate = TRUE, $length = 20) { + // Should the displayed_name be absent from the $object, we're theming a node + // or a comment, and need to know it's author. + if (variable_get('user_displayed_name', 0) && empty($object->displayed_name) && $displayname) { + $author = user_load($object->uid); + $name = $author->displayed_name; + // If the user has failed to enter a displayed_name, prevent an 'anonymous' displayname. + if (empty($object->displayed_name) && !$author->displayed_name) { + $name = t(variable_get('user_displayed_name_token', '...')); } + } + // Return the displayed_name if it's there. + elseif (variable_get('user_displayed_name', 0) && !empty($object->displayed_name) && $displayname) { + $name = $object->displayed_name; + } + else { + $name = $object->name; + } - if (user_access('access user profiles')) { + // Shorten the name when it is too long or it may break tables and blocks. + if (drupal_strlen($name) > $length && $truncate) { + $name = drupal_substr($name, 0, $length-3) . '...'; + } + // If the current user owns this node (or comment), and they haven't created a + // Displayed Name yet, give them a call-to-action link to edit their profile. + if ($object->uid && $name) { + global $user; + if ($object->uid == $user->uid && $name == variable_get('user_displayed_name_token', '...')) { + $name .= ' ' . l(t('edit name'), 'user/' . $user->uid . '/edit'); + $output = $name; + } + elseif (user_access('access user profiles')) { $output = l($name, 'user/' . $object->uid, array('attributes' => array('title' => t('View user profile.')))); } else { $output = check_plain($name); } } - else if ($object->name) { + elseif ($name) { // Sometimes modules display content composed by people who are // not registered members of the site (e.g. mailing list or news // aggregator modules). This clause enables modules to display // the true author of the content. if (!empty($object->homepage)) { - $output = l($object->name, $object->homepage, array('attributes' => array('rel' => 'nofollow'))); + $output = l($name, $object->homepage, array('attributes' => array('rel' => 'nofollow'))); } else { - $output = check_plain($object->name); + $output = check_plain($name); } $output .= ' (' . t('not verified') . ')'; Index: modules/node/node.module =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.module,v retrieving revision 1.961 diff -u -r1.961 node.module --- modules/node/node.module 6 May 2008 12:18:48 -0000 1.961 +++ modules/node/node.module 9 May 2008 16:50:47 -0000 @@ -2472,9 +2472,15 @@ * @ingroup themeable */ function theme_node_submitted($node) { - return t('Submitted by !username on @datetime', + if (variable_get('user_displayed_name', 0)) { + $displayname = TRUE; + } + else { + $displayname = FALSE; + } + return t('Submitted by !name on @datetime', array( - '!username' => theme('username', $node), + '!name' => theme('username', $node, $displayname), '@datetime' => format_date($node->created), )); } Index: modules/user/user.module =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.module,v retrieving revision 1.907 diff -u -r1.907 user.module --- modules/user/user.module 7 May 2008 19:34:24 -0000 1.907 +++ modules/user/user.module 9 May 2008 16:50:47 -0000 @@ -7,6 +7,7 @@ */ define('USERNAME_MAX_LENGTH', 60); +define('DISPLAYED_NAME_MAX_LENGTH', 60); // TODO: Make this an admin-setting. define('EMAIL_MAX_LENGTH', 64); /** @@ -350,8 +351,8 @@ */ function user_validate_name($name) { if (!strlen($name)) return t('You must enter a username.'); - if (substr($name, 0, 1) == ' ') return t('The username cannot begin with a space.'); - if (substr($name, -1) == ' ') return t('The username cannot end with a space.'); + if (substr($name, 0, 1) == ' ') return t('The username cannot begin with a space.'); + if (substr($name, -1) == ' ') return t('The username cannot end with a space.'); if (strpos($name, ' ') !== FALSE) return t('The username cannot contain multiple spaces in a row.'); if (ereg("[^\x80-\xF7 [:alnum:]@_.-]", $name)) return t('The username contains an illegal character.'); if (preg_match('/[\x{80}-\x{A0}' . // Non-printable ISO-8859-1 + NBSP @@ -370,6 +371,26 @@ if (strlen($name) > USERNAME_MAX_LENGTH) return t('The username %name is too long: it must be %max characters or less.', array('%name' => $name, '%max' => USERNAME_MAX_LENGTH)); } +function user_validate_displayed_name($displayed_name) { + // TODO: This first line of validation is only needed if we decide to go #required. + //if (!strlen($displayed_name)) return t('You must choose a Displayed Name. It can be the same as your username, but it cannot be blank.'); + if (substr($displayed_name, 0, 1) == ' ') return t('Your Displayed Name cannot begin with a space.'); + if (substr($displayed_name, -1) == ' ') return t('Your Displayed Name cannot end with a space.'); + if (strpos($displayed_name, ' ') !== FALSE) return t('The Displayed Name field is not allowed to have multiple spaces between words.'); + if (preg_match('/[\x{80}-\x{A0}' . // Non-printable ISO-8859-1 + NBSP + '\x{AD}' . // Soft-hyphen + '\x{2028}-\x{202F}' . // Bidirectional text overrides + '\x{205F}-\x{206F}' . // Various text hinting characters + '\x{FEFF}' . // Byte order mark + '\x{FF01}-\x{FF60}' . // Full-width latin + '\x{FFF9}-\x{FFFD}' . // Replacement characters + '\x{0}]/u', // NULL byte + $displayed_name)) { + return t('Your Displayed Name contains an illegal character.'); + } + if (strlen($displayed_name) > DISPLAYED_NAME_MAX_LENGTH) return t('The Displayed Name you entered, "%displayed_name", is too long: it must be %max characters or less.', array('%displayed_name' => $displayed_name, '%max' => DISPLAYED_NAME_MAX_LENGTH)); +} + function user_validate_mail($mail) { if (!$mail) { return t('You must enter an e-mail address.'); @@ -552,12 +573,23 @@ * Implementation of hook_perm(). */ function user_perm() { - return array( - 'administer permissions' => t('Manage the permissions assigned to user roles. %warning', array('%warning' => t('Warning: Give to trusted roles only; this permission has security implications.'))), - 'administer users' => t('Manage or block users, and manage their role assignments.'), - 'access user profiles' => t('View profiles of users on the site, which may contain personal information.'), - 'change own username' => t('Select a different username.'), - ); + $perms = array( + 'administer permissions' => t('Manage the permissions assigned to user roles. %warning', array('%warning' => t('Warning: Give to trusted roles only; this permission has security implications.'))), + 'administer users' => t('Manage or block users, and manage their role assignments.'), + 'access user profiles' => t('View profiles of users on the site, which may contain personal information.'), + 'change own username' => t('Select a different username.'), + ); + // Add "Change own Displayed Name" settings, but only if the admin has enabled that feature. + if (variable_get('user_displayed_name', 0) && variable_get('user_displayed_name_editable', 0)) { + $perms['change own displayed name'] = t('Allow users to modify their "real name".'); + } + else { + // Uncheck all boxes in the "Change own displayed name" permission row. + // TODO: This db query probably shouldn't be here, but become it's own function. + db_query("DELETE FROM {role_permission} WHERE permission = 'change own displayed name'"); + } + + return $perms; } /** @@ -1136,7 +1168,12 @@ if ($account->uid == $GLOBALS['user']->uid) { return t('My account'); } - return $account->name; + else if (variable_get('user_displayed_name', 0)) { + return $account->displayed_name; + } + else { + return $account->name; + } } /** @@ -1398,6 +1435,35 @@ '#required' => TRUE, ); } + // Figure out whether it's an anonymous user who's registering, a user who's + // editing their own profile, or an admin who's creating new users by hand. + if (variable_get('user_displayed_name', 0) && $uid = 1) { + $sitecreator = TRUE; + } + if (variable_get('user_displayed_name_editable', 0) && (user_access('change own displayed name') || $register || $admin || $sitecreator)) { + $unique = variable_get('user_displayed_name_unique', 0); + // Get the value that was stored in this field from last time... + if ($edit['displayed_name']) { + $default_displayname = $edit['displayed_name']; + } + // If the user hasn't saved a Display Name before, default to their username, + // but make sure this isn't a site admin who's editing other users. + elseif (!$admin && $uid > 0) { + global $user; + $default_displayname = $user->name; + } + else { + $default_displayname = ''; + } + $form['account']['displayed_name'] = array( + '#type' => 'textfield', + '#title' => t('Displayed name'), + '#default_value' => $default_displayname, + '#maxlength' => DISPLAYED_NAME_MAX_LENGTH, + '#description' => theme_displayed_name_field_description($unique), // TODO: Figure out why this theme function doesn't work when called as theme('displayed_name_field_description'). + '#required' => FALSE, + ); + } $form['account']['mail'] = array('#type' => 'textfield', '#title' => t('E-mail address'), '#default_value' => $edit['mail'], @@ -1501,6 +1567,16 @@ } } + // Validate the displayed_name. + if (isset($edit['account']['displayed_name']) && ((user_access('change own displayed name') || user_access('administer users') || !$user->uid))) { + if ($error = user_validate_displayed_name($edit['account']['displayed_name'])) { + form_set_error('displayed_name', $error); + } + elseif (variable_get('user_displayed_name_unique', 0) && db_result(db_query("SELECT COUNT(*) FROM {users} WHERE uid != %d AND LOWER(name) = LOWER('%s')", $uid, $edit['displayed_name'])) > 0) { + form_set_error('displayed_name', t('%displayname is already taken by another user. Please choose another.', array('%displayname' => $edit['displayed_name']))); + } + } + // Validate the e-mail address: if ($error = user_validate_mail($edit['mail'])) { form_set_error('mail', $error); @@ -2299,6 +2375,9 @@ /** * Form builder; The user registration form. * + * This form can be used by a first-time user who's registering, + * or by an admin to create users manually. + * * @ingroup forms * @see user_register_validate() * @see user_register_submit() @@ -2322,23 +2401,24 @@ // Merge in the default user edit fields. $form = array_merge($form, user_edit_form($form_state, NULL, NULL, TRUE)); + // Add a 'Notify User' checkbox if a site admin is creating new users manually. if ($admin) { $form['account']['notify'] = array( '#type' => 'checkbox', '#title' => t('Notify user of new account') ); + // Show the Displayed Name field to admins who are creating new user accounts. // Redirect back to page which initiated the create request; // usually admin/user/user/create. $form['destination'] = array('#type' => 'hidden', '#value' => $_GET['q']); } - // Create a dummy variable for pass-by-reference parameters. $null = NULL; $extra = _user_forms($null, NULL, NULL, 'register'); // Remove form_group around default fields if there are no other groups. if (!$extra) { - foreach (array('name', 'mail', 'pass', 'status', 'roles', 'notify') as $key) { + foreach (array('name', 'displayed_name', 'mail', 'pass', 'status', 'roles', 'notify') as $key) { if (isset($form['account'][$key])) { $form[$key] = $form['account'][$key]; } @@ -2357,7 +2437,6 @@ '#default_value' => variable_get('date_default_timezone', NULL), '#id' => 'edit-user-register-timezone', ); - // Add the JavaScript callback to automatically set the timezone. drupal_add_js(' // Global Killswitch @@ -2392,3 +2471,19 @@ return empty($groups) ? FALSE : $groups; } + +/** + * THEME FUNCTIONS + */ +// When the user is editing their own account, this generates the descriptive +// text for the Displayed Name field. +function theme_displayed_name_field_description($unique = FALSE) { + $output = 'This is your real name, as you would like it to appear to others. You may keep it the same as your username, or change it'; + if ($unique) { + $output .= ', but it must be unique within this site'; + } + $output .= '.'; + return t($output); +} + +// Do not place any code below these theme functions. It makes them easy for themers to find. \ No newline at end of file Index: modules/user/user.js =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.js,v retrieving revision 1.6 diff -u -r1.6 user.js --- modules/user/user.js 12 Sep 2007 18:29:32 -0000 1.6 +++ modules/user/user.js 9 May 2008 16:50:47 -0000 @@ -177,12 +177,28 @@ /** * On the admin/user/settings page, conditionally show all of the - * picture-related form elements depending on the current value of the - * "Picture support" radio buttons. + * related form elements depending on the current value of each + * fieldset's disabled/enabled radio buttons. (Use a separate declaration for + * each desired form element's toggling action.) */ Drupal.behaviors.userSettings = function (context) { $('div.user-admin-picture-radios input[type=radio]:not(.userSettings-processed)', context).addClass('userSettings-processed').click(function () { $('div.user-admin-picture-settings', context)[['hide', 'show'][this.value]](); }); + + $('div.user-admin-displayname-radios input[type=radio]:not(.userSettings-processed)', context).addClass('userSettings-processed').click(function () { + $('div.user-admin-displayname-settings', context)[['hide', 'show'][this.value]](); + }); + + // Disable the Enforce Unique checkbox if the Allow Users To Edit checkbox + // defaults to unchecked. + if (!$('input#edit-user-displayed-name-editable').attr('checked')) { + $('input#edit-user-displayed-name-unique').attr("disabled", "disabled"); + } + // Re-enable the Enforce Unique checkbox. + $('input#edit-user-displayed-name-editable').click(function() { + $('input#edit-user-displayed-name-unique').removeAttr("disabled"); + }); + + }; - Index: modules/user/user.pages.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.pages.inc,v retrieving revision 1.13 diff -u -r1.13 user.pages.inc --- modules/user/user.pages.inc 14 Apr 2008 17:48:43 -0000 1.13 +++ modules/user/user.pages.inc 9 May 2008 16:50:47 -0000 @@ -147,7 +147,12 @@ * Menu callback; Displays a user or user profile page. */ function user_view($account) { - drupal_set_title(check_plain($account->name)); + if (variable_get('user_displayed_name', 0) && $account->displayed_name) { + drupal_set_title(check_plain($account->displayed_name)); + } + else { + drupal_set_title(check_plain($account->name)); + } // Retrieve all profile fields and attach to $account->content. user_build_content($account); Index: modules/user/user.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.admin.inc,v retrieving revision 1.22 diff -u -r1.22 user.admin.inc --- modules/user/user.admin.inc 7 May 2008 19:34:24 -0000 1.22 +++ modules/user/user.admin.inc 9 May 2008 16:50:47 -0000 @@ -132,6 +132,7 @@ $header = array( array(), array('data' => t('Username'), 'field' => 'u.name'), + array('data' => t('Displayed Name'), 'field' => 'u.displayed_name'), array('data' => t('Status'), 'field' => 'u.status'), t('Roles'), array('data' => t('Member for'), 'field' => 'u.created', 'sort' => 'desc'), @@ -139,7 +140,7 @@ t('Operations') ); - $sql = 'SELECT DISTINCT u.uid, u.name, u.status, u.created, u.access FROM {users} u LEFT JOIN {users_roles} ur ON u.uid = ur.uid ' . $filter['join'] . ' WHERE u.uid != 0 ' . $filter['where']; + $sql = 'SELECT DISTINCT u.uid, u.name, u.displayed_name, u.status, u.created, u.access FROM {users} u LEFT JOIN {users_roles} ur ON u.uid = ur.uid ' . $filter['join'] . ' WHERE u.uid != 0 ' . $filter['where']; $sql .= tablesort_sql($header); $query_count = 'SELECT COUNT(DISTINCT u.uid) FROM {users} u LEFT JOIN {users_roles} ur ON u.uid = ur.uid ' . $filter['join'] . ' WHERE u.uid != 0 ' . $filter['where']; $result = pager_query($sql, 50, 0, $query_count, $filter['args']); @@ -400,6 +401,56 @@ '#rows' => 3, ); + // Add some jQuery for the show & hide functionality of these next elements. + drupal_add_js(drupal_get_path('module', 'user') . '/user.js'); + + // User's displayed name. + $displayed_name_support = variable_get('user_displayed_name', 0); + $form['displayname'] = array( + '#type' => 'fieldset', + '#title' => t('Displayed name'), + ); + $form['displayname']['user_displayed_name'] = array( + '#type' => 'radios', + '#title' => t('Use displayed names instead of usernames'), + '#default_value' => variable_get('user_displayed_name', 0), + '#options' => array(t('Disabled'), t('Enabled')), + '#description' => t("Turning this option on will begin showing the user's 'real name', or 'displayed name', rather than the user's login name."), + '#prefix' => '
', + '#suffix' => '
', + ); + // If JS is enabled, and the radio is defaulting to off, hide all + // the settings on page load via .css using the js-hide class so + // that there's no flicker. + $css_class = 'user-admin-displayname-settings'; + if (!$displayed_name_support) { + $css_class .= ' js-hide'; + } + $form['displayname']['settings'] = array( + '#prefix' => '
', + '#suffix' => '
', + ); + $form['displayname']['settings']['user_displayed_name_token'] = array( + '#type' => 'textfield', + '#title' => t('Default name'), + '#default_value' => variable_get('user_displayed_name_token', 'Name withheld'), + '#description' => t('Default name to display if users have not chosen a custom Displayed Name.'), + '#size' => 20, + '#maxlength' => 20, + ); + $form['displayname']['settings']['user_displayed_name_editable'] = array( + '#type' => 'checkbox', + '#title' => t('Allow users to change their own name'), + '#default_value' => variable_get('user_displayed_name_editable', 0), + '#description' => t('Allow all users to create and edit their Displayed Name. When this option is on, fine-grained settings can be found on the !permissions page.', array('!permissions' => l(t('User Permissions'), 'admin/user/permissions', array('fragment' => 'module-user')))), + ); + $form['displayname']['settings']['user_displayed_name_unique'] = array( + '#type' => 'checkbox', + '#title' => t('Enforce unique displayed name'), + '#default_value' => variable_get('user_displayed_name_unique', 0), + '#description' => t('Force users to choose a one-of-a-kind nickname, or "real name" within this site. Requires the above setting, "Allow users to change their own name", to be enabled.'), + ); + // User signatures. $form['signatures'] = array( '#type' => 'fieldset', @@ -418,6 +469,7 @@ file_check_directory($picture_path, 1, 'user_picture_path'); } + // User pictures. $form['pictures'] = array( '#type' => 'fieldset', '#title' => t('Pictures'), @@ -431,7 +483,6 @@ '#prefix' => '
', '#suffix' => '
', ); - drupal_add_js(drupal_get_path('module', 'user') . '/user.js'); // If JS is enabled, and the radio is defaulting to off, hide all // the settings on page load via .css using the js-hide class so // that there's no flicker. Index: modules/user/user.install =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.install,v retrieving revision 1.12 diff -u -r1.12 user.install --- modules/user/user.install 7 May 2008 19:34:24 -0000 1.12 +++ modules/user/user.install 9 May 2008 16:50:47 -0000 @@ -103,6 +103,13 @@ 'default' => '', 'description' => t('Unique user name.'), ), + 'displayed_name' => array( + 'type' => 'varchar', + 'length' => 60, + 'not null' => FALSE, + 'default' => '', + 'description' => t('Real Name of user. Unique or non-unique is admin selectable.') + ), 'pass' => array( 'type' => 'varchar', 'length' => 128, @@ -296,3 +303,18 @@ * The next series of updates should start at 8000. */ +/** + * Implementation of hook_update. + * + * Add a field called "displayed_name" to the Users table. Re: issue 102679. + */ +function user_update_8000() { + $ret = array(); + db_add_field($ret, 'users', 'displayed_name', array('type' => 'varchar', + 'length' => 60, + 'not null' => FALSE, + 'default' => '', + 'description' => t('Real Name of user. Unique or non-unique is admin selectable.'), + )); + return $ret; +} Index: modules/comment/comment.module =================================================================== RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v retrieving revision 1.631 diff -u -r1.631 comment.module --- modules/comment/comment.module 6 May 2008 12:18:47 -0000 1.631 +++ modules/comment/comment.module 9 May 2008 16:50:47 -0000 @@ -1715,9 +1715,15 @@ * @ingroup themeable */ function theme_comment_submitted($comment) { - return t('Submitted by !username on @datetime.', + if (variable_get('user_displayed_name', 0)) { + $displayname = TRUE; + } + else { + $displayname = FALSE; + } + return t('Submitted by !name on @datetime.', array( - '!username' => theme('username', $comment), + '!name' => theme('username', $comment, $displayname), '@datetime' => format_date($comment->timestamp) )); } Index: themes/garland/template.php =================================================================== RCS file: /cvs/drupal/drupal/themes/garland/template.php,v retrieving revision 1.18 diff -u -r1.18 template.php --- themes/garland/template.php 28 Apr 2008 09:25:27 -0000 1.18 +++ themes/garland/template.php 9 May 2008 16:50:47 -0000 @@ -67,9 +67,9 @@ * Format the "Submitted by username on date/time" for each comment. */ function phptemplate_comment_submitted($comment) { - return t('!datetime — !username', + return t('!datetime — !name', array( - '!username' => theme('username', $comment), + '!name' => theme('username', $comment, TRUE, FALSE), '!datetime' => format_date($comment->timestamp) )); } @@ -78,9 +78,9 @@ * Format the "Submitted by username on date/time" for each node. */ function garland_node_submitted($node) { - return t('!datetime — !username', + return t('!datetime — !name', array( - '!username' => theme('username', $node), + '!name' => theme('username', $node, TRUE, FALSE), '!datetime' => format_date($node->created), )); } Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.602 diff -u -r1.602 system.module --- modules/system/system.module 7 May 2008 19:17:50 -0000 1.602 +++ modules/system/system.module 9 May 2008 16:50:47 -0000 @@ -1200,6 +1200,21 @@ if ($op == t('Reset to defaults')) { drupal_set_message(t('The configuration options have been reset to their default values.')); } + // Set the "change own displayed name" permission checkbox for logged-in users. + else if ($form['displayname']['user_displayed_name']['#value'] && $form['displayname']['settings']['user_displayed_name_editable']['#value']) { + // Make sure the row doen't already exist, as in the case of a user submitting this same form again with differing settings. + $query = db_fetch_object(db_query("SELECT rid FROM {role_permission} WHERE permission = 'change own displayed name'")); + if (!$query) { + db_query("INSERT INTO {role_permission} (rid, permission) VALUES (%d, '%s')", 2, 'change own displayed name'); + } + drupal_set_message(t('The configuration options have been saved.')); + drupal_set_message(t('Since you enabled the "allow users to edit their own Displayed Name" option, + please take the time to confirm that your !permissions settings are also correct.', array('!permissions' => l(t('User Permissions'), 'admin/user/permissions', array('fragment' => 'module-user'))))); + } + else if (!$form['displayname']['settings']['user_displayed_name_editable']['#value']) { + db_query("DELETE FROM {role_permission} WHERE permission = 'change own displayed name'"); + drupal_set_message(t('The configuration options have been saved.')); + } else { drupal_set_message(t('The configuration options have been saved.')); }