PLT-4332 Position field for Users (#4632)

* Add User.Position field to store & model.

* GOFMT

* Add Position to user settings.

* Unit tests.

* Add position to profile popup.

* i18n

* Fix log message for invalid position.

* Add Position field attribute to LDAP config.

* Add Position field attribute to SAML config.

* Reword empty position message.

* Change Position Max Length to 35.

* Better invalid position error message.

* Add new fields to config.json.

* Ensure position is never longer than max when displayed.

* Hard limit of 64 chars with soft limit still 35

* Put field with other attributes.
Этот коммит содержится в:
George Goldberg
2016-12-14 11:11:51 +00:00
коммит произвёл enahum
родитель 9735854503
Коммит 8406e854aa
13 изменённых файлов: 231 добавлений и 1 удалений

Просмотреть файл

@@ -186,6 +186,7 @@
"UsernameAttribute": "",
"NicknameAttribute": "",
"IdAttribute": "",
"PositionAttribute": "",
"SyncIntervalMinutes": 60,
"SkipCertificateVerification": false,
"QueryTimeout": 60,
@@ -218,6 +219,7 @@
"UsernameAttribute": "",
"NicknameAttribute": "",
"LocaleAttribute": "",
"PositionAttribute": "",
"LoginButtonText": "With SAML"
},
"NativeAppSettings": {

Просмотреть файл

@@ -3875,6 +3875,10 @@
"id": "model.user.is_valid.nickname.app_error",
"translation": "Invalid nickname"
},
{
"id": "model.user.is_valid.position.app_error",
"translation": "Invalid position: must not be longer than 35 characters."
},
{
"id": "model.user.is_valid.pwd.app_error",
"translation": "Your password must contain at least {{.Min}} characters."

Просмотреть файл

@@ -251,6 +251,7 @@ type LdapSettings struct {
UsernameAttribute *string
NicknameAttribute *string
IdAttribute *string
PositionAttribute *string
// Syncronization
SyncIntervalMinutes *int
@@ -297,6 +298,7 @@ type SamlSettings struct {
UsernameAttribute *string
NicknameAttribute *string
LocaleAttribute *string
PositionAttribute *string
LoginButtonText *string
}
@@ -690,6 +692,11 @@ func (o *Config) SetDefaults() {
*o.LdapSettings.IdAttribute = ""
}
if o.LdapSettings.PositionAttribute == nil {
o.LdapSettings.PositionAttribute = new(string)
*o.LdapSettings.PositionAttribute = ""
}
if o.LdapSettings.SyncIntervalMinutes == nil {
o.LdapSettings.SyncIntervalMinutes = new(int)
*o.LdapSettings.SyncIntervalMinutes = 60
@@ -911,6 +918,11 @@ func (o *Config) SetDefaults() {
*o.SamlSettings.NicknameAttribute = ""
}
if o.SamlSettings.PositionAttribute == nil {
o.SamlSettings.PositionAttribute = new(string)
*o.SamlSettings.PositionAttribute = ""
}
if o.SamlSettings.LocaleAttribute == nil {
o.SamlSettings.LocaleAttribute = new(string)
*o.SamlSettings.LocaleAttribute = ""

Просмотреть файл

@@ -37,6 +37,7 @@ type User struct {
Nickname string `json:"nickname"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Position string `json:"position"`
Roles string `json:"roles"`
AllowMarketing bool `json:"allow_marketing,omitempty"`
Props StringMap `json:"props,omitempty"`
@@ -78,6 +79,10 @@ func (u *User) IsValid() *AppError {
return NewLocAppError("User.IsValid", "model.user.is_valid.nickname.app_error", nil, "user_id="+u.Id)
}
if utf8.RuneCountInString(u.Position) > 35 {
return NewLocAppError("User.IsValid", "model.user.is_valid.position.app_error", nil, "user_id="+u.Id)
}
if utf8.RuneCountInString(u.FirstName) > 64 {
return NewLocAppError("User.IsValid", "model.user.is_valid.first_name.app_error", nil, "user_id="+u.Id)
}

Просмотреть файл

@@ -128,6 +128,17 @@ func TestUserIsValid(t *testing.T) {
if err := user.IsValid(); err == nil {
t.Fatal(err)
}
user.LastName = ""
user.Position = ""
if err := user.IsValid(); err != nil {
t.Fatal(err)
}
user.Position = strings.Repeat("01234567890", 20)
if err := user.IsValid(); err == nil {
t.Fatal(err)
}
}
func TestUserGetFullName(t *testing.T) {

Просмотреть файл

@@ -222,6 +222,9 @@ func UpgradeDatabaseToVersion36(sqlStore *SqlStore) {
// Create Team Description column
sqlStore.CreateColumnIfNotExists("Teams", "Description", "varchar(255)", "varchar(255)", "")
// Add a Position column to users.
sqlStore.CreateColumnIfNotExists("Users", "Position", "varchar(64)", "varchar(64)", "")
//saveSchemaVersion(sqlStore, VERSION_3_6_0)
//}
}

Просмотреть файл

@@ -66,6 +66,7 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore {
table.ColMap("NotifyProps").SetMaxSize(2000)
table.ColMap("Locale").SetMaxSize(5)
table.ColMap("MfaSecret").SetMaxSize(128)
table.ColMap("Position").SetMaxSize(64)
}
return us

Просмотреть файл

@@ -38,6 +38,7 @@ export default class LdapSettings extends AdminSettings {
config.LdapSettings.NicknameAttribute = this.state.nicknameAttribute;
config.LdapSettings.EmailAttribute = this.state.emailAttribute;
config.LdapSettings.UsernameAttribute = this.state.usernameAttribute;
config.LdapSettings.PositionAttribute = this.state.positionAttribute;
config.LdapSettings.IdAttribute = this.state.idAttribute;
config.LdapSettings.SyncIntervalMinutes = this.parseIntNonZero(this.state.syncIntervalMinutes);
config.LdapSettings.SkipCertificateVerification = this.state.skipCertificateVerification;
@@ -63,6 +64,7 @@ export default class LdapSettings extends AdminSettings {
nicknameAttribute: config.LdapSettings.NicknameAttribute,
emailAttribute: config.LdapSettings.EmailAttribute,
usernameAttribute: config.LdapSettings.UsernameAttribute,
positionAttribute: config.LdapSettings.PositionAttribute,
idAttribute: config.LdapSettings.IdAttribute,
syncIntervalMinutes: config.LdapSettings.SyncIntervalMinutes,
skipCertificateVerification: config.LdapSettings.SkipCertificateVerification,
@@ -299,6 +301,25 @@ export default class LdapSettings extends AdminSettings {
onChange={this.handleChange}
disabled={!this.state.enable}
/>
<TextSetting
id='positionAttribute'
label={
<FormattedMessage
id='admin.ldap.positionAttrTitle'
defaultMessage='Position Attribute:'
/>
}
placeholder={Utils.localizeMessage('admin.ldap.positionAttrEx', 'E.g.: "title"')}
helpText={
<FormattedMessage
id='admin.ldap.positionAttrDesc'
defaultMessage='(Optional) The attribute in the AD/LDAP server that will be used to populate the position field in Mattermost.'
/>
}
value={this.state.positionAttribute}
onChange={this.handleChange}
disabled={!this.state.enable}
/>
<TextSetting
id='emailAttribute'
label={

Просмотреть файл

@@ -41,6 +41,7 @@ export default class SamlSettings extends AdminSettings {
config.SamlSettings.EmailAttribute = this.state.emailAttribute;
config.SamlSettings.UsernameAttribute = this.state.usernameAttribute;
config.SamlSettings.NicknameAttribute = this.state.nicknameAttribute;
config.SamlSettings.PositionAttribute = this.state.positionAttribute;
config.SamlSettings.LocaleAttribute = this.state.localeAttribute;
config.SamlSettings.LoginButtonText = this.state.loginButtonText;
@@ -65,6 +66,7 @@ export default class SamlSettings extends AdminSettings {
emailAttribute: settings.EmailAttribute,
usernameAttribute: settings.UsernameAttribute,
nicknameAttribute: settings.NicknameAttribute,
positionAttribute: settings.PositionAttribute,
localeAttribute: settings.LocaleAttribute,
loginButtonText: settings.LoginButtonText
};
@@ -508,6 +510,25 @@ export default class SamlSettings extends AdminSettings {
onChange={this.handleChange}
disabled={!this.state.enable}
/>
<TextSetting
id='positionAttribute'
label={
<FormattedMessage
id='admin.saml.positionAttrTitle'
defaultMessage='Position Attribute:'
/>
}
placeholder={Utils.localizeMessage('admin.saml.positionAttrEx', 'E.g.: "Role"')}
helpText={
<FormattedMessage
id='admin.saml.positionAttrDesc'
defaultMessage='(Optional) The attribute in the SAML Assertion that will be used to populate the position of users in Mattermost.'
/>
}
value={this.state.positionAttribute}
onChange={this.handleChange}
disabled={!this.state.enable}
/>
<TextSetting
id='localeAttribute'
label={

Просмотреть файл

@@ -182,6 +182,23 @@ export default class UserProfile extends React.Component {
dataContent.push(webrtc);
const position = this.props.user.position.substring(0, Constants.MAX_POSITION_LENGTH);
if (position) {
dataContent.push(
<div
data-toggle='tooltip'
title={position}
key='user-popover-position'
>
<p
className='text-nowrap'
>
{position}
</p>
</div>
);
}
if (global.window.mm_config.ShowEmailAddress === 'true' || UserStore.isSystemAdminForCurrentUser() || this.props.user === UserStore.getCurrentUser()) {
dataContent.push(
<div

Просмотреть файл

@@ -69,6 +69,10 @@ const holders = defineMessages({
close: {
id: 'user.settings.general.close',
defaultMessage: 'Close'
},
position: {
id: 'user.settings.general.position',
defaultMessage: 'Position'
}
});
@@ -85,6 +89,7 @@ class UserSettingsGeneralTab extends React.Component {
this.submitEmail = this.submitEmail.bind(this);
this.submitUser = this.submitUser.bind(this);
this.submitPicture = this.submitPicture.bind(this);
this.submitPosition = this.submitPosition.bind(this);
this.updateUsername = this.updateUsername.bind(this);
this.updateFirstName = this.updateFirstName.bind(this);
@@ -94,6 +99,7 @@ class UserSettingsGeneralTab extends React.Component {
this.updateConfirmEmail = this.updateConfirmEmail.bind(this);
this.updatePicture = this.updatePicture.bind(this);
this.updateSection = this.updateSection.bind(this);
this.updatePosition = this.updatePosition.bind(this);
this.state = this.setupInitialState(props);
}
@@ -249,6 +255,22 @@ class UserSettingsGeneralTab extends React.Component {
);
}
submitPosition(e) {
e.preventDefault();
const user = Object.assign({}, this.props.user);
const position = this.state.position.trim();
if (user.position === position) {
this.updateSection('');
return;
}
user.position = position;
this.submitUser(user, Constants.UserUpdateEvents.Position, false);
}
updateUsername(e) {
this.setState({username: e.target.value});
}
@@ -265,6 +287,10 @@ class UserSettingsGeneralTab extends React.Component {
this.setState({nickname: e.target.value});
}
updatePosition(e) {
this.setState({position: e.target.value});
}
updateEmail(e) {
this.setState({email: e.target.value});
}
@@ -302,6 +328,7 @@ class UserSettingsGeneralTab extends React.Component {
firstName: user.first_name,
lastName: user.last_name,
nickname: user.nickname,
position: user.position,
email: user.email,
confirmEmail: '',
picture: null,
@@ -936,6 +963,99 @@ class UserSettingsGeneralTab extends React.Component {
);
}
let positionSection;
if (this.props.activeSection === 'position') {
let extraInfo;
let submit = null;
if ((this.props.user.auth_service === 'ldap' || this.props.user.auth_service === Constants.SAML_SERVICE) && global.window.mm_config.PositionAttributeSet === 'true') {
extraInfo = (
<span>
<FormattedMessage
id='user.settings.general.field_handled_externally'
defaultMessage='This field is handled through your login provider. If you want to change it, you need to do so though your login provider.'
/>
</span>
);
} else {
let positionLabel = (
<FormattedMessage
id='user.settings.general.position'
defaultMessage='Position'
/>
);
if (Utils.isMobile()) {
positionLabel = '';
}
inputs.push(
<div
key='positionSetting'
className='form-group'
>
<label className='col-sm-5 control-label'>{positionLabel}</label>
<div className='col-sm-7'>
<input
className='form-control'
type='text'
onChange={this.updatePosition}
value={this.state.position}
maxLength={Constants.MAX_POSITION_LENGTH}
autoCapitalize='off'
/>
</div>
</div>
);
extraInfo = (
<span>
<FormattedMessage
id='user.settings.general.positionExtra'
defaultMessage='Tell your teammates what you do.'
/>
</span>
);
submit = this.submitPosition;
}
positionSection = (
<SettingItemMax
title={formatMessage(holders.position)}
inputs={inputs}
submit={submit}
server_error={serverError}
client_error={clientError}
updateSection={(e) => {
this.updateSection('');
e.preventDefault();
}}
extraInfo={extraInfo}
/>
);
} else {
let describe = '';
if (user.position) {
describe = user.position;
} else {
describe = (
<FormattedMessage
id='user.settings.general.emptyPosition'
defaultMessage="Click 'Edit' to add your job title / position"
/>
);
}
positionSection = (
<SettingItemMin
title={formatMessage(holders.position)}
describe={describe}
updateSection={() => {
this.updateSection('position');
}}
/>
);
}
const emailSection = this.createEmailSection();
let pictureSection;
@@ -1030,6 +1150,8 @@ class UserSettingsGeneralTab extends React.Component {
<div className='divider-light'/>
{nicknameSection}
<div className='divider-light'/>
{positionSection}
<div className='divider-light'/>
{emailSection}
<div className='divider-light'/>
{pictureSection}

Просмотреть файл

@@ -441,6 +441,9 @@
"admin.ldap.portDesc": "The port Mattermost will use to connect to the AD/LDAP server. Default is 389.",
"admin.ldap.portEx": "E.g.: \"389\"",
"admin.ldap.portTitle": "AD/LDAP Port:",
"admin.ldap.positionAttrEx": "E.g.: \"title\"",
"admin.ldap.positionAttrDesc": "(Optional) The attribute in the AD/LDAP server that will be used to populate the position field in Mattermost.",
"admin.ldap.positionAttrTitle": "Position Attribute:",
"admin.ldap.queryDesc": "The timeout value for queries to the AD/LDAP server. Increase if you are getting timeout errors caused by a slow AD/LDAP server.",
"admin.ldap.queryEx": "E.g.: \"60\"",
"admin.ldap.queryTitle": "Query Timeout (seconds):",
@@ -614,6 +617,9 @@
"admin.saml.nicknameAttrDesc": "(Optional) The attribute in the SAML Assertion that will be used to populate the nickname of users in Mattermost.",
"admin.saml.nicknameAttrEx": "E.g.: \"Nickname\"",
"admin.saml.nicknameAttrTitle": "Nickname Attribute:",
"admin.saml.positionAttrDesc": "(Optional) The attribute in the SAML Assertion that will be used to populate the position of users in Mattermost.",
"admin.saml.positionAttrEx": "E.g.: \"Role\"",
"admin.saml.positionAttrTitle": "Position Attribute:",
"admin.saml.privateKeyFileFileDesc": "The private key used to decrypt SAML Assertions from the Identity Provider.",
"admin.saml.privateKeyFileFileRemoveDesc": "Remove the private key used to decrypt SAML Assertions from the Identity Provider.",
"admin.saml.privateKeyFileTitle": "Service Provider Private Key:",
@@ -1953,6 +1959,7 @@
"user.settings.general.emailUnchanged": "Your new email address is the same as your old email address.",
"user.settings.general.emptyName": "Click 'Edit' to add your full name",
"user.settings.general.emptyNickname": "Click 'Edit' to add a nickname",
"user.settings.general.emptyPosition": "Click 'Edit' to add your job title / position",
"user.settings.general.field_handled_externally": "This field is handled through your login provider. If you want to change it, you need to do so through your login provider.",
"user.settings.general.firstName": "First Name",
"user.settings.general.fullName": "Full Name",
@@ -1969,6 +1976,8 @@
"user.settings.general.nicknameExtra": "Use Nickname for a name you might be called that is different from your first name and username. This is most often used when two or more people have similar sounding names and usernames.",
"user.settings.general.notificationsExtra": "By default, you will receive mention notifications when someone types your first name. Go to {notify} settings to change this default.",
"user.settings.general.notificationsLink": "Notifications",
"user.settings.general.position": "Position",
"user.settings.general.positionExtra": "Tell your teammates what you do.",
"user.settings.general.primaryEmail": "Primary Email",
"user.settings.general.profilePicture": "Profile Picture",
"user.settings.general.title": "General Settings",

Просмотреть файл

@@ -264,7 +264,8 @@ export const Constants = {
FULLNAME: 'fullname',
NICKNAME: 'nickname',
EMAIL: 'email',
LANGUAGE: 'language'
LANGUAGE: 'language',
POSITION: 'position'
},
ScrollTypes: {
@@ -835,6 +836,7 @@ export const Constants = {
MAX_NICKNAME_LENGTH: 22,
MIN_PASSWORD_LENGTH: 5,
MAX_PASSWORD_LENGTH: 64,
MAX_POSITION_LENGTH: 35,
MIN_TRIGGER_LENGTH: 1,
MAX_TRIGGER_LENGTH: 128,
MAX_TEXTSETTING_LENGTH: 1024,