From edb34c178a8bccf5343db4f27e683f90af9f98fb Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 26 Sep 2014 14:48:34 -0400 Subject: [PATCH] FEATURE: Show user fields when the user is signing up --- .../controllers/admin-user-field-item.js.es6 | 6 +- .../controllers/admin-user-fields.js.es6 | 9 +-- .../admin/controllers/admin-user-index.js.es6 | 21 ++++-- .../admin/controllers/admin-user.js.es6 | 9 --- .../admin/models/user-field.js.es6 | 6 +- .../javascripts/admin/templates/customize.hbs | 4 +- .../admin/templates/user-fields.hbs | 22 +++--- .../admin/templates/user_index.hbs | 48 ++++++++---- .../discourse/components/user-field.js.es6 | 6 ++ .../controllers/create-account.js.es6 | 67 +++++++++++++---- .../discourse/controllers/preferences.js.es6 | 25 ++++++- .../javascripts/discourse/models/site.js | 6 ++ .../javascripts/discourse/models/user.js | 46 +++++------- .../components/user-fields/confirm.hbs | 3 + .../templates/components/user-fields/text.hbs | 4 + ...{create_account.hbs => create-account.hbs} | 17 ++++- .../discourse/templates/user/preferences.hbs | 5 ++ .../discourse/views/create-account.js.es6 | 2 +- .../stylesheets/common/admin/admin_base.scss | 11 +-- app/assets/stylesheets/common/base/login.scss | 27 ++++++- app/assets/stylesheets/common/base/user.scss | 1 + app/assets/stylesheets/desktop/user.scss | 9 +++ .../admin/user_fields_controller.rb | 5 +- app/controllers/users_controller.rb | 40 +++++++++- app/models/site.rb | 4 + app/models/user.rb | 12 +++ app/models/user_field_serializer.rb | 2 +- .../admin_detailed_user_serializer.rb | 11 ++- app/serializers/site_serializer.rb | 1 + app/serializers/user_serializer.rb | 11 ++- app/services/user_updater.rb | 2 +- config/locales/client.en.yml | 5 ++ config/locales/server.en.yml | 1 + ...40929181930_add_editable_to_user_fields.rb | 5 ++ spec/controllers/users_controller_spec.rb | 73 +++++++++++++++++-- spec/fabricators/user_field.rb | 1 + .../javascripts/fixtures/site_fixtures.js.es6 | 3 + ...{qunit_helpers.js => qunit-helpers.js.es6} | 19 +++-- .../create-account-user-fields-test.js.es6 | 46 ++++++++++++ ...hint_all.js.erb => jshint-test.js.es6.erb} | 0 test/javascripts/models/topic-test.js.es6 | 8 +- test/javascripts/test_helper.js | 14 +++- 42 files changed, 476 insertions(+), 141 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/user-field.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/components/user-fields/confirm.hbs create mode 100644 app/assets/javascripts/discourse/templates/components/user-fields/text.hbs rename app/assets/javascripts/discourse/templates/modal/{create_account.hbs => create-account.hbs} (89%) create mode 100644 db/migrate/20140929181930_add_editable_to_user_fields.rb create mode 100644 test/javascripts/fixtures/site_fixtures.js.es6 rename test/javascripts/helpers/{qunit_helpers.js => qunit-helpers.js.es6} (70%) create mode 100644 test/javascripts/integration/create-account-user-fields-test.js.es6 rename test/javascripts/{jshint_all.js.erb => jshint-test.js.es6.erb} (100%) diff --git a/app/assets/javascripts/admin/controllers/admin-user-field-item.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-field-item.js.es6 index d958f7c1c6f..9a320ad12b7 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-field-item.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-field-item.js.es6 @@ -13,10 +13,12 @@ export default Ember.ObjectController.extend(BufferedContent, { save: function() { var self = this; - this.commitBuffer(); - this.get('model').save().then(function(res) { + var attrs = this.get('buffered').getProperties('name', 'field_type', 'editable'); + + this.get('model').save(attrs).then(function(res) { self.set('model.id', res.user_field.id); self.set('editing', false); + self.commitBuffer(); }).catch(function() { bootbox.alert(I18n.t('generic_error')); }); diff --git a/app/assets/javascripts/admin/controllers/admin-user-fields.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-fields.js.es6 index 77c159750c0..0d66804896b 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-fields.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-fields.js.es6 @@ -2,8 +2,10 @@ import UserField from 'admin/models/user-field'; export default Ember.ArrayController.extend({ fieldTypes: null, - createDisabled: Em.computed.gte('model.length', 3), + userFieldsName: function() { + return I18n.t('admin.user_fields.name'); + }.property(), _performDestroy: function(f, model) { return f.destroy().then(function() { @@ -13,10 +15,7 @@ export default Ember.ArrayController.extend({ actions: { createField: function() { - this.pushObject(UserField.create({ - field_type: 'text', - name: I18n.t('admin.user_fields.untitled') - })); + this.pushObject(UserField.create({ field_type: 'text' })); }, destroy: function(f) { diff --git a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 index ea3093ff6ce..7c9efafa038 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 @@ -1,14 +1,6 @@ import ObjectController from 'discourse/controllers/object'; 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, { editingTitle: false, originalPrimaryGroupId: null, @@ -23,6 +15,19 @@ export default ObjectController.extend(CanCheckEmails, { 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: { toggleTitleEdit: function() { this.toggleProperty('editingTitle'); diff --git a/app/assets/javascripts/admin/controllers/admin-user.js.es6 b/app/assets/javascripts/admin/controllers/admin-user.js.es6 index 85be90825ea..33f6459f8ea 100644 --- a/app/assets/javascripts/admin/controllers/admin-user.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user.js.es6 @@ -1,12 +1,3 @@ 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(); diff --git a/app/assets/javascripts/admin/models/user-field.js.es6 b/app/assets/javascripts/admin/models/user-field.js.es6 index 98f3da5c00e..56218d8d80f 100644 --- a/app/assets/javascripts/admin/models/user-field.js.es6 +++ b/app/assets/javascripts/admin/models/user-field.js.es6 @@ -17,17 +17,17 @@ var UserField = Ember.Object.extend({ }); }, - save: function() { + save: function(attrs) { var id = this.get('id'); if (!id) { return Discourse.ajax("/admin/customize/user_fields", { type: "POST", - data: { user_field: this.getProperties('name', 'field_type') } + data: { user_field: attrs } }); } else { return Discourse.ajax("/admin/customize/user_fields/" + id, { type: "PUT", - data: { user_field: this.getProperties('name', 'field_type') } + data: { user_field: attrs } }); } } diff --git a/app/assets/javascripts/admin/templates/customize.hbs b/app/assets/javascripts/admin/templates/customize.hbs index 190b852cd71..8ac29110e12 100644 --- a/app/assets/javascripts/admin/templates/customize.hbs +++ b/app/assets/javascripts/admin/templates/customize.hbs @@ -4,9 +4,7 @@
  • {{#link-to 'adminCustomize.colors'}}{{i18n admin.customize.colors.title}}{{/link-to}}
  • {{#link-to 'adminCustomize.css_html'}}{{i18n admin.customize.css_html.title}}{{/link-to}}
  • {{#link-to 'adminSiteText'}}{{i18n admin.site_text.title}}{{/link-to}}
  • - {{#if userFieldFeatureComplete}} -
  • {{#link-to 'adminUserFields'}}{{i18n admin.user_fields.title}}{{/link-to}}
  • - {{/if}} +
  • {{#link-to 'adminUserFields'}}{{i18n admin.user_fields.title}}{{/link-to}}
  • diff --git a/app/assets/javascripts/admin/templates/user-fields.hbs b/app/assets/javascripts/admin/templates/user-fields.hbs index 540c67b0b67..eba47013808 100644 --- a/app/assets/javascripts/admin/templates/user-fields.hbs +++ b/app/assets/javascripts/admin/templates/user-fields.hbs @@ -7,13 +7,14 @@ {{#each f in model itemController="admin-user-field-item" itemView="admin-user-field-item"}} {{#if f.editing}}
    - + {{input value=f.buffered.name class="user-field-name" placeholder=userFieldsName}}
    -
    +
    +
    @@ -21,11 +22,14 @@
    {{else}} +
    {{f.name}}
    +
    {{f.fieldName}}
    - {{f.name}} -
    -
    - {{f.fieldName}} + {{#if f.editable}} + {{i18n admin.user_fields.editable.enabled}} + {{else}} + {{i18n admin.user_fields.editable.disabled}} + {{/if}}
    diff --git a/app/assets/javascripts/admin/templates/user_index.hbs b/app/assets/javascripts/admin/templates/user_index.hbs index 7bac456429f..c9bf94c564a 100644 --- a/app/assets/javascripts/admin/templates/user_index.hbs +++ b/app/assets/javascripts/admin/templates/user_index.hbs @@ -85,22 +85,22 @@ {{#if currentUser.admin}} -
    -
    {{i18n admin.groups.title}}
    -
    - {{admin-group-selector selected=model.groups available=availableGroups}} +
    +
    {{i18n admin.groups.title}}
    +
    + {{admin-group-selector selected=model.groups available=availableGroups}} +
    +
    + {{#if custom_groups}} + {{i18n admin.groups.primary}} + {{combo-box content=custom_groups value=primary_group_id nameProperty="name" none="admin.groups.no_primary"}} + {{/if}} + {{#if primaryGroupDirty}} + + + {{/if}} +
    -
    - {{#if custom_groups}} - {{i18n admin.groups.primary}} - {{combo-box content=custom_groups value=primary_group_id nameProperty="name" none="admin.groups.no_primary"}} - {{/if}} - {{#if primaryGroupDirty}} - - - {{/if}} -
    -
    {{/if}}
    @@ -137,9 +137,25 @@
    {{/if}} - +{{#if userFields}} +
    + {{#each userFields}} +
    +
    {{name}}
    +
    + {{#if value}} + {{value}} + {{else}} + — + {{/if}} +
    +
    + {{/each}} +
    +{{/if}} +

    {{i18n admin.user.permissions}}

    diff --git a/app/assets/javascripts/discourse/components/user-field.js.es6 b/app/assets/javascripts/discourse/components/user-field.js.es6 new file mode 100644 index 00000000000..ab345644e39 --- /dev/null +++ b/app/assets/javascripts/discourse/components/user-field.js.es6 @@ -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') +}); diff --git a/app/assets/javascripts/discourse/controllers/create-account.js.es6 b/app/assets/javascripts/discourse/controllers/create-account.js.es6 index 01cab4acf75..56e8aadfa30 100644 --- a/app/assets/javascripts/discourse/controllers/create-account.js.es6 +++ b/app/assets/javascripts/discourse/controllers/create-account.js.es6 @@ -15,6 +15,7 @@ export default DiscourseController.extend(ModalFunctionality, { rejectedPasswords: Em.A([]), prefilledUsername: null, tosAccepted: false, + userFields: null, hasAuthOptions: Em.computed.notEmpty('authOptions'), canCreateLocal: Discourse.computed.setting('enable_local_logins'), @@ -22,6 +23,8 @@ export default DiscourseController.extend(ModalFunctionality, { maxUsernameLength: Discourse.computed.setting('max_username_length'), resetForm: function() { + + // We wrap the fields in a structure so we can assign a value this.setProperties({ accountName: '', accountEmail: '', @@ -31,10 +34,11 @@ export default DiscourseController.extend(ModalFunctionality, { globalNicknameExists: false, complete: false, formSubmitted: false, - rejectedEmails: Em.A([]), - rejectedPasswords: Em.A([]), - prefilledUsername: null + rejectedEmails: [], + rejectedPasswords: [], + prefilledUsername: null, }); + this._createUserFields(); }, submitDisabled: function() { @@ -47,8 +51,18 @@ export default DiscourseController.extend(ModalFunctionality, { if (this.get('emailValidation.failed')) return true; if (this.get('usernameValidation.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; - }.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() { return this.blank('authOptions.auth_provider'); @@ -337,20 +351,25 @@ export default DiscourseController.extend(ModalFunctionality, { }, 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); - var name = this.get('accountName'); - 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) { + return Discourse.User.createAccount(attrs).then(function(result) { if (result.success) { // Trigger the browser's password manager using the hidden static 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=password]').val(self.get('accountPassword')); + $hidden_login_form.find('input[name=username]').val(attrs.accountName); + $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.submit(); } else { @@ -359,7 +378,7 @@ export default DiscourseController.extend(ModalFunctionality, { self.get('rejectedEmails').pushObject(result.values.email); } 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); } @@ -371,5 +390,21 @@ export default DiscourseController.extend(ModalFunctionality, { 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') + }); diff --git a/app/assets/javascripts/discourse/controllers/preferences.js.es6 b/app/assets/javascripts/discourse/controllers/preferences.js.es6 index b70f843ebcc..b0bea6e09e4 100644 --- a/app/assets/javascripts/discourse/controllers/preferences.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences.js.es6 @@ -18,6 +18,17 @@ export default ObjectController.extend(CanCheckEmails, { 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'), deleteDisabled: Em.computed.or('saving', 'deleting', 'cannotDeleteAccount'), @@ -70,8 +81,20 @@ export default ObjectController.extend(CanCheckEmails, { var self = this; 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 - var model = this.get('model'); model.set('name', this.get('newNameInput')); return model.save().then(function() { // model was saved diff --git a/app/assets/javascripts/discourse/models/site.js b/app/assets/javascripts/discourse/models/site.js index fd6868613d4..c66481c9910 100644 --- a/app/assets/javascripts/discourse/models/site.js +++ b/app/assets/javascripts/discourse/models/site.js @@ -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; } }); diff --git a/app/assets/javascripts/discourse/models/user.js b/app/assets/javascripts/discourse/models/user.js index a854a14ddb5..68af6279940 100644 --- a/app/assets/javascripts/discourse/models/user.js +++ b/app/assets/javascripts/discourse/models/user.js @@ -188,8 +188,8 @@ Discourse.User = Discourse.Model.extend({ @returns {Promise} the result of the operation **/ save: function() { - var user = this; - var data = this.getProperties('auto_track_topics_after_msecs', + var self = this, + data = this.getProperties('auto_track_topics_after_msecs', 'bio_raw', 'website', 'location', @@ -206,10 +206,11 @@ Discourse.User = Discourse.Model.extend({ 'mailing_list_mode', 'enable_quoting', 'disable_jump_reply', - 'custom_fields'); + 'custom_fields', + 'user_fields'); - _.each(['muted','watched','tracked'], function(s){ - var cats = user.get(s + 'Categories').map(function(c){ return c.get('id')}); + ['muted','watched','tracked'].forEach(function(s){ + var cats = self.get(s + 'Categories').map(function(c){ return c.get('id')}); // HACK: denote lack of categories if(cats.length === 0) { cats = [-1]; } data[s + '_category_ids'] = cats; @@ -223,13 +224,10 @@ Discourse.User = Discourse.Model.extend({ data: data, type: 'PUT' }).then(function(data) { - user.set('bio_excerpt',data.user.bio_excerpt); + self.set('bio_excerpt',data.user.bio_excerpt); - _.each([ - 'enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon' - ], function(preference) { - Discourse.User.current().set(preference, user.get(preference)); - }); + var userProps = self.getProperties('enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon'); + Discourse.User.current().setProperties(userProps); }); }, @@ -542,26 +540,18 @@ Discourse.User.reopenClass(Discourse.Singleton, { }, /** - Creates a new account over POST - - @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 + Creates a new account **/ - createAccount: function(name, email, password, username, passwordConfirm, challenge) { + createAccount: function(attrs) { return Discourse.ajax("/users", { data: { - name: name, - email: email, - password: password, - username: username, - password_confirmation: passwordConfirm, - challenge: challenge + name: attrs.accountName, + email: attrs.accountEmail, + password: attrs.accountPassword, + username: attrs.accountUsername, + password_confirmation: attrs.accountPasswordConfirm, + challenge: attrs.accountChallenge, + user_fields: attrs.userFields }, type: 'POST' }); diff --git a/app/assets/javascripts/discourse/templates/components/user-fields/confirm.hbs b/app/assets/javascripts/discourse/templates/components/user-fields/confirm.hbs new file mode 100644 index 00000000000..a6164962ee2 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/user-fields/confirm.hbs @@ -0,0 +1,3 @@ + diff --git a/app/assets/javascripts/discourse/templates/components/user-fields/text.hbs b/app/assets/javascripts/discourse/templates/components/user-fields/text.hbs new file mode 100644 index 00000000000..10b4461de7c --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/user-fields/text.hbs @@ -0,0 +1,4 @@ + diff --git a/app/assets/javascripts/discourse/templates/modal/create_account.hbs b/app/assets/javascripts/discourse/templates/modal/create-account.hbs similarity index 89% rename from app/assets/javascripts/discourse/templates/modal/create_account.hbs rename to app/assets/javascripts/discourse/templates/modal/create-account.hbs index 5d6d75fbd28..ff5975a5e7a 100644 --- a/app/assets/javascripts/discourse/templates/modal/create_account.hbs +++ b/app/assets/javascripts/discourse/templates/modal/create-account.hbs @@ -62,14 +62,25 @@ {{/if}} - - + + {{input type="password" value=accountPasswordConfirm id="new-account-confirmation"}} {{input value=accountChallenge id="new-account-challenge"}} - + + + {{#if userFields}} +
    +

    {{i18n create_account.required_information}}

    + + {{#each userFields}} + {{user-field field=field value=value}} + {{/each}} +
    + {{/if}} +
    {{/if}} diff --git a/app/assets/javascripts/discourse/templates/user/preferences.hbs b/app/assets/javascripts/discourse/templates/user/preferences.hbs index b5a1c83f45f..5f40d4c54ef 100644 --- a/app/assets/javascripts/discourse/templates/user/preferences.hbs +++ b/app/assets/javascripts/discourse/templates/user/preferences.hbs @@ -178,6 +178,11 @@ {{#unless editHistoryVisible}} {{preference-checkbox labelKey="user.edit_history_public" checked=edit_history_public}} {{/unless}} + + {{#each userFields}} + {{user-field field=field value=value}} + {{/each}} + {{plugin-outlet "user_custom_preferences"}} diff --git a/app/assets/javascripts/discourse/views/create-account.js.es6 b/app/assets/javascripts/discourse/views/create-account.js.es6 index f3a269f8fb3..8de0a1864d2 100644 --- a/app/assets/javascripts/discourse/views/create-account.js.es6 +++ b/app/assets/javascripts/discourse/views/create-account.js.es6 @@ -1,5 +1,5 @@ export default Discourse.ModalBodyView.extend({ - templateName: 'modal/create_account', + templateName: 'modal/create-account', title: I18n.t('create_account.title'), classNames: ['create-account'], diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index c068dd08654..ba9aac6d8f6 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -1331,21 +1331,14 @@ tr.not-activated { border-bottom: 1px solid scale-color-diff(); .form-display { - width: 35%; + width: 25%; display: inline-block; float: left; } .form-element { float: left; - width: 35%; - margin-right: 10px; - label { - margin-right: 10px; - } - input, div.combobox { - margin-left: 10px; - } + width: 25%; } .controls { diff --git a/app/assets/stylesheets/common/base/login.scss b/app/assets/stylesheets/common/base/login.scss index 8e2065d00ef..04c71d75c95 100644 --- a/app/assets/stylesheets/common/base/login.scss +++ b/app/assets/stylesheets/common/base/login.scss @@ -10,4 +10,29 @@ .discourse-touch .caps-lock-warning { display: none; -} \ No newline at end of file +} + + +.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; + } +} diff --git a/app/assets/stylesheets/common/base/user.scss b/app/assets/stylesheets/common/base/user.scss index e2b9a56e3e6..d447e4d8723 100644 --- a/app/assets/stylesheets/common/base/user.scss +++ b/app/assets/stylesheets/common/base/user.scss @@ -63,3 +63,4 @@ margin: 5px 10px 5px 0; } } + diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index d7250476fd4..cf0ab75c321 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -426,4 +426,13 @@ .suspensions { background-color: #c22020; } + + .user-field { + margin-left: 160px; + margin-top: 10px; + input[type=text] { + width: 540px; + display: block; + } + } } diff --git a/app/controllers/admin/user_fields_controller.rb b/app/controllers/admin/user_fields_controller.rb index a10147d0be7..99a95c46a1e 100644 --- a/app/controllers/admin/user_fields_controller.rb +++ b/app/controllers/admin/user_fields_controller.rb @@ -1,7 +1,7 @@ class Admin::UserFieldsController < Admin::AdminController 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) end @@ -15,7 +15,8 @@ class Admin::UserFieldsController < Admin::AdminController field = UserField.where(id: params.require(:id)).first field.name = field_params[:name] field.field_type = field_params[:field_type] - field.save + field.editable = field_params[:editable] == "true" + field.save! render_serialized(field, UserFieldSerializer) end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index ea1298e377c..806315ca8f2 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -46,6 +46,16 @@ class UsersController < ApplicationController def update user = fetch_user_from_params 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| updater = UserUpdater.new(current_user, user) updater.update(params) @@ -162,18 +172,34 @@ class UsersController < ApplicationController end def create + params.permit(:user_fields) + unless SiteSetting.allow_new_registrations - render json: { success: false, message: I18n.t("login.new_registrations_disabled") } - return + return fail_with("login.new_registrations_disabled") end if params[:password] && params[:password].length > User.max_password_length - render json: { success: false, message: I18n.t("login.password_too_long") } - return + return fail_with("login.password_too_long") end 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) if !authentication.has_authenticator? && !SiteSetting.enable_local_logins @@ -194,6 +220,7 @@ class UsersController < ApplicationController authentication.finish activation.finish + render json: { success: true, active: user.active?, @@ -550,4 +577,9 @@ class UsersController < ApplicationController :active ).merge(ip_address: request.ip, registration_ip_address: request.ip) end + + def fail_with(key) + render json: { success: false, message: I18n.t(key) } + end + end diff --git a/app/models/site.rb b/app/models/site.rb index 6ae248eca62..84c69b807a6 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -33,6 +33,10 @@ class Site @groups ||= Group.order(:name).map { |g| {:id => g.id, :name => g.name}} end + def user_fields + UserField.all + end + def categories @categories ||= begin categories = Category diff --git a/app/models/user.rb b/app/models/user.rb index fd2bef99cc4..8ddbbd3344e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -657,6 +657,18 @@ class User < ActiveRecord::Base result.empty? ? I18n.t("user.no_accounts_associated") : result.join(", ") 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 def badge_grant diff --git a/app/models/user_field_serializer.rb b/app/models/user_field_serializer.rb index 316f3511765..e3df3422658 100644 --- a/app/models/user_field_serializer.rb +++ b/app/models/user_field_serializer.rb @@ -1,3 +1,3 @@ class UserFieldSerializer < ApplicationSerializer - attributes :id, :name, :field_type + attributes :id, :name, :field_type, :editable end diff --git a/app/serializers/admin_detailed_user_serializer.rb b/app/serializers/admin_detailed_user_serializer.rb index c762e1da187..3646ccd210e 100644 --- a/app/serializers/admin_detailed_user_serializer.rb +++ b/app/serializers/admin_detailed_user_serializer.rb @@ -18,7 +18,8 @@ class AdminDetailedUserSerializer < AdminUserSerializer :suspend_reason, :primary_group_id, :badge_count, - :warnings_received_count + :warnings_received_count, + :user_fields has_one :approved_by, serializer: BasicUserSerializer, embed: :objects has_one :api_key, serializer: ApiKeySerializer, embed: :objects @@ -74,4 +75,12 @@ class AdminDetailedUserSerializer < AdminUserSerializer object.has_trust_level?(TrustLevel[2]) end + def user_fields + object.user_fields + end + + def include_user_fields? + object.user_fields.present? + end + end diff --git a/app/serializers/site_serializer.rb b/app/serializers/site_serializer.rb index ca0355ddfc0..7d0dde56f09 100644 --- a/app/serializers/site_serializer.rb +++ b/app/serializers/site_serializer.rb @@ -16,6 +16,7 @@ class SiteSerializer < ApplicationSerializer has_many :topic_flag_types, serializer: TopicFlagTypeSerializer, embed: :objects has_many :trust_levels, embed: :objects has_many :archetypes, embed: :objects, serializer: ArchetypeSerializer + has_many :user_fields, embed: :objects, serialzer: UserFieldSerializer def default_archetype diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index f6f4f8f8057..38ae31e6b74 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -46,7 +46,8 @@ class UserSerializer < BasicUserSerializer :notification_count, :has_title_badges, :edit_history_public, - :custom_fields + :custom_fields, + :user_fields has_one :invited_by, embed: :object, serializer: BasicUserSerializer has_many :custom_groups, embed: :object, serializer: BasicGroupSerializer @@ -253,6 +254,14 @@ class UserSerializer < BasicUserSerializer can_edit && !SiteSetting.edit_history_visible_to_public end + def user_fields + object.user_fields + end + + def include_user_fields? + user_fields.present? + end + def custom_fields fields = nil diff --git a/app/services/user_updater.rb b/app/services/user_updater.rb index 0203637e8dd..4afc58047e8 100644 --- a/app/services/user_updater.rb +++ b/app/services/user_updater.rb @@ -68,7 +68,7 @@ class UserUpdater fields = attributes[:custom_fields] if fields.present? - user.custom_fields = fields + user.custom_fields = user.custom_fields.merge(fields) end User.transaction do diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 082a11851e1..f37374ae054 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -579,6 +579,7 @@ en: create_account: title: "Create New Account" failed: "Something went wrong, perhaps this email is already registered, try the forgot password link" + required_information: "Required Information" forgot_password: title: "Forgot Password" @@ -2006,6 +2007,10 @@ en: delete: "Delete" cancel: "Cancel" delete_confirm: "Are you sure you want to delete that user field?" + editable: + title: "Editable after signup?" + enabled: "editable" + disabled: "not editable" field_types: text: 'Text Field' diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 4d4ec306cf3..ea4402a96c1 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1103,6 +1103,7 @@ en: 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." password_too_long: "Passwords are limited to 200 characters." + missing_user_field: "You have not completed all the user fields" user: no_accounts_associated: "No accounts associated" diff --git a/db/migrate/20140929181930_add_editable_to_user_fields.rb b/db/migrate/20140929181930_add_editable_to_user_fields.rb new file mode 100644 index 00000000000..bfb4ee47462 --- /dev/null +++ b/db/migrate/20140929181930_add_editable_to_user_fields.rb @@ -0,0 +1,5 @@ +class AddEditableToUserFields < ActiveRecord::Migration + def change + add_column :user_fields, :editable, :boolean, default: false, null: false + end +end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 4af59dcc5fb..6ba16251564 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -531,7 +531,6 @@ describe UsersController do end context 'when an Exception is raised' do - [ ActiveRecord::StatementInvalid, RestClient::Forbidden ].each do |exception| before { User.any_instance.stubs(:save).raises(exception) } @@ -545,6 +544,40 @@ describe UsersController do 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 context '.username' do @@ -844,12 +877,10 @@ describe UsersController do context 'with authenticated user' do context 'with permission to update' do + let!(:user) { log_in(:user) } + 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} - expect(response).to be_success user.reload @@ -858,14 +889,42 @@ describe UsersController do expect(user.custom_fields['test']).to eq 'it' end - it 'returns user JSON' do - user = log_in + context "with user fields" do + 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 json = JSON.parse(response.body) expect(json['user']['id']).to eq user.id end + end context 'without permission to update' do diff --git a/spec/fabricators/user_field.rb b/spec/fabricators/user_field.rb index 3257d636f8e..16031101319 100644 --- a/spec/fabricators/user_field.rb +++ b/spec/fabricators/user_field.rb @@ -1,4 +1,5 @@ Fabricator(:user_field) do name { sequence(:name) {|i| "field_#{i}" } } field_type 'text' + editable true end diff --git a/test/javascripts/fixtures/site_fixtures.js.es6 b/test/javascripts/fixtures/site_fixtures.js.es6 new file mode 100644 index 00000000000..712b9a9df20 --- /dev/null +++ b/test/javascripts/fixtures/site_fixtures.js.es6 @@ -0,0 +1,3 @@ +export default { + "site.json": {"site":{"default_archetype":"regular","notification_types":{"mentioned":1,"replied":2,"quoted":3,"edited":4,"liked":5,"private_message":6,"invited_to_private_message":7,"invitee_accepted":8,"posted":9,"moved_post":10,"linked":11,"granted_badge":12},"post_types":{"regular":1,"moderator_action":2},"group_names":["admins","discourse","everyone","mcneel","moderators","newrelic","plugin_authors","sitepoint","staff","translators","trust_level_0","trust_level_1","trust_level_2","trust_level_3","trust_level_4","ubuntu"],"filters":["latest","unread","new","starred","read","posted"],"periods":["yearly","monthly","weekly","daily"],"top_menu_items":["latest","unread","new","starred","read","posted","category","categories","top"],"anonymous_top_menu_items":["latest","top","categories","category","categories","top"],"uncategorized_category_id":17,"is_readonly":false,"categories":[{"id":3,"name":"meta","color":"aaa","text_color":"FFFFFF","slug":"meta","topic_count":122,"post_count":1023,"description":"Discussion about meta.discourse.org itself, the organization of this forum about Discourse, how it works, and how we can improve this site.","topic_url":"/t/category-definition-for-meta/24","read_restricted":false,"permission":null,"notification_level":null,"logo_url":null,"background_url":null},{"id":10,"name":"howto","color":"76923C","text_color":"FFFFFF","slug":"howto","topic_count":72,"post_count":1022,"description":"Tutorial topics that describe how to set up, configure, or install Discourse using a specific platform or environment. Topics in this category may only be created by trust level 2 and up. ","topic_url":"/t/category-definition-for-howto/2629","read_restricted":false,"permission":null,"notification_level":null,"logo_url":null,"background_url":null},{"id":26,"name":"spec","color":"33B0B0","text_color":"FFFFFF","slug":"spec","topic_count":20,"post_count":278,"description":"My idea here is to have mini specs for features we would like built but have no bandwidth to build","topic_url":"/t/about-the-spec-category/13965","read_restricted":false,"permission":null,"parent_category_id":2,"notification_level":null,"logo_url":null,"background_url":null},{"id":7,"name":"dev","color":"000","text_color":"FFFFFF","slug":"dev","topic_count":481,"post_count":3575,"description":"This category is for topics related to hacking on Discourse: submitting pull requests, configuring development environments, coding conventions, and so forth.","topic_url":"/t/category-definition-for-dev/1026","read_restricted":false,"permission":null,"notification_level":null,"logo_url":null,"background_url":null},{"id":6,"name":"support","color":"b99","text_color":"FFFFFF","slug":"support","topic_count":1603,"post_count":11075,"description":"Support on configuring, using, and installing Discourse. Not for software development related topics, but for admins and end users configuring and using Discourse.","topic_url":"/t/category-definition-for-support/389","read_restricted":false,"permission":null,"notification_level":null,"logo_url":null,"background_url":null},{"id":24,"name":"sso","color":"92278F","text_color":"FFFFFF","slug":"sso","topic_count":13,"post_count":53,"description":"Only include actual maintained SSO (single sign on) implementations in this category. See the official documentation on Discourse's SSO support.","topic_url":"/t/about-the-sso-category/13110","read_restricted":false,"permission":null,"parent_category_id":5,"notification_level":null,"logo_url":null,"background_url":null},{"id":28,"name":"hack night","color":"B3B5B4","text_color":"FFFFFF","slug":"hack-night","topic_count":8,"post_count":33,"description":"This is a special, temporary category to organize work on the Discourse Hack Night in Toronto. ","topic_url":"/t/about-the-hack-night-category/17878","read_restricted":false,"permission":null,"parent_category_id":7,"notification_level":null,"logo_url":null,"background_url":null},{"id":27,"name":"translations","color":"808281","text_color":"FFFFFF","slug":"translations","topic_count":95,"post_count":827,"description":"This category is for discussion about localizing Discourse.","topic_url":"/t/about-the-translations-category/14549","read_restricted":false,"permission":null,"parent_category_id":7,"notification_level":null,"logo_url":null,"background_url":null},{"id":4,"name":"faq","color":"33b","text_color":"FFFFFF","slug":"faq","topic_count":48,"post_count":501,"description":"Topics that come up very often when discussing Discourse will eventually be classified into this Frequently Asked Questions category. Should only be added to popular topics.","topic_url":"/t/category-definition-for-faq/25","read_restricted":false,"permission":null,"notification_level":null,"logo_url":null,"background_url":null},{"id":14,"name":"marketplace","color":"8C6238","text_color":"FFFFFF","slug":"marketplace","topic_count":66,"post_count":361,"description":"About commercial Discourse related stuff: jobs or paid gigs, plugins, themes, hosting, etc.","topic_url":"/t/category-definition-for-marketplace/5425","read_restricted":false,"permission":null,"notification_level":null,"logo_url":null,"background_url":null},{"id":12,"name":"discourse hub","color":"b2c79f","text_color":"FFFFFF","slug":"discourse-hub","topic_count":10,"post_count":164,"description":"Topics about current or future Discourse Hub functionality at discourse.org including nickname registration, global user pages, and the site directory.","topic_url":"/t/category-definition-for-discourse-hub/3038","read_restricted":false,"permission":null,"notification_level":null,"logo_url":null,"background_url":null},{"id":13,"name":"blog","color":"ED207B","text_color":"FFFFFF","slug":"blog","topic_count":22,"post_count":390,"description":"Discussion topics generated from the official Discourse Blog. These topics are linked from the bottom of each blog entry where the blog comments would normally be.","topic_url":"/t/category-definition-for-blog/5250","read_restricted":false,"permission":null,"notification_level":null,"logo_url":null,"background_url":null},{"id":5,"name":"extensibility","color":"FE8432","text_color":"FFFFFF","slug":"extensibility","topic_count":226,"post_count":1874,"description":"Topics about extending the functionality of Discourse with plugins, themes, add-ons, or other mechanisms for extensibility. ","topic_url":"/t/about-the-extensibility-category/28","read_restricted":false,"permission":null,"notification_level":null,"logo_url":null,"background_url":null},{"id":11,"name":"login","color":"edb400","text_color":"FFFFFF","slug":"login","topic_count":48,"post_count":357,"description":"Topics about logging in to Discourse, using any standard third party provider (Twitter, Facebook, Google), traditional username and password, or with a custom plugin.","topic_url":"/t/category-definition-for-login/2828","read_restricted":false,"permission":null,"notification_level":null,"logo_url":null,"background_url":null},{"id":22,"name":"plugin","color":"d47711","text_color":"FFFFFF","slug":"plugin","topic_count":40,"post_count":466,"description":"One post per plugin! Only plugin owners should post here. ","topic_url":"/t/about-the-plugin-category/12648","read_restricted":false,"permission":null,"parent_category_id":5,"notification_level":null,"logo_url":null,"background_url":null},{"id":1,"name":"bug","color":"e9dd00","text_color":"000000","slug":"bug","topic_count":1469,"post_count":9295,"description":"A bug report means something is broken, preventing normal/typical use of Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please.","topic_url":"/t/category-definition-for-bug/2","read_restricted":false,"permission":null,"notification_level":null,"logo_url":null,"background_url":null},{"id":17,"name":"uncategorized","color":"AB9364","text_color":"FFFFFF","slug":"uncategorized","topic_count":342,"post_count":3090,"description":"Topics that don't need a category, or don't fit into any other existing category.","topic_url":null,"read_restricted":false,"permission":null,"notification_level":null,"logo_url":"","background_url":""},{"id":21,"name":"wordpress","color":"1E8CBE","text_color":"FFFFFF","slug":"wordpress","topic_count":26,"post_count":135,"description":"Support for the official Discourse WordPress plugin at https://github.com/discourse/wp-discourse","topic_url":"/t/category-definition-for-wordpress/12282","read_restricted":false,"permission":null,"parent_category_id":6,"notification_level":null,"logo_url":null,"background_url":null},{"id":8,"name":"hosting","color":"74CCED","text_color":"FFFFFF","slug":"hosting","topic_count":100,"post_count":917,"description":"Topics about hosting Discourse, either on your own servers, in the cloud, or with specific hosting services.","topic_url":"/t/category-definition-for-hosting/2626","read_restricted":false,"permission":null,"notification_level":null,"logo_url":null,"background_url":null},{"id":9,"name":"ux","color":"5F497A","text_color":"FFFFFF","slug":"ux","topic_count":452,"post_count":4472,"description":"Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.","topic_url":"/t/category-definition-for-ux/2628","read_restricted":false,"permission":null,"notification_level":null,"logo_url":null,"background_url":null},{"id":2,"name":"feature","color":"0E76BD","text_color":"FFFFFF","slug":"feature","topic_count":1367,"post_count":11942,"description":"Discussion about features or potential features of Discourse: how they work, why they work, etc.","topic_url":"/t/category-definition-for-feature/11","read_restricted":false,"permission":null,"notification_level":null,"logo_url":null,"background_url":null}],"post_action_types":[{"name_key":"bookmark","name":"Bookmark","description":"Bookmark this post","long_form":"bookmarked this post","is_flag":false,"icon":null,"id":1,"is_custom_flag":false},{"name_key":"like","name":"Like","description":"Like this post","long_form":"liked this","is_flag":false,"icon":"heart","id":2,"is_custom_flag":false},{"name_key":"off_topic","name":"Off-Topic","description":"This post is radically off-topic in the current topic, and should probably be moved. If this is a topic, perhaps it does not belong here.","long_form":"flagged this as off-topic","is_flag":true,"icon":null,"id":3,"is_custom_flag":false},{"name_key":"inappropriate","name":"Inappropriate","description":"This post contains content that a reasonable person would consider offensive, abusive, or a violation of our community guidelines.","long_form":"flagged this as inappropriate","is_flag":true,"icon":null,"id":4,"is_custom_flag":false},{"name_key":"vote","name":"Vote","description":"Vote for this post","long_form":"voted for this post","is_flag":false,"icon":null,"id":5,"is_custom_flag":false},{"name_key":"spam","name":"Spam","description":"This post is an advertisement. It is not useful or relevant to the current topic, but promotional in nature.","long_form":"flagged this as spam","is_flag":true,"icon":null,"id":8,"is_custom_flag":false},{"name_key":"notify_user","name":"Notify {{username}}","description":"This post contains something I want to talk to this person directly and privately about. Does not cast a flag.","long_form":"notified user","is_flag":true,"icon":null,"id":6,"is_custom_flag":true},{"name_key":"notify_moderators","name":"Notify moderators","description":"This post requires general moderator attention based on the guidelines, TOS, or for another reason not listed above.","long_form":"notified moderators","is_flag":true,"icon":null,"id":7,"is_custom_flag":true}],"topic_flag_types":[{"name_key":"inappropriate","name":"Inappropriate","description":"This topic contains content that a reasonable person would consider offensive, abusive, or a violation of our community guidelines.","long_form":"flagged this as inappropriate","is_flag":true,"icon":null,"id":4,"is_custom_flag":false},{"name_key":"spam","name":"Spam","description":"This topic is an advertisement. It is not useful or relevant to this site, but promotional in nature.","long_form":"flagged this as spam","is_flag":true,"icon":null,"id":8,"is_custom_flag":false},{"name_key":"notify_moderators","name":"Notify moderators","description":"This topic requires general moderator attention based on the guidelines, TOS, or for another reason not listed above.","long_form":"notified moderators","is_flag":true,"icon":null,"id":7,"is_custom_flag":true}],"trust_levels":[{"id":0,"name":"new user"},{"id":1,"name":"basic user"},{"id":2,"name":"member"},{"id":3,"name":"regular"},{"id":4,"name":"leader"}],"archetypes":[{"id":"regular","name":"Regular Topic","options":[]},{"id":"banner","name":"translation missing: en.archetypes.banner.title","options":[]}]}} +}; diff --git a/test/javascripts/helpers/qunit_helpers.js b/test/javascripts/helpers/qunit-helpers.js.es6 similarity index 70% rename from test/javascripts/helpers/qunit_helpers.js rename to test/javascripts/helpers/qunit-helpers.js.es6 index b6450807431..3aa4b2fcc02 100644 --- a/test/javascripts/helpers/qunit_helpers.js +++ b/test/javascripts/helpers/qunit-helpers.js.es6 @@ -1,9 +1,13 @@ /* 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, { setup: function() { Ember.run(Discourse, Discourse.advanceReadiness); + + var siteJson = siteFixtures['site.json'].site; if (options) { if (options.setup) { options.setup.call(this); @@ -16,7 +20,12 @@ function integration(name, options) { if (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(); }, @@ -30,13 +39,13 @@ function integration(name, options) { }); } -function controllerFor(controller, model) { +export function controllerFor(controller, model) { controller = Discourse.__container__.lookup('controller:' + controller); if (model) { controller.set('model', model ); } return controller; } -function asyncTestDiscourse(text, func) { +export function asyncTestDiscourse(text, func) { asyncTest(text, function () { var self = this; Ember.run(function () { @@ -45,7 +54,7 @@ function asyncTestDiscourse(text, func) { }); } -function fixture(selector) { +export function fixture(selector) { if (selector) { return $("#qunit-fixture").find(selector); } diff --git a/test/javascripts/integration/create-account-user-fields-test.js.es6 b/test/javascripts/integration/create-account-user-fields-test.js.es6 new file mode 100644 index 00000000000..939ea8fcc04 --- /dev/null +++ b/test/javascripts/integration/create-account-user-fields-test.js.es6 @@ -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'); + }); + +}); diff --git a/test/javascripts/jshint_all.js.erb b/test/javascripts/jshint-test.js.es6.erb similarity index 100% rename from test/javascripts/jshint_all.js.erb rename to test/javascripts/jshint-test.js.es6.erb diff --git a/test/javascripts/models/topic-test.js.es6 b/test/javascripts/models/topic-test.js.es6 index 1ab2d25963f..e557cf7c1b7 100644 --- a/test/javascripts/models/topic-test.js.es6 +++ b/test/javascripts/models/topic-test.js.es6 @@ -20,16 +20,18 @@ test('has a postStream', function() { equal(postStream.get('topic'), topic, "the postStream has a reference back to the topic"); }); -var category = _.first(Discourse.Category.list()); test('category relationship', function() { // 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); }); test("updateFromJson", function() { - var topic = Discourse.Topic.create({id: 1234}); + var topic = Discourse.Topic.create({id: 1234}), + category = Discourse.Category.list()[0]; topic.updateFromJson({ post_stream: [1,2,3], diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js index 542ac57178d..863d76e452d 100644 --- a/test/javascripts/test_helper.js +++ b/test/javascripts/test_helper.js @@ -42,7 +42,7 @@ //= require sinon-qunit-1.0.0 //= require jshint -//= require helpers/qunit_helpers +//= require helpers/qunit-helpers //= require helpers/assertions //= require helpers/init-ember-qunit @@ -50,7 +50,6 @@ //= require_tree ./lib //= require_tree . //= require_self -//= require jshint_all // sinon settings sinon.config = { @@ -87,6 +86,7 @@ if (window.Logster) { var origDebounce = Ember.run.debounce, createPretendServer = require('helpers/create-pretender', null, null, false).default, + fixtures = require('fixtures/site_fixtures', null, null, false).default, server; QUnit.testStart(function(ctx) { @@ -97,6 +97,7 @@ QUnit.testStart(function(ctx) { Discourse.BaseUri = "/"; Discourse.BaseUrl = ""; Discourse.User.resetCurrent(); + Discourse.Site.resetCurrent(Discourse.Site.create(fixtures['site.json'].site)); PreloadStore.reset(); window.sandbox = sinon.sandbox.create(); @@ -121,6 +122,15 @@ QUnit.testDone(function() { }); // 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) { if ((/\-test/).test(entry)) { require(entry, null, null, true);