mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-30 10:56:53 -05:00
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:
parent
ae4a5eb460
commit
e748417007
11 changed files with 212 additions and 25 deletions
|
@ -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')
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
|
|
32
test/demo/views/modals/WizardSettingsModal.demo.coffee
Normal file
32
test/demo/views/modals/WizardSettingsModal.demo.coffee
Normal file
File diff suppressed because one or more lines are too long
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue