FEATURE: Show user fields when the user is signing up

This commit is contained in:
Robin Ward
2014-09-26 14:48:34 -04:00
parent 872d8fce58
commit edb34c178a
42 changed files with 476 additions and 141 deletions

View File

@ -13,10 +13,12 @@ export default Ember.ObjectController.extend(BufferedContent, {
save: function() { save: function() {
var self = this; var self = this;
this.commitBuffer(); var attrs = this.get('buffered').getProperties('name', 'field_type', 'editable');
this.get('model').save().then(function(res) {
this.get('model').save(attrs).then(function(res) {
self.set('model.id', res.user_field.id); self.set('model.id', res.user_field.id);
self.set('editing', false); self.set('editing', false);
self.commitBuffer();
}).catch(function() { }).catch(function() {
bootbox.alert(I18n.t('generic_error')); bootbox.alert(I18n.t('generic_error'));
}); });

View File

@ -2,8 +2,10 @@ import UserField from 'admin/models/user-field';
export default Ember.ArrayController.extend({ export default Ember.ArrayController.extend({
fieldTypes: null, fieldTypes: null,
createDisabled: Em.computed.gte('model.length', 3), createDisabled: Em.computed.gte('model.length', 3),
userFieldsName: function() {
return I18n.t('admin.user_fields.name');
}.property(),
_performDestroy: function(f, model) { _performDestroy: function(f, model) {
return f.destroy().then(function() { return f.destroy().then(function() {
@ -13,10 +15,7 @@ export default Ember.ArrayController.extend({
actions: { actions: {
createField: function() { createField: function() {
this.pushObject(UserField.create({ this.pushObject(UserField.create({ field_type: 'text' }));
field_type: 'text',
name: I18n.t('admin.user_fields.untitled')
}));
}, },
destroy: function(f) { destroy: function(f) {

View File

@ -1,14 +1,6 @@
import ObjectController from 'discourse/controllers/object'; import ObjectController from 'discourse/controllers/object';
import CanCheckEmails from 'discourse/mixins/can-check-emails'; import CanCheckEmails from 'discourse/mixins/can-check-emails';
/**
A controller related to viewing a user in the admin section
@class AdminUserIndexController
@extends ObjectController
@namespace Discourse
@module Discourse
**/
export default ObjectController.extend(CanCheckEmails, { export default ObjectController.extend(CanCheckEmails, {
editingTitle: false, editingTitle: false,
originalPrimaryGroupId: null, originalPrimaryGroupId: null,
@ -23,6 +15,19 @@ export default ObjectController.extend(CanCheckEmails, {
return (!g.automatic && g.visible); return (!g.automatic && g.visible);
}), }),
userFields: function() {
var siteUserFields = this.site.get('user_fields'),
userFields = this.get('user_fields');
if (!Ember.empty(siteUserFields)) {
return siteUserFields.map(function(uf) {
var value = userFields ? userFields[uf.get('id').toString()] : null;
return {name: uf.get('name'), value: value};
});
}
return [];
}.property('user_fields.@each'),
actions: { actions: {
toggleTitleEdit: function() { toggleTitleEdit: function() {
this.toggleProperty('editingTitle'); this.toggleProperty('editingTitle');

View File

@ -1,12 +1,3 @@
import ObjectController from 'discourse/controllers/object'; import ObjectController from 'discourse/controllers/object';
/**
The top-level controller for user pages in admin.
Ember assertion says that this class needs to be defined even if it's empty.
@class AdminUserController
@extends ObjectController
@namespace Discourse
@module Discourse
**/
export default ObjectController.extend(); export default ObjectController.extend();

View File

@ -17,17 +17,17 @@ var UserField = Ember.Object.extend({
}); });
}, },
save: function() { save: function(attrs) {
var id = this.get('id'); var id = this.get('id');
if (!id) { if (!id) {
return Discourse.ajax("/admin/customize/user_fields", { return Discourse.ajax("/admin/customize/user_fields", {
type: "POST", type: "POST",
data: { user_field: this.getProperties('name', 'field_type') } data: { user_field: attrs }
}); });
} else { } else {
return Discourse.ajax("/admin/customize/user_fields/" + id, { return Discourse.ajax("/admin/customize/user_fields/" + id, {
type: "PUT", type: "PUT",
data: { user_field: this.getProperties('name', 'field_type') } data: { user_field: attrs }
}); });
} }
} }

View File

@ -4,9 +4,7 @@
<li>{{#link-to 'adminCustomize.colors'}}{{i18n admin.customize.colors.title}}{{/link-to}}</li> <li>{{#link-to 'adminCustomize.colors'}}{{i18n admin.customize.colors.title}}{{/link-to}}</li>
<li>{{#link-to 'adminCustomize.css_html'}}{{i18n admin.customize.css_html.title}}{{/link-to}}</li> <li>{{#link-to 'adminCustomize.css_html'}}{{i18n admin.customize.css_html.title}}{{/link-to}}</li>
<li>{{#link-to 'adminSiteText'}}{{i18n admin.site_text.title}}{{/link-to}}</li> <li>{{#link-to 'adminSiteText'}}{{i18n admin.site_text.title}}{{/link-to}}</li>
{{#if userFieldFeatureComplete}}
<li>{{#link-to 'adminUserFields'}}{{i18n admin.user_fields.title}}{{/link-to}}</li> <li>{{#link-to 'adminUserFields'}}{{i18n admin.user_fields.title}}{{/link-to}}</li>
{{/if}}
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -7,13 +7,14 @@
{{#each f in model itemController="admin-user-field-item" itemView="admin-user-field-item"}} {{#each f in model itemController="admin-user-field-item" itemView="admin-user-field-item"}}
{{#if f.editing}} {{#if f.editing}}
<div class='form-element'> <div class='form-element'>
<label>{{i18n admin.user_fields.name}} {{input value=f.buffered.name class="user-field-name" placeholder=userFieldsName}}
{{input value=f.buffered.name class="user-field-name"}}
</label>
</div> </div>
<div class='form-element'> <div class='form-element'>
<label>{{i18n admin.user_fields.type}}
{{combo-box content=fieldTypes valueAttribute="id" value=f.buffered.field_type}} {{combo-box content=fieldTypes valueAttribute="id" value=f.buffered.field_type}}
</div>
<div class='form-element'>
<label>
{{input type="checkbox" checked=f.buffered.editable}} {{i18n admin.user_fields.editable.title}}
</label> </label>
</div> </div>
<div class='form-element controls'> <div class='form-element controls'>
@ -21,11 +22,14 @@
<button {{action "cancel"}} class='btn btn-danger'>{{fa-icon 'times'}} {{i18n admin.user_fields.cancel}}</button> <button {{action "cancel"}} class='btn btn-danger'>{{fa-icon 'times'}} {{i18n admin.user_fields.cancel}}</button>
</div> </div>
{{else}} {{else}}
<div class='form-display'>{{f.name}}</div>
<div class='form-display'>{{f.fieldName}}</div>
<div class='form-display'> <div class='form-display'>
{{f.name}} {{#if f.editable}}
</div> {{i18n admin.user_fields.editable.enabled}}
<div class='form-display'> {{else}}
{{f.fieldName}} {{i18n admin.user_fields.editable.disabled}}
{{/if}}
</div> </div>
<div class='form-element controls'> <div class='form-element controls'>
<button {{action "edit"}}class='btn btn-default'>{{fa-icon 'pencil'}} {{i18n admin.user_fields.edit}}</button> <button {{action "edit"}}class='btn btn-default'>{{fa-icon 'pencil'}} {{i18n admin.user_fields.edit}}</button>

View File

@ -137,9 +137,25 @@
</div> </div>
</div> </div>
{{/if}} {{/if}}
</section> </section>
{{#if userFields}}
<section class='details'>
{{#each userFields}}
<div class='display-row'>
<div class='field'>{{name}}</div>
<div class='value'>
{{#if value}}
{{value}}
{{else}}
&mdash;
{{/if}}
</div>
</div>
{{/each}}
</section>
{{/if}}
<section class='details'> <section class='details'>
<h1>{{i18n admin.user.permissions}}</h1> <h1>{{i18n admin.user.permissions}}</h1>

View File

@ -0,0 +1,6 @@
export default Ember.Component.extend({
classNameBindings: [':user-field'],
layoutName: function() {
return "components/user-fields/" + this.get('field.field_type');
}.property('field.field_type')
});

View File

@ -15,6 +15,7 @@ export default DiscourseController.extend(ModalFunctionality, {
rejectedPasswords: Em.A([]), rejectedPasswords: Em.A([]),
prefilledUsername: null, prefilledUsername: null,
tosAccepted: false, tosAccepted: false,
userFields: null,
hasAuthOptions: Em.computed.notEmpty('authOptions'), hasAuthOptions: Em.computed.notEmpty('authOptions'),
canCreateLocal: Discourse.computed.setting('enable_local_logins'), canCreateLocal: Discourse.computed.setting('enable_local_logins'),
@ -22,6 +23,8 @@ export default DiscourseController.extend(ModalFunctionality, {
maxUsernameLength: Discourse.computed.setting('max_username_length'), maxUsernameLength: Discourse.computed.setting('max_username_length'),
resetForm: function() { resetForm: function() {
// We wrap the fields in a structure so we can assign a value
this.setProperties({ this.setProperties({
accountName: '', accountName: '',
accountEmail: '', accountEmail: '',
@ -31,10 +34,11 @@ export default DiscourseController.extend(ModalFunctionality, {
globalNicknameExists: false, globalNicknameExists: false,
complete: false, complete: false,
formSubmitted: false, formSubmitted: false,
rejectedEmails: Em.A([]), rejectedEmails: [],
rejectedPasswords: Em.A([]), rejectedPasswords: [],
prefilledUsername: null prefilledUsername: null,
}); });
this._createUserFields();
}, },
submitDisabled: function() { submitDisabled: function() {
@ -47,8 +51,18 @@ export default DiscourseController.extend(ModalFunctionality, {
if (this.get('emailValidation.failed')) return true; if (this.get('emailValidation.failed')) return true;
if (this.get('usernameValidation.failed')) return true; if (this.get('usernameValidation.failed')) return true;
if (this.get('passwordValidation.failed')) return true; if (this.get('passwordValidation.failed')) return true;
// Validate required fields
var userFields = this.get('userFields');
if (!Ember.empty(userFields)) {
var anyEmpty = userFields.any(function(uf) {
var val = uf.get('value');
return !val || Ember.empty(val);
});
if (anyEmpty) { return true; }
}
return false; return false;
}.property('passwordRequired', 'nameValidation.failed', 'emailValidation.failed', 'usernameValidation.failed', 'passwordValidation.failed', 'formSubmitted', 'tosAccepted'), }.property('passwordRequired', 'nameValidation.failed', 'emailValidation.failed', 'usernameValidation.failed', 'passwordValidation.failed', 'formSubmitted', 'tosAccepted', 'userFields.@each.value'),
passwordRequired: function() { passwordRequired: function() {
return this.blank('authOptions.auth_provider'); return this.blank('authOptions.auth_provider');
@ -337,20 +351,25 @@ export default DiscourseController.extend(ModalFunctionality, {
}, },
createAccount: function() { createAccount: function() {
var self = this; var self = this,
attrs = this.getProperties('accountName', 'accountEmail', 'accountPassword', 'accountUsername', 'accountPasswordConfirm', 'accountChallenge'),
userFields = this.get('userFields');
// Add the userfields to the data
if (!Em.empty(userFields)) {
attrs.userFields = {};
userFields.forEach(function(f) {
attrs.userFields[f.get('field.id')] = f.get('value');
});
}
this.set('formSubmitted', true); this.set('formSubmitted', true);
var name = this.get('accountName'); return Discourse.User.createAccount(attrs).then(function(result) {
var email = this.get('accountEmail');
var password = this.get('accountPassword');
var username = this.get('accountUsername');
var passwordConfirm = this.get('accountPasswordConfirm');
var challenge = this.get('accountChallenge');
return Discourse.User.createAccount(name, email, password, username, passwordConfirm, challenge).then(function(result) {
if (result.success) { if (result.success) {
// Trigger the browser's password manager using the hidden static login form: // Trigger the browser's password manager using the hidden static login form:
var $hidden_login_form = $('#hidden-login-form'); var $hidden_login_form = $('#hidden-login-form');
$hidden_login_form.find('input[name=username]').val(self.get('accountName')); $hidden_login_form.find('input[name=username]').val(attrs.accountName);
$hidden_login_form.find('input[name=password]').val(self.get('accountPassword')); $hidden_login_form.find('input[name=password]').val(attrs.accountPassword);
$hidden_login_form.find('input[name=redirect]').val(Discourse.getURL('/users/account-created')); $hidden_login_form.find('input[name=redirect]').val(Discourse.getURL('/users/account-created'));
$hidden_login_form.submit(); $hidden_login_form.submit();
} else { } else {
@ -359,7 +378,7 @@ export default DiscourseController.extend(ModalFunctionality, {
self.get('rejectedEmails').pushObject(result.values.email); self.get('rejectedEmails').pushObject(result.values.email);
} }
if (result.errors && result.errors.password && result.errors.password.length > 0) { if (result.errors && result.errors.password && result.errors.password.length > 0) {
self.get('rejectedPasswords').pushObject(password); self.get('rejectedPasswords').pushObject(attrs.accountPassword);
} }
self.set('formSubmitted', false); self.set('formSubmitted', false);
} }
@ -371,5 +390,21 @@ export default DiscourseController.extend(ModalFunctionality, {
return self.flash(I18n.t('create_account.failed'), 'error'); return self.flash(I18n.t('create_account.failed'), 'error');
}); });
} }
} },
_createUserFields: function() {
if (!this.site) { return; }
var userFields = this.site.get('user_fields');
if (userFields) {
userFields = userFields.map(function(f) {
return Ember.Object.create({
value: null,
field: f
});
});
}
this.set('userFields', userFields);
}.on('init')
}); });

View File

@ -18,6 +18,17 @@ export default ObjectController.extend(CanCheckEmails, {
newNameInput: null, newNameInput: null,
userFields: function() {
var siteUserFields = this.site.get('user_fields');
if (!Ember.empty(siteUserFields)) {
var userFields = this.get('user_fields');
return siteUserFields.filterProperty('editable', true).map(function(uf) {
var val = userFields ? userFields[uf.get('id').toString()] : null;
return Ember.Object.create({value: val, field: uf});
});
}
}.property('user_fields.@each.value'),
cannotDeleteAccount: Em.computed.not('can_delete_account'), cannotDeleteAccount: Em.computed.not('can_delete_account'),
deleteDisabled: Em.computed.or('saving', 'deleting', 'cannotDeleteAccount'), deleteDisabled: Em.computed.or('saving', 'deleting', 'cannotDeleteAccount'),
@ -70,8 +81,20 @@ export default ObjectController.extend(CanCheckEmails, {
var self = this; var self = this;
this.setProperties({ saving: true, saved: false }); this.setProperties({ saving: true, saved: false });
var model = this.get('model'),
userFields = this.get('userFields');
// Update the user fields
if (!Em.empty(userFields)) {
var modelFields = model.get('user_fields');
if (!Em.empty(modelFields)) {
userFields.forEach(function(uf) {
modelFields[uf.get('field.id').toString()] = uf.get('value');
});
}
}
// Cook the bio for preview // Cook the bio for preview
var model = this.get('model');
model.set('name', this.get('newNameInput')); model.set('name', this.get('newNameInput'));
return model.save().then(function() { return model.save().then(function() {
// model was saved // model was saved

View File

@ -133,6 +133,12 @@ Discourse.Site.reopenClass(Discourse.Singleton, {
}); });
} }
if (result.user_fields) {
result.user_fields = result.user_fields.map(function(uf) {
return Ember.Object.create(uf);
});
}
return result; return result;
} }
}); });

View File

@ -188,8 +188,8 @@ Discourse.User = Discourse.Model.extend({
@returns {Promise} the result of the operation @returns {Promise} the result of the operation
**/ **/
save: function() { save: function() {
var user = this; var self = this,
var data = this.getProperties('auto_track_topics_after_msecs', data = this.getProperties('auto_track_topics_after_msecs',
'bio_raw', 'bio_raw',
'website', 'website',
'location', 'location',
@ -206,10 +206,11 @@ Discourse.User = Discourse.Model.extend({
'mailing_list_mode', 'mailing_list_mode',
'enable_quoting', 'enable_quoting',
'disable_jump_reply', 'disable_jump_reply',
'custom_fields'); 'custom_fields',
'user_fields');
_.each(['muted','watched','tracked'], function(s){ ['muted','watched','tracked'].forEach(function(s){
var cats = user.get(s + 'Categories').map(function(c){ return c.get('id')}); var cats = self.get(s + 'Categories').map(function(c){ return c.get('id')});
// HACK: denote lack of categories // HACK: denote lack of categories
if(cats.length === 0) { cats = [-1]; } if(cats.length === 0) { cats = [-1]; }
data[s + '_category_ids'] = cats; data[s + '_category_ids'] = cats;
@ -223,13 +224,10 @@ Discourse.User = Discourse.Model.extend({
data: data, data: data,
type: 'PUT' type: 'PUT'
}).then(function(data) { }).then(function(data) {
user.set('bio_excerpt',data.user.bio_excerpt); self.set('bio_excerpt',data.user.bio_excerpt);
_.each([ var userProps = self.getProperties('enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon');
'enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon' Discourse.User.current().setProperties(userProps);
], function(preference) {
Discourse.User.current().set(preference, user.get(preference));
});
}); });
}, },
@ -542,26 +540,18 @@ Discourse.User.reopenClass(Discourse.Singleton, {
}, },
/** /**
Creates a new account over POST Creates a new account
@method createAccount
@param {String} name This user's name
@param {String} email This user's email
@param {String} password This user's password
@param {String} username This user's username
@param {String} passwordConfirm This user's confirmed password
@param {String} challenge
@returns Result of ajax call
**/ **/
createAccount: function(name, email, password, username, passwordConfirm, challenge) { createAccount: function(attrs) {
return Discourse.ajax("/users", { return Discourse.ajax("/users", {
data: { data: {
name: name, name: attrs.accountName,
email: email, email: attrs.accountEmail,
password: password, password: attrs.accountPassword,
username: username, username: attrs.accountUsername,
password_confirmation: passwordConfirm, password_confirmation: attrs.accountPasswordConfirm,
challenge: challenge challenge: attrs.accountChallenge,
user_fields: attrs.userFields
}, },
type: 'POST' type: 'POST'
}); });

View File

@ -0,0 +1,3 @@
<label>
{{input checked=value type="checkbox"}} {{field.name}}
</label>

View File

@ -0,0 +1,4 @@
<label>
{{field.name}}
{{input value=value}}
</label>

View File

@ -70,6 +70,17 @@
</tr> </tr>
</table> </table>
{{#if userFields}}
<div class='user-fields'>
<h3>{{i18n create_account.required_information}}</h3>
{{#each userFields}}
{{user-field field=field value=value}}
{{/each}}
</div>
{{/if}}
</form> </form>
</div> </div>
{{/if}} {{/if}}

View File

@ -178,6 +178,11 @@
{{#unless editHistoryVisible}} {{#unless editHistoryVisible}}
{{preference-checkbox labelKey="user.edit_history_public" checked=edit_history_public}} {{preference-checkbox labelKey="user.edit_history_public" checked=edit_history_public}}
{{/unless}} {{/unless}}
{{#each userFields}}
{{user-field field=field value=value}}
{{/each}}
{{plugin-outlet "user_custom_preferences"}} {{plugin-outlet "user_custom_preferences"}}
</div> </div>

View File

@ -1,5 +1,5 @@
export default Discourse.ModalBodyView.extend({ export default Discourse.ModalBodyView.extend({
templateName: 'modal/create_account', templateName: 'modal/create-account',
title: I18n.t('create_account.title'), title: I18n.t('create_account.title'),
classNames: ['create-account'], classNames: ['create-account'],

View File

@ -1331,21 +1331,14 @@ tr.not-activated {
border-bottom: 1px solid scale-color-diff(); border-bottom: 1px solid scale-color-diff();
.form-display { .form-display {
width: 35%; width: 25%;
display: inline-block; display: inline-block;
float: left; float: left;
} }
.form-element { .form-element {
float: left; float: left;
width: 35%; width: 25%;
margin-right: 10px;
label {
margin-right: 10px;
}
input, div.combobox {
margin-left: 10px;
}
} }
.controls { .controls {

View File

@ -11,3 +11,28 @@
.discourse-touch .caps-lock-warning { .discourse-touch .caps-lock-warning {
display: none; display: none;
} }
.user-fields {
h3 {
line-height: 1.5em;
color: scale-color($primary, $lightness: 20%);
border-bottom: 1px solid scale-color($primary, $lightness: 50%);
margin-bottom: 20px;
}
.user-field {
label: {
display: block;
}
input[type=text] {
width: 80%;
display: block;
}
input[type=checkbox] {
margin-right: 5px;
}
margin-bottom: 20px;
}
}

View File

@ -63,3 +63,4 @@
margin: 5px 10px 5px 0; margin: 5px 10px 5px 0;
} }
} }

View File

@ -426,4 +426,13 @@
.suspensions { .suspensions {
background-color: #c22020; background-color: #c22020;
} }
.user-field {
margin-left: 160px;
margin-top: 10px;
input[type=text] {
width: 540px;
display: block;
}
}
} }

View File

@ -1,7 +1,7 @@
class Admin::UserFieldsController < Admin::AdminController class Admin::UserFieldsController < Admin::AdminController
def create def create
field = UserField.create!(params.require(:user_field).permit(:name, :field_type)) field = UserField.create!(params.require(:user_field).permit(:name, :field_type, :editable))
render_serialized(field, UserFieldSerializer) render_serialized(field, UserFieldSerializer)
end end
@ -15,7 +15,8 @@ class Admin::UserFieldsController < Admin::AdminController
field = UserField.where(id: params.require(:id)).first field = UserField.where(id: params.require(:id)).first
field.name = field_params[:name] field.name = field_params[:name]
field.field_type = field_params[:field_type] field.field_type = field_params[:field_type]
field.save field.editable = field_params[:editable] == "true"
field.save!
render_serialized(field, UserFieldSerializer) render_serialized(field, UserFieldSerializer)
end end

View File

@ -46,6 +46,16 @@ class UsersController < ApplicationController
def update def update
user = fetch_user_from_params user = fetch_user_from_params
guardian.ensure_can_edit!(user) guardian.ensure_can_edit!(user)
if params[:user_fields].present?
params[:custom_fields] ||= {}
UserField.where(editable: true).pluck(:id).each do |fid|
val = params[:user_fields][fid.to_s]
return render_json_error(I18n.t("login.missing_user_field")) if val.blank?
params[:custom_fields]["user_field_#{fid}"] = val
end
end
json_result(user, serializer: UserSerializer, additional_errors: [:user_profile]) do |u| json_result(user, serializer: UserSerializer, additional_errors: [:user_profile]) do |u|
updater = UserUpdater.new(current_user, user) updater = UserUpdater.new(current_user, user)
updater.update(params) updater.update(params)
@ -162,18 +172,34 @@ class UsersController < ApplicationController
end end
def create def create
params.permit(:user_fields)
unless SiteSetting.allow_new_registrations unless SiteSetting.allow_new_registrations
render json: { success: false, message: I18n.t("login.new_registrations_disabled") } return fail_with("login.new_registrations_disabled")
return
end end
if params[:password] && params[:password].length > User.max_password_length if params[:password] && params[:password].length > User.max_password_length
render json: { success: false, message: I18n.t("login.password_too_long") } return fail_with("login.password_too_long")
return
end end
user = User.new(user_params) user = User.new(user_params)
# Handle custom fields
user_field_ids = UserField.pluck(:id)
if user_field_ids.present?
if params[:user_fields].blank?
return fail_with("login.missing_user_field")
else
fields = user.custom_fields
user_field_ids.each do |fid|
field_val = params[:user_fields][fid.to_s]
return fail_with("login.missing_user_field") if field_val.blank?
fields["user_field_#{fid}"] = field_val
end
user.custom_fields = fields
end
end
authentication = UserAuthenticator.new(user, session) authentication = UserAuthenticator.new(user, session)
if !authentication.has_authenticator? && !SiteSetting.enable_local_logins if !authentication.has_authenticator? && !SiteSetting.enable_local_logins
@ -194,6 +220,7 @@ class UsersController < ApplicationController
authentication.finish authentication.finish
activation.finish activation.finish
render json: { render json: {
success: true, success: true,
active: user.active?, active: user.active?,
@ -550,4 +577,9 @@ class UsersController < ApplicationController
:active :active
).merge(ip_address: request.ip, registration_ip_address: request.ip) ).merge(ip_address: request.ip, registration_ip_address: request.ip)
end end
def fail_with(key)
render json: { success: false, message: I18n.t(key) }
end
end end

View File

@ -33,6 +33,10 @@ class Site
@groups ||= Group.order(:name).map { |g| {:id => g.id, :name => g.name}} @groups ||= Group.order(:name).map { |g| {:id => g.id, :name => g.name}}
end end
def user_fields
UserField.all
end
def categories def categories
@categories ||= begin @categories ||= begin
categories = Category categories = Category

View File

@ -657,6 +657,18 @@ class User < ActiveRecord::Base
result.empty? ? I18n.t("user.no_accounts_associated") : result.join(", ") result.empty? ? I18n.t("user.no_accounts_associated") : result.join(", ")
end end
def user_fields
return @user_fields if @user_fields
user_field_ids = UserField.pluck(:id)
if user_field_ids.present?
@user_fields = {}
user_field_ids.each do |fid|
@user_fields[fid.to_s] = custom_fields["user_field_#{fid}"]
end
end
@user_fields
end
protected protected
def badge_grant def badge_grant

View File

@ -1,3 +1,3 @@
class UserFieldSerializer < ApplicationSerializer class UserFieldSerializer < ApplicationSerializer
attributes :id, :name, :field_type attributes :id, :name, :field_type, :editable
end end

View File

@ -18,7 +18,8 @@ class AdminDetailedUserSerializer < AdminUserSerializer
:suspend_reason, :suspend_reason,
:primary_group_id, :primary_group_id,
:badge_count, :badge_count,
:warnings_received_count :warnings_received_count,
:user_fields
has_one :approved_by, serializer: BasicUserSerializer, embed: :objects has_one :approved_by, serializer: BasicUserSerializer, embed: :objects
has_one :api_key, serializer: ApiKeySerializer, embed: :objects has_one :api_key, serializer: ApiKeySerializer, embed: :objects
@ -74,4 +75,12 @@ class AdminDetailedUserSerializer < AdminUserSerializer
object.has_trust_level?(TrustLevel[2]) object.has_trust_level?(TrustLevel[2])
end end
def user_fields
object.user_fields
end
def include_user_fields?
object.user_fields.present?
end
end end

View File

@ -16,6 +16,7 @@ class SiteSerializer < ApplicationSerializer
has_many :topic_flag_types, serializer: TopicFlagTypeSerializer, embed: :objects has_many :topic_flag_types, serializer: TopicFlagTypeSerializer, embed: :objects
has_many :trust_levels, embed: :objects has_many :trust_levels, embed: :objects
has_many :archetypes, embed: :objects, serializer: ArchetypeSerializer has_many :archetypes, embed: :objects, serializer: ArchetypeSerializer
has_many :user_fields, embed: :objects, serialzer: UserFieldSerializer
def default_archetype def default_archetype

View File

@ -46,7 +46,8 @@ class UserSerializer < BasicUserSerializer
:notification_count, :notification_count,
:has_title_badges, :has_title_badges,
:edit_history_public, :edit_history_public,
:custom_fields :custom_fields,
:user_fields
has_one :invited_by, embed: :object, serializer: BasicUserSerializer has_one :invited_by, embed: :object, serializer: BasicUserSerializer
has_many :custom_groups, embed: :object, serializer: BasicGroupSerializer has_many :custom_groups, embed: :object, serializer: BasicGroupSerializer
@ -253,6 +254,14 @@ class UserSerializer < BasicUserSerializer
can_edit && !SiteSetting.edit_history_visible_to_public can_edit && !SiteSetting.edit_history_visible_to_public
end end
def user_fields
object.user_fields
end
def include_user_fields?
user_fields.present?
end
def custom_fields def custom_fields
fields = nil fields = nil

View File

@ -68,7 +68,7 @@ class UserUpdater
fields = attributes[:custom_fields] fields = attributes[:custom_fields]
if fields.present? if fields.present?
user.custom_fields = fields user.custom_fields = user.custom_fields.merge(fields)
end end
User.transaction do User.transaction do

View File

@ -579,6 +579,7 @@ en:
create_account: create_account:
title: "Create New Account" title: "Create New Account"
failed: "Something went wrong, perhaps this email is already registered, try the forgot password link" failed: "Something went wrong, perhaps this email is already registered, try the forgot password link"
required_information: "Required Information"
forgot_password: forgot_password:
title: "Forgot Password" title: "Forgot Password"
@ -2006,6 +2007,10 @@ en:
delete: "Delete" delete: "Delete"
cancel: "Cancel" cancel: "Cancel"
delete_confirm: "Are you sure you want to delete that user field?" delete_confirm: "Are you sure you want to delete that user field?"
editable:
title: "Editable after signup?"
enabled: "editable"
disabled: "not editable"
field_types: field_types:
text: 'Text Field' text: 'Text Field'

View File

@ -1103,6 +1103,7 @@ en:
omniauth_error_unknown: "Something went wrong processing your log in, please try again." omniauth_error_unknown: "Something went wrong processing your log in, please try again."
new_registrations_disabled: "New account registrations are not allowed at this time." new_registrations_disabled: "New account registrations are not allowed at this time."
password_too_long: "Passwords are limited to 200 characters." password_too_long: "Passwords are limited to 200 characters."
missing_user_field: "You have not completed all the user fields"
user: user:
no_accounts_associated: "No accounts associated" no_accounts_associated: "No accounts associated"

View File

@ -0,0 +1,5 @@
class AddEditableToUserFields < ActiveRecord::Migration
def change
add_column :user_fields, :editable, :boolean, default: false, null: false
end
end

View File

@ -531,7 +531,6 @@ describe UsersController do
end end
context 'when an Exception is raised' do context 'when an Exception is raised' do
[ ActiveRecord::StatementInvalid, [ ActiveRecord::StatementInvalid,
RestClient::Forbidden ].each do |exception| RestClient::Forbidden ].each do |exception|
before { User.any_instance.stubs(:save).raises(exception) } before { User.any_instance.stubs(:save).raises(exception) }
@ -545,6 +544,40 @@ describe UsersController do
end end
end end
context "with custom fields" do
let!(:user_field) { Fabricate(:user_field) }
let!(:another_field) { Fabricate(:user_field) }
context "without a value for the fields" do
let(:create_params) { {name: @user.name, password: 'watwatwat', username: @user.username, email: @user.email} }
include_examples 'failed signup'
end
context "with values for the fields" do
let(:create_params) { {
name: @user.name,
password: 'watwatwat',
username: @user.username,
email: @user.email,
user_fields: {
user_field.id.to_s => 'value1',
another_field.id.to_s => 'value2',
}
} }
it "should succeed" do
xhr :post, :create, create_params
response.should be_success
inserted = User.where(email: @user.email).first
inserted.should be_present
inserted.custom_fields.should be_present
inserted.custom_fields["user_field_#{user_field.id}"].should == 'value1'
inserted.custom_fields["user_field_#{another_field.id}"].should == 'value2'
end
end
end
end end
context '.username' do context '.username' do
@ -844,12 +877,10 @@ describe UsersController do
context 'with authenticated user' do context 'with authenticated user' do
context 'with permission to update' do context 'with permission to update' do
let!(:user) { log_in(:user) }
it 'allows the update' do it 'allows the update' do
user = Fabricate(:user, name: 'Billy Bob')
log_in_user(user)
put :update, username: user.username, name: 'Jim Tom', custom_fields: {test: :it} put :update, username: user.username, name: 'Jim Tom', custom_fields: {test: :it}
expect(response).to be_success expect(response).to be_success
user.reload user.reload
@ -858,14 +889,42 @@ describe UsersController do
expect(user.custom_fields['test']).to eq 'it' expect(user.custom_fields['test']).to eq 'it'
end end
it 'returns user JSON' do context "with user fields" do
user = log_in context "an editable field" do
let!(:user_field) { Fabricate(:user_field) }
it "should update the user field" do
put :update, username: user.username, name: 'Jim Tom', user_fields: { user_field.id.to_s => 'happy' }
expect(response).to be_success
expect(user.user_fields[user_field.id.to_s]).to eq 'happy'
end
it "cannot be updated to blank" do
put :update, username: user.username, name: 'Jim Tom', user_fields: { user_field.id.to_s => '' }
response.should_not be_success
user.user_fields[user_field.id.to_s].should_not == 'happy'
end
end
context "uneditable field" do
let!(:user_field) { Fabricate(:user_field, editable: false) }
it "does not update the user field" do
put :update, username: user.username, name: 'Jim Tom', user_fields: { user_field.id.to_s => 'happy' }
expect(response).to be_success
expect(user.user_fields[user_field.id.to_s]).to be_blank
end
end
end
it 'returns user JSON' do
put :update, username: user.username put :update, username: user.username
json = JSON.parse(response.body) json = JSON.parse(response.body)
expect(json['user']['id']).to eq user.id expect(json['user']['id']).to eq user.id
end end
end end
context 'without permission to update' do context 'without permission to update' do

View File

@ -1,4 +1,5 @@
Fabricator(:user_field) do Fabricator(:user_field) do
name { sequence(:name) {|i| "field_#{i}" } } name { sequence(:name) {|i| "field_#{i}" } }
field_type 'text' field_type 'text'
editable true
end end

File diff suppressed because one or more lines are too long

View File

@ -1,9 +1,13 @@
/* global asyncTest */ /* global asyncTest */
/* exported integration, testController, controllerFor, asyncTestDiscourse, fixture */
function integration(name, options) { import siteFixtures from 'fixtures/site_fixtures';
export function integration(name, options) {
module("Integration: " + name, { module("Integration: " + name, {
setup: function() { setup: function() {
Ember.run(Discourse, Discourse.advanceReadiness); Ember.run(Discourse, Discourse.advanceReadiness);
var siteJson = siteFixtures['site.json'].site;
if (options) { if (options) {
if (options.setup) { if (options.setup) {
options.setup.call(this); options.setup.call(this);
@ -16,7 +20,12 @@ function integration(name, options) {
if (options.settings) { if (options.settings) {
Discourse.SiteSettings = jQuery.extend(true, Discourse.SiteSettings, options.settings); Discourse.SiteSettings = jQuery.extend(true, Discourse.SiteSettings, options.settings);
} }
if (options.site) {
Discourse.Site.resetCurrent(Discourse.Site.create(jQuery.extend(true, {}, siteJson, options.site)));
} }
}
Discourse.reset(); Discourse.reset();
}, },
@ -30,13 +39,13 @@ function integration(name, options) {
}); });
} }
function controllerFor(controller, model) { export function controllerFor(controller, model) {
controller = Discourse.__container__.lookup('controller:' + controller); controller = Discourse.__container__.lookup('controller:' + controller);
if (model) { controller.set('model', model ); } if (model) { controller.set('model', model ); }
return controller; return controller;
} }
function asyncTestDiscourse(text, func) { export function asyncTestDiscourse(text, func) {
asyncTest(text, function () { asyncTest(text, function () {
var self = this; var self = this;
Ember.run(function () { Ember.run(function () {
@ -45,7 +54,7 @@ function asyncTestDiscourse(text, func) {
}); });
} }
function fixture(selector) { export function fixture(selector) {
if (selector) { if (selector) {
return $("#qunit-fixture").find(selector); return $("#qunit-fixture").find(selector);
} }

View File

@ -0,0 +1,46 @@
import { integration } from "helpers/qunit-helpers";
integration("Create Account - User Fields", {
site: {
user_fields: [{"id":34,"name":"I've read the terms of service","field_type":"confirm"},
{"id":35,"name":"What is your pet's name?","field_type":"text"}]
}
});
test("create account with user fields", function() {
visit("/");
click("header .sign-up-button");
andThen(function() {
ok(exists('.create-account'), "it shows the create account modal");
ok(exists('.user-field'), "it has at least one user field");
ok(exists('.modal-footer .btn-primary:disabled'), 'create account is disabled at first');
});
fillIn('#new-account-name', 'Dr. Good Tuna');
fillIn('#new-account-password', 'cool password bro');
fillIn('#new-account-email', 'good.tuna@test.com');
fillIn('#new-account-username', 'goodtuna');
andThen(function() {
ok(exists('#username-validation.good'), 'the username validation is good');
ok(exists('.modal-footer .btn-primary:disabled'), 'create account is still disabled due to lack of user fields');
});
fillIn(".user-field input[type=text]", "Barky");
andThen(function() {
ok(exists('.modal-footer .btn-primary:disabled'), 'create account is disabled because field is not checked');
});
click(".user-field input[type=checkbox]");
andThen(function() {
not(exists('.modal-footer .btn-primary:disabled'), 'create account is disabled because field is not checked');
});
click(".user-field input[type=checkbox]");
andThen(function() {
ok(exists('.modal-footer .btn-primary:disabled'), 'unclicking the checkbox disables the submit');
});
});

View File

@ -20,16 +20,18 @@ test('has a postStream', function() {
equal(postStream.get('topic'), topic, "the postStream has a reference back to the topic"); equal(postStream.get('topic'), topic, "the postStream has a reference back to the topic");
}); });
var category = _.first(Discourse.Category.list());
test('category relationship', function() { test('category relationship', function() {
// It finds the category by id // It finds the category by id
var topic = Discourse.Topic.create({id: 1111, category_id: category.get('id') }); var category = Discourse.Category.list()[0],
topic = Discourse.Topic.create({id: 1111, category_id: category.get('id') });
equal(topic.get('category'), category); equal(topic.get('category'), category);
}); });
test("updateFromJson", function() { test("updateFromJson", function() {
var topic = Discourse.Topic.create({id: 1234}); var topic = Discourse.Topic.create({id: 1234}),
category = Discourse.Category.list()[0];
topic.updateFromJson({ topic.updateFromJson({
post_stream: [1,2,3], post_stream: [1,2,3],

View File

@ -42,7 +42,7 @@
//= require sinon-qunit-1.0.0 //= require sinon-qunit-1.0.0
//= require jshint //= require jshint
//= require helpers/qunit_helpers //= require helpers/qunit-helpers
//= require helpers/assertions //= require helpers/assertions
//= require helpers/init-ember-qunit //= require helpers/init-ember-qunit
@ -50,7 +50,6 @@
//= require_tree ./lib //= require_tree ./lib
//= require_tree . //= require_tree .
//= require_self //= require_self
//= require jshint_all
// sinon settings // sinon settings
sinon.config = { sinon.config = {
@ -87,6 +86,7 @@ if (window.Logster) {
var origDebounce = Ember.run.debounce, var origDebounce = Ember.run.debounce,
createPretendServer = require('helpers/create-pretender', null, null, false).default, createPretendServer = require('helpers/create-pretender', null, null, false).default,
fixtures = require('fixtures/site_fixtures', null, null, false).default,
server; server;
QUnit.testStart(function(ctx) { QUnit.testStart(function(ctx) {
@ -97,6 +97,7 @@ QUnit.testStart(function(ctx) {
Discourse.BaseUri = "/"; Discourse.BaseUri = "/";
Discourse.BaseUrl = ""; Discourse.BaseUrl = "";
Discourse.User.resetCurrent(); Discourse.User.resetCurrent();
Discourse.Site.resetCurrent(Discourse.Site.create(fixtures['site.json'].site));
PreloadStore.reset(); PreloadStore.reset();
window.sandbox = sinon.sandbox.create(); window.sandbox = sinon.sandbox.create();
@ -121,6 +122,15 @@ QUnit.testDone(function() {
}); });
// Load ES6 tests // Load ES6 tests
var helpers = require("helpers/qunit-helpers");
// TODO: Replace with proper imports rather than globals
window.asyncTestDiscourse = helpers.asyncTestDiscourse;
window.controllerFor = helpers.controllerFor;
window.fixture = helpers.fixture;
window.integration = helpers.integration;
Ember.keys(requirejs.entries).forEach(function(entry) { Ember.keys(requirejs.entries).forEach(function(entry) {
if ((/\-test/).test(entry)) { if ((/\-test/).test(entry)) {
require(entry, null, null, true); require(entry, null, null, true);