Merge "Add display_name account property."

This commit is contained in:
Ben Rohlfs 2020-02-13 10:01:34 +00:00 committed by Gerrit Code Review
commit 329677cfc6
14 changed files with 260 additions and 22 deletions

View File

@ -7,10 +7,10 @@ Starting from 2.15 Gerrit accounts are fully stored in
link:note-db.html[NoteDb].
The account data consists of a sequence number (account ID), account
properties (full name, preferred email, registration date, status,
inactive flag), preferences (general, diff and edit preferences),
project watches, SSH keys, external IDs, starred changes and reviewed
flags.
properties (full name, display name, preferred email, registration
date, status, inactive flag), preferences (general, diff and edit
preferences), project watches, SSH keys, external IDs, starred changes
and reviewed flags.
Most account data is stored in a special link:#all-users[All-Users]
repository, which has one branch per user. Within the user branch there
@ -155,6 +155,7 @@ The account properties are stored in the user branch in the
----
[account]
fullName = John Doe
displayName = John
preferredEmail = john.doe@example.com
status = OOO
active = false

View File

@ -58,8 +58,8 @@ generally disabled by default. Optional fields are:
[[details]]
--
* `DETAILS`: Includes full name, preferred email, username, avatars,
status and state for each account.
* `DETAILS`: Includes full name, preferred email, username, display
name, avatars, status and state for each account.
--
[[all-emails]]
@ -99,12 +99,14 @@ capability.
"name": "John Doe",
"email": "john.doe@example.com",
"username": "john"
"display_name": "John D"
},
{
"_account_id": 1001439,
"name": "John Smith",
"email": "john.smith@example.com",
"username": "jsmith"
"display_name": "Johnny"
},
]
----
@ -134,6 +136,7 @@ Returns an account as an link:#account-info[AccountInfo] entity.
"name": "John Doe",
"email": "john.doe@example.com",
"username": "john"
"display_name": "Super John"
}
----
@ -155,6 +158,7 @@ link:#account-input[AccountInput].
{
"name": "John Doe",
"display_name": "Super John",
"email": "john.doe@example.com",
"ssh_key": "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw==",
"http_password": "19D9aIn7zePb",
@ -208,6 +212,7 @@ AccountDetailInfo] entity.
"name": "John Doe",
"email": "john.doe@example.com",
"username": "john"
"display_name": "Super John"
}
----
@ -401,6 +406,27 @@ fails with "`405 Method Not Allowed`".
As response the new username is returned.
[[set-display-name]]
=== Set Display Name
--
'PUT /accounts/link:#account-id[\{account-id\}]/displayname'
--
The new display name must be provided in the request body inside
a link:#display-name-input[DisplayNameInput] entity.
.Request
----
PUT /accounts/self/displayname HTTP/1.0
Content-Type: application/json; charset=UTF-8
{
"display_name": "John"
}
----
As response the new display name is returned.
[[get-active]]
=== Get Active
--
@ -2228,6 +2254,11 @@ Only set if detailed account information is requested. +
See option link:rest-api-changes.html#detailed-accounts[
DETAILED_ACCOUNTS] for change queries +
and option link:#details[DETAILS] for account queries.
|`display_name` |optional|The display name of the user. +
Only set if detailed account information is requested. +
See option link:rest-api-changes.html#detailed-accounts[
DETAILED_ACCOUNTS] for change queries +
and option link:#details[DETAILS] for account queries.
|`email` |optional|
The email address the user prefers to be contacted through. +
Only set if detailed account information is requested. +
@ -2268,6 +2299,7 @@ a new account.
|`username` |optional|
The user name. If provided, must match the user name from the URL.
|`name` |optional|The full name of the user.
|`display_name` |optional|The display name of the user.
|`email` |optional|The email address of the user.
|`ssh_key` |optional|The public SSH key of the user.
|`http_password`|optional|The HTTP password of the user.
@ -2870,6 +2902,17 @@ username for an account.
|`username` |The new username of the account.
|=======================
[[display-name-input]]
=== DisplayNameInput
The `DisplayNameInput` entity contains information for setting the
display name for an account.
[options="header",cols="1,6"]
|=======================
|Field Name |Description
|`display_name` |The new display name of the account.
|=======================
[[project-watch-info]]
=== ProjectWatchInfo
The `WatchedProjectsInfo` entity contains information about a project watch

View File

