GET /auth/name/<name> now serves possible free names

anonymous users are now warned if their new name is already chosen

User Settings is now without auto-save

Upon name conflict, a user will be suggested a new name which is then submitted if the user chooses to save after all.

Refactored conflicted name checking so it can be used in more places

Signup form now has an optional name field

Covered extra case where the debounced check happened too late. Support for submitting on enter.

Worked in scott's comments and got tests working again
This commit is contained in:
Ruben Vereecken 2014-07-10 20:50:16 +02:00
parent ae4a5eb460
commit e748417007
11 changed files with 212 additions and 25 deletions

View file

@ -19,8 +19,10 @@ module.exports = class CocoRouter extends Backbone.Router
# Direct links
'test': go('TestView')
'test/*subpath': go('TestView')
'demo': go('DemoView')
'demo/*subpath': go('DemoView')
'play/ladder/:levelID': go('play/ladder/ladder_view')
'play/ladder': go('play/ladder_home')

View file

@ -9,12 +9,12 @@ module.exports.formToObject = (el) ->
obj
module.exports.applyErrorsToForm = (el, errors) ->
module.exports.applyErrorsToForm = (el, errors, warning=false) ->
errors = [errors] if not $.isArray(errors)
missingErrors = []
for error in errors
if error.dataPath
prop = error.dataPath[1..]
console.log prop
message = error.message
else
@ -23,16 +23,28 @@ module.exports.applyErrorsToForm = (el, errors) ->
message = error.message if error.formatted
prop = error.property
input = $("[name='#{prop}']", el)
if not input.length
missingErrors.push(error)
continue
formGroup = input.closest('.form-group')
formGroup.addClass 'has-error'
formGroup.append($("<span class='help-block error-help-block'>#{message}</span>"))
return missingErrors
setErrorToProperty el, prop, message, warning
module.exports.setErrorToField = setErrorToField = (el, message, warning=false) ->
formGroup = el.closest('.form-group')
unless formGroup.length
return console.error "#{el} did not contain a form group"
kind = if warning then 'warning' else 'error'
formGroup.addClass "has-#{kind}"
formGroup.append $("<span class='help-block #{kind}-help-block'>#{message}</span>")
module.exports.setErrorToProperty = setErrorToProperty = (el, property, message, warning=false) ->
input = $("[name='#{property}']", el)
unless input.length
return console.error "#{property} not found in #{el}"
setErrorToField input, message, warning
module.exports.clearFormAlerts = (el) ->
$('.has-error', el).removeClass('has-error')
$('.has-warning', el).removeClass('has-warning')
$('.alert.alert-danger', el).remove()
$('.alert.alert-warning', el).remove()
el.find('.help-block.error-help-block').remove()
el.find('.help-block.warning-help-block').remove()

View file

@ -69,6 +69,13 @@ module.exports = class User extends CocoModel
cache[idOrSlug] = user
user
@getUnconflictedName: (name, done) ->
$.ajax "/auth/name/#{name}",
success: (data) -> done data.name
statusCode: 409: (data) ->
response = JSON.parse data.responseText
done response.name
getEnabledEmails: ->
@migrateEmails()
emails = _.clone(@get('emails')) or {}

View file

@ -9,7 +9,7 @@ block content
else
#save-button-container
button.btn#save-button.disabled.secret(data-i18n="account_settings.autosave") Changes Save Automatically
button.btn#save-button.disabled(data-i18n="general.save" disabled="true") No Changes
ul.nav.nav-pills#settings-tabs
li
@ -30,16 +30,19 @@ block content
#general-pane.tab-pane
p
.form
- var name = me.get('name') || '';
- var email = me.get('email');
- var admin = me.get('permissions').indexOf('admin') != -1;
.form-group
label.control-label(for="name", data-i18n="general.name") Name
input#name.form-control(name="name", type="text", value="#{me.get('name') || ''}")
input#name.form-control(name="name", type="text", value="#{name}")
.form-group
label.control-label(for="email", data-i18n="general.email") Email
input#email.form-control(name="email", type="text", value="#{me.get('email')}")
input#email.form-control(name="email", type="text", value="#{email}")
if !isProduction
.form-group.checkbox
label(for="admin", data-i18n="account_settings.admin") Admin
input#admin(name="admin", type="checkbox", checked=me.get('permissions').indexOf('admin') != -1)
input#admin(name="admin", type="checkbox", checked=admin)
#picture-pane.tab-pane

View file

@ -34,6 +34,12 @@ block modal-body-content
input#password.input-large.form-control(name="password", type="password", value=formValues.password)
if mode === 'signup'
.form-group
label.control-label(for="name", data-i18n="general.name") Name
if me.get('name')
input#name.input-large.form-control(name="name", type="text", value="#{me.get('name')}")
else
input#name.input-large.form-control(name="name", type="text", value="", placeholder="Anoner")
.form-group.checkbox
label.control-label(for="subscribe")
input#subscribe(name="subscribe", type="checkbox", checked='checked')

View file

@ -11,17 +11,73 @@ JobProfileView = require './job_profile_view'
module.exports = class SettingsView extends View
id: 'account-settings-view'
template: template
changedFields: [] # DOM input fields
events:
'click #save-button': 'save'
'change #settings-panes input': 'save'
'change #settings-panes input:checkbox': (e) -> @trigger 'checkboxToggled', e
'keyup #settings-panes input:text, #settings-panes input:password': (e) -> @trigger 'inputChanged', e
'keyup #name': 'onNameChange'
'click #toggle-all-button': 'toggleEmailSubscriptions'
'keypress #settings-panes': 'onKeyPress'
constructor: (options) ->
@save = _.debounce(@save, 200)
@onNameChange = _.debounce @checkNameExists, 500
super options
return unless me
@listenTo(me, 'invalid', (errors) -> forms.applyErrorsToForm(@$el, me.validationError))
@on 'checkboxToggled', @onToggle
@on 'checkboxToggled', @onInputChanged
@on 'inputChanged', @onInputChanged
@on 'enterPressed', @onEnter
onInputChanged: (e) ->
return @enableSaveButton() unless e?.currentTarget
that = e.currentTarget
$that = $(that)
savedValue = $that.data 'saved-value'
currentValue = $that.val()
if savedValue isnt currentValue
@changedFields.push that unless that in @changedFields
@enableSaveButton()
else
_.pull @changedFields, that
@disableSaveButton() if _.isEmpty @changedFields
onToggle: (e) ->
$that = $(e.currentTarget)
$that.val $that[0].checked
onEnter: ->
@save()
onKeyPress: (e) ->
@trigger 'enterPressed', e if e.which is 13
enableSaveButton: ->
$('#save-button', @$el).removeClass 'disabled'
$('#save-button', @$el).removeClass 'btn-danger'
$('#save-button', @$el).removeAttr 'disabled'
$('#save-button', @$el).text 'Save'
disableSaveButton: ->
$('#save-button', @$el).addClass 'disabled'
$('#save-button', @$el).removeClass 'btn-danger'
$('#save-button', @$el).attr 'disabled', "true"
$('#save-button', @$el).text 'No Changes'
checkNameExists: =>
name = $('#name', @$el).val()
return if name is me.get 'name'
User.getUnconflictedName name, (newName) =>
forms.clearFormAlerts(@$el)
if name is newName
@suggestedName = undefined
else
@suggestedName = newName
forms.setErrorToProperty @$el, 'name', "That name is taken! How about #{newName}?", true
afterRender: ->
super()
@ -50,6 +106,7 @@ module.exports = class SettingsView extends View
super()
if me.get('anonymous')
@openModalView new AuthModalView()
@updateSavedValues()
chooseTab: (category) ->
id = "##{category}-pane"
@ -98,7 +155,7 @@ module.exports = class SettingsView extends View
@$el.find('.gravatar-fallback').toggle not me.get 'photoURL'
onPictureChanged: (e) =>
@trigger 'change'
@trigger 'inputChanged', e
@$el.find('.gravatar-fallback').toggle not me.get 'photoURL'
save: (e) ->
@ -122,7 +179,9 @@ module.exports = class SettingsView extends View
errors = JSON.parse(res.responseText)
forms.applyErrorsToForm(@$el, errors)
save.text($.i18n.t('account_settings.error_saving', defaultValue: 'Error Saving')).removeClass('btn-success').addClass('btn-danger', 500)
res.success (model, response, options) ->
res.success (model, response, options) =>
@changedFields = []
@updateSavedValues()
save.text($.i18n.t('account_settings.saved', defaultValue: 'Changes Saved')).removeClass('btn-success', 500)
grabData: ->
@ -142,6 +201,7 @@ module.exports = class SettingsView extends View
me.set('password', password1)
grabOtherData: ->
$('#name', @$el).val @suggestedName if @suggestedName
me.set 'name', $('#name', @$el).val()
me.set 'email', $('#email', @$el).val()
for emailName, enabled of @getSubscriptions()
@ -162,3 +222,9 @@ module.exports = class SettingsView extends View
if updated
jobProfile.updated = (new Date()).toISOString()
me.set 'jobProfile', jobProfile
updateSavedValues: ->
$('#settings-panes input:text').each ->
$(@).data 'saved-value', $(@).val()
$('#settings-panes input:checkbox').each ->
$(@).data 'saved-value', JSON.stringify $(@)[0].checked

View file

@ -15,11 +15,16 @@ module.exports = class AuthModalView extends View
'click #switch-to-signup-button': 'onSignupInstead'
'click #signup-confirm-age': 'checkAge'
'submit': 'onSubmitForm' # handles both submit buttons
'keyup #name': 'onNameChange'
subscriptions:
'server-error': 'onServerError'
'logging-in-with-facebook': 'onLoggingInWithFacebook'
constructor: (options) ->
@onNameChange = _.debounce @checkNameExists, 500
super options
getRenderData: ->
c = super()
c.showRequiredError = @options.showRequiredError
@ -31,6 +36,7 @@ module.exports = class AuthModalView extends View
c.mode = @mode
c.formValues = @previousFormInputs or {}
c.onEmployersPage = Backbone.history.fragment is "employers"
c.me = me
c
afterInsert: ->
@ -64,6 +70,8 @@ module.exports = class AuthModalView extends View
userObject = forms.formToObject @$el
delete userObject.subscribe
delete userObject['confirm-age']
delete userObject.name if userObject.name is ''
userObject.name = @suggestedName if @suggestedName
for key, val of me.attributes when key in ['preferredLanguage', 'testGroupNumber', 'dateCreated', 'wizardColor1', 'name', 'music', 'volume', 'emails']
userObject[key] ?= val
subscribe = @$el.find('#signup-subscribe').prop('checked')
@ -82,3 +90,14 @@ module.exports = class AuthModalView extends View
onServerError: (e) -> # TODO: work error handling into a separate forms system
@disableModalInProgress(@$el)
checkNameExists: =>
name = $('#name', @$el).val()
return forms.clearFormAlerts(@$el) if name is ''
User.getUnconflictedName name, (newName) =>
forms.clearFormAlerts(@$el)
if name is newName
@suggestedName = undefined
else
@suggestedName = newName
forms.setErrorToProperty @$el, 'name', "That name is taken! How about #{newName}?", true

View file

@ -4,20 +4,22 @@ WizardSprite = require 'lib/surface/WizardSprite'
ThangType = require 'models/ThangType'
{me} = require 'lib/auth'
forms = require 'lib/forms'
User = require 'models/User'
module.exports = class WizardSettingsModal extends View
id: 'wizard-settings-modal'
template: template
closesOnClickOutside: false
events:
'keyup #wizard-settings-name': -> @trigger 'nameChanged'
'click #wizard-settings-done': 'onWizardSettingsDone'
constructor: (options) ->
@onNameChange = _.debounce(@checkNameExists, 500)
@on 'nameChanged', @onNameChange
super options
events:
'keyup #wizard-settings-name': 'onNameChange'
'click #wizard-settings-done': 'onWizardSettingsDone'
afterRender: ->
WizardSettingsView = require 'views/account/wizard_settings_view'
view = new WizardSettingsView()
@ -27,10 +29,10 @@ module.exports = class WizardSettingsModal extends View
checkNameExists: =>
forms.clearFormAlerts(@$el)
name = $('#wizard-settings-name').val()
success = (id) =>
User.getUnconflictedName name, (newName) =>
forms.clearFormAlerts(@$el)
forms.applyErrorsToForm(@$el, {property: 'name', message: 'is already taken'}) if id and id isnt me.id
$.ajax("/db/user/#{name}/nameToID", {success: success})
if name isnt newName
forms.setErrorToProperty @$el, 'name', 'This name is already taken so you won\'t be able to keep it.', true
onWizardSettingsDone: ->
me.set('name', $('#wizard-settings-name').val())

View file

@ -157,13 +157,27 @@ module.exports.setup = (app) ->
res.send msg + '<p><a href="/account/settings">Account settings</a></p>'
res.end()
app.get '/auth/name*', (req, res) ->
parts = req.path.split '/'
originalName = decodeURI parts[3]
return errors.badInput res, 'No name provided.' unless parts.length > 3 and originalName? and originalName isnt ''
return errors.notFound res if parts.length isnt 4
User.unconflictName originalName, (err, name) ->
return errors.serverError res, err if err
response = name: name
if originalName is name
res.send 200, response
else
errors.conflict res, response
module.exports.loginUser = loginUser = (req, res, user, send=true, next=null) ->
user.save((err) ->
return errors.serverError res, err if err?
req.logIn(user, (err) ->
return errors.serverError res, err if err?
return res.send user if send
return res.send(user) and res.end() if send
next() if next
)
)

File diff suppressed because one or more lines are too long

View file

@ -152,3 +152,27 @@ describe '/auth/unsubscribe', ->
expect(user.get('emails').recruitNotes.enabled).toBe(false)
expect(user.isEmailSubscriptionEnabled('generalNews')).toBeTruthy()
done()
describe '/auth/name', ->
url = '/auth/name'
it 'must provide a name to check with', (done) ->
request.get {url: getURL(url + '/'), json: {}}, (err, response) ->
expect(err).toBeNull()
expect(response.statusCode).toBe 422
done()
it 'can GET a non-conflicting name', (done) ->
request.get {url: getURL(url + '/Gandalf'), json: {}}, (err, response) ->
expect(err).toBeNull()
expect(response.statusCode).toBe 200
expect(response.body.name).toBe 'Gandalf'
done()
it 'can GET a new name in case of conflict', (done) ->
request.get {url: getURL(url + '/joe'), json: {}}, (err, response) ->
expect(err).toBeNull()
expect(response.statusCode).toBe 409
expect(response.body.name).not.toBe 'joe'
expect(response.body.name.length).toBe 4 # 'joe' and a random number
done()