Merge branch 'master' into achievements

This commit is contained in:
Ruben Vereecken 2014-07-15 16:16:46 +02:00
commit 0288786098
23 changed files with 269 additions and 51 deletions

View file

@ -25,8 +25,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

@ -242,7 +242,17 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
args = JSON.parse(event[4...])
pos = @options.camera.worldToSurface {x: args[0], y: args[1]}
circle = new createjs.Shape()
circle.graphics.beginFill(args[3]).drawCircle(0, 0, args[2]*Camera.PPM)
radius = args[2] * Camera.PPM
if args.length is 4
circle.graphics.beginFill(args[3]).drawCircle(0, 0, radius)
else
startAngle = args[4]
endAngle = args[5]
circle.graphics.beginFill(args[3])
.lineTo(0, 0)
.lineTo(radius * Math.cos(startAngle), radius * Math.sin(endAngle))
.arc(0, 0, radius, startAngle, endAngle)
.lineTo(0, 0)
circle.x = pos.x
circle.y = pos.y
circle.scaleY = @options.camera.y2x * 0.7

View file

@ -66,10 +66,10 @@ module.exports = class Mark extends CocoClass
@mark = new createjs.Container()
@mark.mouseChildren = false
style = @sprite.thang.drawsBoundsStyle
@drawsBoundsIndex = @sprite.thang.drawsBoundsIndex
return if style is 'corner-text' and @sprite.thang.world.age is 0
# Confusingly make some semi-random colors that'll be consistent based on the drawsBoundsIndex
@drawsBoundsIndex = @sprite.thang.drawsBoundsIndex
colors = (128 + Math.floor(('0.'+Math.sin(3 * @drawsBoundsIndex + i).toString().substr(6)) * 128) for i in [1 ... 4])
color = "rgba(#{colors[0]}, #{colors[1]}, #{colors[2]}, 0.5)"
[w, h] = [@sprite.thang.width * Camera.PPM, @sprite.thang.height * Camera.PPM * @camera.y2x]

View file

@ -144,7 +144,7 @@ module.exports = class ThangState
# We make sure the array keys won't collide with any string keys by using some unprintable characters.
stringPieces = ['\x1D'] # Group Separator
for element in value
if element and element.isThang
if element and element.id # Was checking element.isThang, but we can't store non-strings anyway
element = element.id
stringPieces.push element, '\x1E' # Record Separator(s)
value = stringPieces.join('')

View file

@ -79,6 +79,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

@ -3,16 +3,23 @@
.row
.index-column, .documentation-column
overflow-y: scroll
overflow-x: hidden
min-height: 600px
> ul
padding: 0px 20px 20px 20px
.doc-name
color: rgb(139, 69, 19)
.documentation-column
.specialList
list-style-type: none
.doc-description
list-style-type: none

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

@ -6,18 +6,25 @@ block content
.col-xs-3.index-column.nano
ul.nav.nav-list.list-group.nano-content
for component in components
li= component.get('name')
ul
each doc in component.attributes.propertyDocumentation
a(href="##{component.get('name')}#{doc.name}")
li
| #{doc.name}
a.doc-name(href="##{component.get('name')}")
li= component.get('name')
ul
each doc in component.attributes.propertyDocumentation
a(href="##{component.get('name')}#{doc.name}")
li
| #{doc.name}
.col-xs-9.documentation-column.nano
ul.nano-content
for component in components
each doc in component.attributes.propertyDocumentation
li(id="#{component.get('name')}#{doc.name}")
| #{doc.name}
ul.specialList
li=doc.description
li(id="#{component.get('name')}")
| #{component.get('name')}
ul
li.doc-description
| #{component.get('description')}
ul
each doc in component.attributes.propertyDocumentation
li(id="#{component.get('name')}#{doc.name}")
| #{doc.name}
ul.specialList
li!=marked(doc.description)

View file

@ -28,9 +28,10 @@ nav.navbar.navbar-default(role='navigation')
span.glyphicon.glyphicon-eye-close
span.spl Unwatch
li#patch-component-button
a(data-i18n="common.submit_patch") Submit Patch
if me.isAdmin()
if !component.hasWriteAccess()
li#patch-component-button
a(data-i18n="common.submit_patch") Submit Patch
if !me.get('anonymous')
li#create-new-component-button
a(data-i18n="editor.level_component_b_new") Create New Component

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

@ -24,4 +24,5 @@ module.exports = class UnnamedView extends RootView
getRenderData: ->
c = super()
c.components = @componentDocs.models
c.marked = marked
c

View file

@ -31,6 +31,9 @@ module.exports = class DeltaView extends CocoView
for modelName in ['model', 'headModel', 'comparisonModel']
@[modelName] = options[modelName]
continue unless @[modelName]
if not @[modelName].isLoaded
@[modelName] = @supermodel.loadModel(@[modelName], 'document').model
@buildDeltas() if @supermodel.finished()

View file

@ -8,7 +8,7 @@ module.exports = class PatchModal extends ModalView
template: template
plain: true
modalWidthPercent: 60
@DOC_SKIP_PATHS = ['_id','version', 'commitMessage', 'parent', 'created', 'slug', 'index', '__v', 'patches', 'creator']
@DOC_SKIP_PATHS = ['_id','version', 'commitMessage', 'parent', 'created', 'slug', 'index', '__v', 'patches', 'creator', 'js']
events:
'click #withdraw-button': 'withdrawPatch'

View file

@ -285,7 +285,7 @@ module.exports = class ThangTypeEditView extends View
@scale = scaleValue
@$el.find('.scale-label').text " #{fixed}x "
if @currentSprite
@currentSprite.scaleFactor = scaleValue
@currentSprite.scaleFactorX = @currentSprite.scaleFactorY = scaleValue
@currentSprite.updateScale()
else if @currentObject?
@currentObject.scaleX = @currentObject.scaleY = scaleValue / resValue

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

@ -368,7 +368,7 @@ module.exports = class SpellView extends View
# Now that that's figured out, perform the update.
# The web worker Aether won't track state, so don't have to worry about updating it
finishUpdatingAether = (aether) =>
@displayAether aether
@displayAether aether, codeIsAsCast
@lastUpdatedAetherSpellThang = @spellThang
@guessWhetherFinished aether if fromCodeChange
@ -396,10 +396,9 @@ module.exports = class SpellView extends View
@aceSession.setAnnotations []
@highlightCurrentLine {} # This'll remove all highlights
displayAether: (aether) ->
displayAether: (aether, isCast=false) ->
@displayedAether = aether
isCast = not _.isEmpty(aether.metrics) or _.some aether.problems.errors, {type: 'runtime'}
isCast = isCast or @spell.language isnt 'javascript' # Since we don't have linting for other languages
isCast = isCast or not _.isEmpty(aether.metrics) or _.some aether.problems.errors, {type: 'runtime'}
problem.destroy() for problem in @problems # Just in case another problem was added since clearAetherDisplay() ran.
@problems = []
annotations = []

View file

@ -84,7 +84,7 @@
"nodemon": "0.7.5",
"marked": "0.2.x",
"telepath-brunch": "https://github.com/nwinter/telepath-brunch/tarball/master",
"bower": "~1.2.8",
"bower": "~1.3.8",
"bless-brunch": "~1.6.1",
"karma-script-launcher": "~0.1.0",
"karma-chrome-launcher": "~0.1.2",

View file

@ -157,13 +157,30 @@ 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 '/'
console.log parts
originalName = parts[3]
return errors.badInput res, 'No name provided.' unless 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()