summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGeorge Goldberg <george@gberg.me>2016-12-14 11:11:51 +0000
committerenahum <nahumhbl@gmail.com>2016-12-14 08:11:51 -0300
commit8406e854aa912f3d7f9179b10356444f07e25223 (patch)
tree353294a5ae87cf0c0cf3e675f32073fff1988d16
parent973585450378a457a94824b7852c7ab7194e2b3e (diff)
downloadchat-8406e854aa912f3d7f9179b10356444f07e25223.tar.gz
chat-8406e854aa912f3d7f9179b10356444f07e25223.tar.bz2
chat-8406e854aa912f3d7f9179b10356444f07e25223.zip
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.
-rw-r--r--config/config.json2
-rw-r--r--i18n/en.json4
-rw-r--r--model/config.go12
-rw-r--r--model/user.go5
-rw-r--r--model/user_test.go11
-rw-r--r--store/sql_upgrade.go3
-rw-r--r--store/sql_user_store.go1
-rw-r--r--webapp/components/admin_console/ldap_settings.jsx21
-rw-r--r--webapp/components/admin_console/saml_settings.jsx21
-rw-r--r--webapp/components/user_profile.jsx17
-rw-r--r--webapp/components/user_settings/user_settings_general.jsx122
-rw-r--r--webapp/i18n/en.json9
-rw-r--r--webapp/utils/constants.jsx4
13 files changed, 231 insertions, 1 deletions
diff --git a/config/config.json b/config/config.json
index a51e8f60e..2209a9656 100644
--- a/config/config.json
+++ b/config/config.json
@@ -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": {
diff --git a/i18n/en.json b/i18n/en.json
index 37acda939..a035da711 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -3876,6 +3876,10 @@
"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."
},
diff --git a/model/config.go b/model/config.go
index 7d3cb93d6..0a3fcb33e 100644
--- a/model/config.go
+++ b/model/config.go
@@ -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 = ""
diff --git a/model/user.go b/model/user.go
index 330d26d82..76c3772cb 100644
--- a/model/user.go
+++ b/model/user.go
@@ -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)
}
diff --git a/model/user_test.go b/model/user_test.go
index 2f6524c05..15b1aae6f 100644
--- a/model/user_test.go
+++ b/model/user_test.go
@@ -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) {
diff --git a/store/sql_upgrade.go b/store/sql_upgrade.go
index e4b1906e0..a275f664c 100644
--- a/store/sql_upgrade.go
+++ b/store/sql_upgrade.go
@@ -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)
//}
}
diff --git a/store/sql_user_store.go b/store/sql_user_store.go
index 5882ed454..b71d8214c 100644
--- a/store/sql_user_store.go
+++ b/store/sql_user_store.go
@@ -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
diff --git a/webapp/components/admin_console/ldap_settings.jsx b/webapp/components/admin_console/ldap_settings.jsx
index 5aa23fde6..b774d34f3 100644
--- a/webapp/components/admin_console/ldap_settings.jsx
+++ b/webapp/components/admin_console/ldap_settings.jsx
@@ -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,
@@ -300,6 +302,25 @@ export default class LdapSettings extends AdminSettings {
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={
<FormattedMessage
diff --git a/webapp/components/admin_console/saml_settings.jsx b/webapp/components/admin_console/saml_settings.jsx
index c7cd73399..1391df0b6 100644
--- a/webapp/components/admin_console/saml_settings.jsx
+++ b/webapp/components/admin_console/saml_settings.jsx
@@ -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
};
@@ -509,6 +511,25 @@ export default class SamlSettings extends AdminSettings {
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={
<FormattedMessage
diff --git a/webapp/components/user_profile.jsx b/webapp/components/user_profile.jsx
index 21dbf9699..9cf2464cd 100644
--- a/webapp/components/user_profile.jsx
+++ b/webapp/components/user_profile.jsx
@@ -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
diff --git a/webapp/components/user_settings/user_settings_general.jsx b/webapp/components/user_settings/user_settings_general.jsx
index b9db1389f..abc0e02f0 100644
--- a/webapp/components/user_settings/user_settings_general.jsx
+++ b/webapp/components/user_settings/user_settings_general.jsx
@@ -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}
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index c7198817f..3658b40ae 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -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",
diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx
index 1eac2732b..94fa19ea9 100644
--- a/webapp/utils/constants.jsx
+++ b/webapp/utils/constants.jsx
@@ -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,