@ -129,6 +129,10 @@ public abstract class Account {
@Nullable
public abstract String fullName();
/** Optional display name of the user to be shown in the UI. */
@Nullable
public abstract String displayName();
/** Email address the user prefers to be contacted through. */
@Nullable
public abstract String preferredEmail();
@ -235,6 +239,11 @@ public abstract class Account {
public abstract Builder setFullName(String fullName);
@Nullable
public abstract String displayName();
public abstract Builder setDisplayName(String displayName);
@Nullable
public abstract String preferredEmail();

View File

@ -87,6 +87,8 @@ public interface AccountApi {
void setStatus(String status) throws RestApiException;
void setDisplayName(String displayName) throws RestApiException;
List<SshKeyInfo> listSshKeys() throws RestApiException;
SshKeyInfo addSshKey(String key) throws RestApiException;
@ -268,6 +270,11 @@ public interface AccountApi {
throw new NotImplementedException();
}
@Override
public void setDisplayName(String displayName) throws RestApiException {
throw new NotImplementedException();
}
@Override
public List<SshKeyInfo> listSshKeys() throws RestApiException {
throw new NotImplementedException();

View File

@ -20,6 +20,7 @@ import java.util.List;
public class AccountInput {
@DefaultInput public String username;
public String name;
public String displayName;
public String email;
public String sshKey;
public String httpPassword;

View File

@ -0,0 +1,27 @@
// Copyright (C) 2020 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.extensions.api.accounts;
import com.google.gerrit.extensions.restapi.DefaultInput;
public class DisplayNameInput {
public @DefaultInput String displayName;
public DisplayNameInput(String displayName) {
this.displayName = displayName;
}
public DisplayNameInput() {}
}

View File

@ -33,6 +33,14 @@ public class AccountInfo {
/** The full name of the user. */
public String name;
/**
* The display name of the user. This allows users to control how their name is displayed in the
* UI. It will likely be unset for most users. Host admins will just choose a default (full name,
* user name, first name, ...) for all users, and this account property is just a way to opt out
* of the host wide default strategy of choosing the display name.
*/
public String displayName;
/** The preferred email address of the user. */
public String email;
@ -73,6 +81,7 @@ public class AccountInfo {
AccountInfo accountInfo = (AccountInfo) o;
return Objects.equals(_accountId, accountInfo._accountId)
&& Objects.equals(name, accountInfo.name)
&& Objects.equals(displayName, accountInfo.displayName)
&& Objects.equals(email, accountInfo.email)
&& Objects.equals(secondaryEmails, accountInfo.secondaryEmails)
&& Objects.equals(username, accountInfo.username)
@ -88,6 +97,7 @@ public class AccountInfo {
return MoreObjects.toStringHelper(this)
.add("id", _accountId)
.add("name", name)
.add("displayname", displayName)
.add("email", email)
.add("username", username)
.toString();
@ -96,7 +106,15 @@ public class AccountInfo {
@Override
public int hashCode() {
return Objects.hash(
_accountId, name, email, secondaryEmails, username, avatars, _moreAccounts, status);
_accountId,
name,
displayName,
email,
secondaryEmails,
username,
avatars,
_moreAccounts,
status);
}
protected AccountInfo() {}

View File

@ -190,6 +190,7 @@ public class AccountConfig extends VersionedMetaData implements ValidationError.
InternalAccountUpdate.builder()
.setActive(account.isActive())
.setFullName(account.fullName())
.setDisplayName(account.displayName())
.setPreferredEmail(account.preferredEmail())
.setStatus(account.status())
.build());

View File

@ -33,6 +33,7 @@ import org.eclipse.jgit.lib.ObjectId;
* [account]
* active = false
* fullName = John Doe
* displayName = John
* preferredEmail = john.doe@foo.com
* status = Overloaded with reviews
* </pre>
@ -51,6 +52,7 @@ public class AccountProperties {
public static final String ACCOUNT = "account";
public static final String KEY_ACTIVE = "active";
public static final String KEY_FULL_NAME = "fullName";
public static final String KEY_DISPLAY_NAME = "displayName";
public static final String KEY_PREFERRED_EMAIL = "preferredEmail";
public static final String KEY_STATUS = "status";
@ -91,6 +93,7 @@ public class AccountProperties {
Account.Builder accountBuilder = Account.builder(accountId, registeredOn);
accountBuilder.setActive(accountConfig.getBoolean(ACCOUNT, null, KEY_ACTIVE, true));
accountBuilder.setFullName(get(accountConfig, KEY_FULL_NAME));
accountBuilder.setDisplayName(get(accountConfig, KEY_DISPLAY_NAME));
String preferredEmail = get(accountConfig, KEY_PREFERRED_EMAIL);
accountBuilder.setPreferredEmail(preferredEmail);
@ -108,6 +111,9 @@ public class AccountProperties {
public static void writeToAccountConfig(InternalAccountUpdate accountUpdate, Config cfg) {
accountUpdate.getActive().ifPresent(active -> setActive(cfg, active));
accountUpdate.getFullName().ifPresent(fullName -> set(cfg, KEY_FULL_NAME, fullName));
accountUpdate
.getDisplayName()
.ifPresent(displayName -> set(cfg, KEY_DISPLAY_NAME, displayName));
accountUpdate
.getPreferredEmail()
.ifPresent(preferredEmail -> set(cfg, KEY_PREFERRED_EMAIL, preferredEmail));

View File

@ -83,14 +83,14 @@ import org.eclipse.jgit.lib.Repository;
*
* <p>The account updates are written to NoteDb. In NoteDb accounts are represented as user branches
* in the {@code All-Users} repository. Optionally a user branch can contain a 'account.config' file
* that stores account properties, such as full name, preferred email, status and the active flag.
* The timestamp of the first commit on a user branch denotes the registration date. The initial
* commit on the user branch may be empty (since having an 'account.config' is optional). See {@link
* AccountConfig} for details of the 'account.config' file format. In addition the user branch can
* contain a 'preferences.config' config file to store preferences (see {@link StoredPreferences})
* and a 'watch.config' config file to store project watches (see {@link ProjectWatches}). External
* IDs are stored separately in the {@code refs/meta/external-ids} notes branch (see {@link
* ExternalIdNotes}).
* that stores account properties, such as full name, display name, preferred email, status and the
* active flag. The timestamp of the first commit on a user branch denotes the registration date.
* The initial commit on the user branch may be empty (since having an 'account.config' is
* optional). See {@link AccountConfig} for details of the 'account.config' file format. In addition
* the user branch can contain a 'preferences.config' config file to store preferences (see {@link
* StoredPreferences}) and a 'watch.config' config file to store project watches (see {@link
* ProjectWatches}). External IDs are stored separately in the {@code refs/meta/external-ids} notes
* branch (see {@link ExternalIdNotes}).
*
* <p>On updating an account the account is evicted from the account cache and reindexed. The
* eviction from the account cache and the reindexing is done by the {@link ReindexAfterRefUpdate}

View File

@ -18,6 +18,7 @@ import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.client.EditPreferencesInfo;
@ -60,6 +61,15 @@ public abstract class InternalAccountUpdate {
*/
public abstract Optional<String> getFullName();
/**
* Returns the new value for the display name.
*
* @return the new value for the display name, {@code Optional#empty()} if the display name is not
* being updated, {@code Optional#of("")} if the display name is unset, the wrapped value is
* never {@code null}
*/
public abstract Optional<String> getDisplayName();
/**
* Returns the new value for the preferred email.
*
@ -166,25 +176,30 @@ public abstract class InternalAccountUpdate {
* Sets a new full name for the account.
*
* @param fullName the new full name, if {@code null} or empty string the full name is unset
* @return the builder
*/
public abstract Builder setFullName(String fullName);
public abstract Builder setFullName(@Nullable String fullName);
/**
* Sets a new display name for the account.
*
* @param displayName the new display name, if {@code null} or empty string the display name is
* unset
*/
public abstract Builder setDisplayName(@Nullable String displayName);
/**
* Sets a new preferred email for the account.
*
* @param preferredEmail the new preferred email, if {@code null} or empty string the preferred
* email is unset
* @return the builder
*/
public abstract Builder setPreferredEmail(String preferredEmail);
public abstract Builder setPreferredEmail(@Nullable String preferredEmail);
/**
* Sets the active flag for the account.
*
* @param active {@code true} if the account should be set to active, {@code false} if the
* account should be set to inactive
* @return the builder
*/
public abstract Builder setActive(boolean active);
@ -192,9 +207,8 @@ public abstract class InternalAccountUpdate {
* Sets a new status for the account.
*
* @param status the new status, if {@code null} or empty string the status is unset
* @return the builder
*/
public abstract Builder setStatus(String status);
public abstract Builder setStatus(@Nullable String status);
/**
* Returns a builder for the set of created external IDs.
@ -486,6 +500,12 @@ public abstract class InternalAccountUpdate {
return this;
}
@Override
public Builder setDisplayName(String displayName) {
delegate.setDisplayName(Strings.nullToEmpty(displayName));
return this;
}
@Override
public Builder setPreferredEmail(String preferredEmail) {
delegate.setPreferredEmail(Strings.nullToEmpty(preferredEmail));

View File

@ -22,6 +22,7 @@ import com.google.gerrit.extensions.api.accounts.AccountApi;
import com.google.gerrit.extensions.api.accounts.AgreementInput;
import com.google.gerrit.extensions.api.accounts.DeleteDraftCommentsInput;
import com.google.gerrit.extensions.api.accounts.DeletedDraftCommentInfo;
import com.google.gerrit.extensions.api.accounts.DisplayNameInput;
import com.google.gerrit.extensions.api.accounts.EmailApi;
import com.google.gerrit.extensions.api.accounts.EmailInput;
import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
@ -76,6 +77,7 @@ import com.google.gerrit.server.restapi.account.Index;
import com.google.gerrit.server.restapi.account.PostWatchedProjects;
import com.google.gerrit.server.restapi.account.PutActive;
import com.google.gerrit.server.restapi.account.PutAgreement;
import com.google.gerrit.server.restapi.account.PutDisplayName;
import com.google.gerrit.server.restapi.account.PutHttpPassword;
import com.google.gerrit.server.restapi.account.PutName;
import com.google.gerrit.server.restapi.account.PutStatus;
@ -134,6 +136,7 @@ public class AccountApiImpl implements AccountApi {
private final DeleteExternalIds deleteExternalIds;
private final DeleteDraftComments deleteDraftComments;
private final PutStatus putStatus;
private final PutDisplayName putDisplayName;
private final GetGroups getGroups;
private final EmailApiImpl.Factory emailApi;
private final PutName putName;
@ -177,6 +180,7 @@ public class AccountApiImpl implements AccountApi {
DeleteExternalIds deleteExternalIds,
DeleteDraftComments deleteDraftComments,
PutStatus putStatus,
PutDisplayName putDisplayName,
GetGroups getGroups,
EmailApiImpl.Factory emailApi,
PutName putName,
@ -219,6 +223,7 @@ public class AccountApiImpl implements AccountApi {
this.deleteExternalIds = deleteExternalIds;
this.deleteDraftComments = deleteDraftComments;
this.putStatus = putStatus;
this.putDisplayName = putDisplayName;
this.getGroups = getGroups;
this.emailApi = emailApi;
this.putName = putName;
@ -476,6 +481,16 @@ public class AccountApiImpl implements AccountApi {
}
}
@Override
public void setDisplayName(String displayName) throws RestApiException {
DisplayNameInput in = new DisplayNameInput(displayName);
try {
putDisplayName.apply(account, in);
} catch (Exception e) {
throw asRestApiException("Cannot set display name", e);
}
}
@Override
public List<SshKeyInfo> listSshKeys() throws RestApiException {
try {

View File

@ -53,6 +53,7 @@ public class Module extends RestApiModule {
delete(ACCOUNT_KIND, "name").to(PutName.class);
get(ACCOUNT_KIND, "status").to(GetStatus.class);
put(ACCOUNT_KIND, "status").to(PutStatus.class);
put(ACCOUNT_KIND, "displayname").to(PutDisplayName.class);
get(ACCOUNT_KIND, "username").to(GetUsername.class);
put(ACCOUNT_KIND, "username").to(PutUsername.class);
get(ACCOUNT_KIND, "active").to(GetActive.class);

View File

@ -0,0 +1,89 @@
// Copyright (C) 2020 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.restapi.account;
import com.google.common.base.Strings;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.api.accounts.DisplayNameInput;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ServerInitiated;
import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import org.eclipse.jgit.errors.ConfigInvalidException;
/**
* REST endpoint to set the display name of an account.
*
* <p>This REST endpoint handles {@code PUT /accounts/<account-identifier>/displayname} requests.
*
* <p>The display name is a free-form text that a user can set for their own account. It defines how
* the user's name will be rendered in the UI in most screens. It is optional, and if not set, then
* the UI falls back to whatever is configured as the default display name, e.g. the full name.
*/
@Singleton
public class PutDisplayName implements RestModifyView<AccountResource, DisplayNameInput> {
private final Provider<CurrentUser> self;
private final PermissionBackend permissionBackend;
private final Provider<AccountsUpdate> accountsUpdateProvider;
@Inject
PutDisplayName(
Provider<CurrentUser> self,
PermissionBackend permissionBackend,
@ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider) {
this.self = self;
this.permissionBackend = permissionBackend;
this.accountsUpdateProvider = accountsUpdateProvider;
}
@Override
public Response<String> apply(AccountResource rsrc, @Nullable DisplayNameInput input)
throws AuthException, ResourceNotFoundException, IOException, PermissionBackendException,
ConfigInvalidException {
IdentifiedUser user = rsrc.getUser();
if (!self.get().hasSameAccountId(user)) {
permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
}
if (input == null) {
input = new DisplayNameInput();
}
String newDisplayName = input.displayName;
AccountState accountState =
accountsUpdateProvider
.get()
.update(
"Set Display Name via API",
user.getAccountId(),
u -> u.setDisplayName(newDisplayName))
.orElseThrow(() -> new ResourceNotFoundException("account not found"));
return Strings.isNullOrEmpty(accountState.account().displayName())
? Response.none()
: Response.ok(accountState.account().displayName());
}
}