mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-12 08:41:46 -05:00
Merge branch 'master' into production
This commit is contained in:
commit
2c1ed6867d
48 changed files with 174 additions and 606 deletions
BIN
app/assets/images/pages/about/lisa_small.png
Normal file
BIN
app/assets/images/pages/about/lisa_small.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
|
@ -239,6 +239,12 @@ particleKinds['level-dungeon-replayable'] = particleKinds['level-dungeon-replaya
|
||||||
colorMiddle: hsl 0.17, 0.75, 0.5
|
colorMiddle: hsl 0.17, 0.75, 0.5
|
||||||
colorEnd: hsl 0.17, 0.75, 0.3
|
colorEnd: hsl 0.17, 0.75, 0.3
|
||||||
|
|
||||||
|
particleKinds['level-dungeon-game-dev'] = particleKinds['level-dungeon-game-dev-premium'] = ext particleKinds['level-dungeon-hero-ladder'],
|
||||||
|
emitter:
|
||||||
|
colorStart: hsl 0.7, 0.75, 0.7
|
||||||
|
colorMiddle: hsl 0.7, 0.75, 0.5
|
||||||
|
colorEnd: hsl 0.7, 0.75, 0.3
|
||||||
|
|
||||||
particleKinds['level-dungeon-premium-item'] = ext particleKinds['level-dungeon-gate'],
|
particleKinds['level-dungeon-premium-item'] = ext particleKinds['level-dungeon-gate'],
|
||||||
emitter:
|
emitter:
|
||||||
particleCount: 2000
|
particleCount: 2000
|
||||||
|
@ -288,6 +294,12 @@ particleKinds['level-forest-replayable'] = particleKinds['level-forest-replayabl
|
||||||
colorMiddle: hsl 0.17, 0.75, 0.5
|
colorMiddle: hsl 0.17, 0.75, 0.5
|
||||||
colorEnd: hsl 0.17, 0.75, 0.3
|
colorEnd: hsl 0.17, 0.75, 0.3
|
||||||
|
|
||||||
|
particleKinds['level-forest-game-dev'] = particleKinds['level-forest-game-dev-premium'] = ext particleKinds['level-forest-hero-ladder'],
|
||||||
|
emitter:
|
||||||
|
colorStart: hsl 0.7, 0.75, 0.7
|
||||||
|
colorMiddle: hsl 0.7, 0.75, 0.5
|
||||||
|
colorEnd: hsl 0.7, 0.75, 0.3
|
||||||
|
|
||||||
particleKinds['level-forest-premium-item'] = ext particleKinds['level-forest-gate'],
|
particleKinds['level-forest-premium-item'] = ext particleKinds['level-forest-gate'],
|
||||||
emitter:
|
emitter:
|
||||||
particleCount: 2000
|
particleCount: 2000
|
||||||
|
@ -337,6 +349,12 @@ particleKinds['level-desert-replayable'] = particleKinds['level-desert-replayabl
|
||||||
colorMiddle: hsl 0.17, 0.75, 0.5
|
colorMiddle: hsl 0.17, 0.75, 0.5
|
||||||
colorEnd: hsl 0.17, 0.75, 0.3
|
colorEnd: hsl 0.17, 0.75, 0.3
|
||||||
|
|
||||||
|
particleKinds['level-desert-game-dev'] = particleKinds['level-desert-game-dev-premium'] = ext particleKinds['level-desert-hero-ladder'],
|
||||||
|
emitter:
|
||||||
|
colorStart: hsl 0.7, 0.75, 0.7
|
||||||
|
colorMiddle: hsl 0.7, 0.75, 0.5
|
||||||
|
colorEnd: hsl 0.7, 0.75, 0.3
|
||||||
|
|
||||||
particleKinds['level-mountain-premium-hero'] = ext particleKinds['level-mountain-premium'],
|
particleKinds['level-mountain-premium-hero'] = ext particleKinds['level-mountain-premium'],
|
||||||
emitter:
|
emitter:
|
||||||
particleCount: 200
|
particleCount: 200
|
||||||
|
@ -371,6 +389,12 @@ particleKinds['level-mountain-replayable'] = particleKinds['level-mountain-repla
|
||||||
colorMiddle: hsl 0.17, 0.75, 0.5
|
colorMiddle: hsl 0.17, 0.75, 0.5
|
||||||
colorEnd: hsl 0.17, 0.75, 0.3
|
colorEnd: hsl 0.17, 0.75, 0.3
|
||||||
|
|
||||||
|
particleKinds['level-mountain-game-dev'] = particleKinds['level-mountain-game-dev-premium'] = ext particleKinds['level-mountain-hero-ladder'],
|
||||||
|
emitter:
|
||||||
|
colorStart: hsl 0.7, 0.75, 0.7
|
||||||
|
colorMiddle: hsl 0.7, 0.75, 0.5
|
||||||
|
colorEnd: hsl 0.7, 0.75, 0.3
|
||||||
|
|
||||||
particleKinds['level-glacier-premium-hero'] = ext particleKinds['level-glacier-premium'],
|
particleKinds['level-glacier-premium-hero'] = ext particleKinds['level-glacier-premium'],
|
||||||
emitter:
|
emitter:
|
||||||
particleCount: 200
|
particleCount: 200
|
||||||
|
@ -405,6 +429,12 @@ particleKinds['level-glacier-replayable'] = particleKinds['level-glacier-replaya
|
||||||
colorMiddle: hsl 0.17, 0.75, 0.5
|
colorMiddle: hsl 0.17, 0.75, 0.5
|
||||||
colorEnd: hsl 0.17, 0.75, 0.3
|
colorEnd: hsl 0.17, 0.75, 0.3
|
||||||
|
|
||||||
|
particleKinds['level-glacier-game-dev'] = particleKinds['level-glacier-game-dev-premium'] = ext particleKinds['level-glacier-hero-ladder'],
|
||||||
|
emitter:
|
||||||
|
colorStart: hsl 0.7, 0.75, 0.7
|
||||||
|
colorMiddle: hsl 0.7, 0.75, 0.5
|
||||||
|
colorEnd: hsl 0.7, 0.75, 0.3
|
||||||
|
|
||||||
particleKinds['level-volcano-premium-hero'] = ext particleKinds['level-volcano-premium'],
|
particleKinds['level-volcano-premium-hero'] = ext particleKinds['level-volcano-premium'],
|
||||||
emitter:
|
emitter:
|
||||||
particleCount: 200
|
particleCount: 200
|
||||||
|
@ -438,3 +468,9 @@ particleKinds['level-volcano-replayable'] = particleKinds['level-volcano-replaya
|
||||||
colorStart: hsl 0.17, 0.75, 0.7
|
colorStart: hsl 0.17, 0.75, 0.7
|
||||||
colorMiddle: hsl 0.17, 0.75, 0.5
|
colorMiddle: hsl 0.17, 0.75, 0.5
|
||||||
colorEnd: hsl 0.17, 0.75, 0.3
|
colorEnd: hsl 0.17, 0.75, 0.3
|
||||||
|
|
||||||
|
particleKinds['level-volcano-game-dev'] = particleKinds['level-volcano-game-dev-premium'] = ext particleKinds['level-volcano-hero-ladder'],
|
||||||
|
emitter:
|
||||||
|
colorStart: hsl 0.7, 0.75, 0.7
|
||||||
|
colorMiddle: hsl 0.7, 0.75, 0.5
|
||||||
|
colorEnd: hsl 0.7, 0.75, 0.3
|
||||||
|
|
|
@ -14,40 +14,6 @@ init = ->
|
||||||
|
|
||||||
Backbone.listenTo me, 'sync', -> Backbone.Mediator.publish('auth:me-synced', me: me)
|
Backbone.listenTo me, 'sync', -> Backbone.Mediator.publish('auth:me-synced', me: me)
|
||||||
|
|
||||||
module.exports.createUser = (userObject, failure=backboneFailure, nextURL=null) ->
|
|
||||||
user = new User(userObject)
|
|
||||||
user.notyErrors = false
|
|
||||||
user.save({}, {
|
|
||||||
error: (model, jqxhr, options) ->
|
|
||||||
error = parseServerError(jqxhr.responseText)
|
|
||||||
property = error.property if error.property
|
|
||||||
if jqxhr.status is 409 and property is 'name'
|
|
||||||
anonUserObject = _.omit(userObject, 'name')
|
|
||||||
module.exports.createUser anonUserObject, failure, nextURL
|
|
||||||
else
|
|
||||||
genericFailure(jqxhr)
|
|
||||||
success: -> if nextURL then window.location.href = nextURL else window.location.reload()
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports.createUserWithoutReload = (userObject, failure=backboneFailure) ->
|
|
||||||
user = new User(userObject)
|
|
||||||
user.save({}, {
|
|
||||||
error: failure
|
|
||||||
success: ->
|
|
||||||
Backbone.Mediator.publish('created-user-without-reload')
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports.loginUser = (userObject, failure=genericFailure, nextURL=null) ->
|
|
||||||
console.log 'logging in as', userObject.email
|
|
||||||
jqxhr = $.post('/auth/login',
|
|
||||||
{
|
|
||||||
username: userObject.email,
|
|
||||||
password: userObject.password
|
|
||||||
},
|
|
||||||
(model) -> if nextURL then window.location.href = nextURL else window.location.reload()
|
|
||||||
)
|
|
||||||
jqxhr.fail(failure)
|
|
||||||
|
|
||||||
module.exports.logoutUser = ->
|
module.exports.logoutUser = ->
|
||||||
# TODO: Refactor to use User.logout
|
# TODO: Refactor to use User.logout
|
||||||
FB?.logout?()
|
FB?.logout?()
|
||||||
|
|
|
@ -243,6 +243,7 @@
|
||||||
|
|
||||||
login:
|
login:
|
||||||
sign_up: "Create Account"
|
sign_up: "Create Account"
|
||||||
|
email_or_username: "Email or username"
|
||||||
log_in: "Log In"
|
log_in: "Log In"
|
||||||
logging_in: "Logging In"
|
logging_in: "Logging In"
|
||||||
log_out: "Log Out"
|
log_out: "Log Out"
|
||||||
|
|
|
@ -58,7 +58,7 @@ module.exports = class Level extends CocoModel
|
||||||
|
|
||||||
denormalize: (supermodel, session, otherSession) ->
|
denormalize: (supermodel, session, otherSession) ->
|
||||||
o = $.extend true, {}, @attributes
|
o = $.extend true, {}, @attributes
|
||||||
if o.thangs and @get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder']
|
if o.thangs and @get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']
|
||||||
thangTypesWithComponents = (tt for tt in supermodel.getModels(ThangType) when tt.get('components')?)
|
thangTypesWithComponents = (tt for tt in supermodel.getModels(ThangType) when tt.get('components')?)
|
||||||
thangTypesByOriginal = _.indexBy thangTypesWithComponents, (tt) -> tt.get('original') # Optimization
|
thangTypesByOriginal = _.indexBy thangTypesWithComponents, (tt) -> tt.get('original') # Optimization
|
||||||
for levelThang in o.thangs
|
for levelThang in o.thangs
|
||||||
|
|
|
@ -275,6 +275,13 @@ module.exports = class User extends CocoModel
|
||||||
options.data.facebookAccessToken = application.facebookHandler.token()
|
options.data.facebookAccessToken = application.facebookHandler.token()
|
||||||
@fetch(options)
|
@fetch(options)
|
||||||
|
|
||||||
|
loginPasswordUser: (usernameOrEmail, password, options={}) ->
|
||||||
|
options.url = '/auth/login'
|
||||||
|
options.type = 'POST'
|
||||||
|
options.data ?= {}
|
||||||
|
_.extend(options.data, { username: usernameOrEmail, password })
|
||||||
|
@fetch(options)
|
||||||
|
|
||||||
makeCoursePrepaid: ->
|
makeCoursePrepaid: ->
|
||||||
coursePrepaid = @get('coursePrepaid')
|
coursePrepaid = @get('coursePrepaid')
|
||||||
return null unless coursePrepaid
|
return null unless coursePrepaid
|
||||||
|
|
|
@ -61,7 +61,7 @@ _.extend CampaignSchema.properties, {
|
||||||
i18n: { type: 'object', format: 'hidden' }
|
i18n: { type: 'object', format: 'hidden' }
|
||||||
requiresSubscription: { type: 'boolean' }
|
requiresSubscription: { type: 'boolean' }
|
||||||
replayable: { type: 'boolean' }
|
replayable: { type: 'boolean' }
|
||||||
type: {'enum': ['ladder', 'ladder-tutorial', 'hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder']}
|
type: {'enum': ['ladder', 'ladder-tutorial', 'hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']}
|
||||||
slug: { type: 'string', format: 'hidden' }
|
slug: { type: 'string', format: 'hidden' }
|
||||||
original: { type: 'string', format: 'hidden' }
|
original: { type: 'string', format: 'hidden' }
|
||||||
adventurer: { type: 'boolean' }
|
adventurer: { type: 'boolean' }
|
||||||
|
|
|
@ -306,7 +306,7 @@ _.extend LevelSchema.properties,
|
||||||
icon: {type: 'string', format: 'image-file', title: 'Icon'}
|
icon: {type: 'string', format: 'image-file', title: 'Icon'}
|
||||||
banner: {type: 'string', format: 'image-file', title: 'Banner'}
|
banner: {type: 'string', format: 'image-file', title: 'Banner'}
|
||||||
goals: c.array {title: 'Goals', description: 'An array of goals which are visible to the player and can trigger scripts.'}, GoalSchema
|
goals: c.array {title: 'Goals', description: 'An array of goals which are visible to the player and can trigger scripts.'}, GoalSchema
|
||||||
type: c.shortString(title: 'Type', description: 'What kind of level this is.', 'enum': ['campaign', 'ladder', 'ladder-tutorial', 'hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'])
|
type: c.shortString(title: 'Type', description: 'What kind of level this is.', 'enum': ['campaign', 'ladder', 'ladder-tutorial', 'hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'])
|
||||||
terrain: c.terrainString
|
terrain: c.terrainString
|
||||||
showsGuide: c.shortString(title: 'Shows Guide', description: 'If the guide is shown at the beginning of the level.', 'enum': ['first-time', 'always'])
|
showsGuide: c.shortString(title: 'Shows Guide', description: 'If the guide is shown at the beginning of the level.', 'enum': ['first-time', 'always'])
|
||||||
requiresSubscription: {title: 'Requires Subscription', description: 'Whether this level is available to subscribers only.', type: 'boolean'}
|
requiresSubscription: {title: 'Requires Subscription', description: 'Whether this level is available to subscribers only.', type: 'boolean'}
|
||||||
|
|
|
@ -10,7 +10,7 @@ class AttacksSelf extends Component
|
||||||
systems = [
|
systems = [
|
||||||
'action', 'ai', 'alliance', 'collision', 'combat', 'display', 'event', 'existence', 'hearing',
|
'action', 'ai', 'alliance', 'collision', 'combat', 'display', 'event', 'existence', 'hearing',
|
||||||
'inventory', 'movement', 'programming', 'targeting', 'ui', 'vision', 'misc', 'physics', 'effect',
|
'inventory', 'movement', 'programming', 'targeting', 'ui', 'vision', 'misc', 'physics', 'effect',
|
||||||
'magic'
|
'magic', 'game'
|
||||||
]
|
]
|
||||||
|
|
||||||
PropertyDocumentationSchema = c.object {
|
PropertyDocumentationSchema = c.object {
|
||||||
|
|
|
@ -18,21 +18,6 @@ class Jitter extends System
|
||||||
return hash
|
return hash
|
||||||
"""
|
"""
|
||||||
|
|
||||||
PropertyDocumentationSchema = c.object {
|
|
||||||
title: 'Property Documentation'
|
|
||||||
description: 'Documentation entry for a property this System will add to its Thang which other Systems might want to also use.'
|
|
||||||
default:
|
|
||||||
name: 'foo'
|
|
||||||
type: 'object'
|
|
||||||
description: 'This System provides a "foo" property to satisfy all one\'s foobar needs. Use it wisely.'
|
|
||||||
required: ['name', 'type', 'description']
|
|
||||||
},
|
|
||||||
name: {type: 'string', pattern: c.identifierPattern, title: 'Name', description: 'Name of the property.'}
|
|
||||||
# not actual JS types, just whatever they describe...
|
|
||||||
type: c.shortString(title: 'Type', description: 'Intended type of the property.')
|
|
||||||
description: {type: 'string', description: 'Description of the property.', maxLength: 1000}
|
|
||||||
args: c.array {title: 'Arguments', description: 'If this property has type "function", then provide documentation for any function arguments.'}, c.FunctionArgumentSchema
|
|
||||||
|
|
||||||
DependencySchema = c.object {
|
DependencySchema = c.object {
|
||||||
title: 'System Dependency'
|
title: 'System Dependency'
|
||||||
description: 'A System upon which this System depends.'
|
description: 'A System upon which this System depends.'
|
||||||
|
@ -50,14 +35,14 @@ DependencySchema = c.object {
|
||||||
LevelSystemSchema = c.object {
|
LevelSystemSchema = c.object {
|
||||||
title: 'System'
|
title: 'System'
|
||||||
description: 'A System which can affect Level behavior.'
|
description: 'A System which can affect Level behavior.'
|
||||||
required: ['name', 'description', 'code', 'dependencies', 'propertyDocumentation', 'codeLanguage']
|
required: ['name', 'code']
|
||||||
default:
|
default:
|
||||||
name: 'JitterSystem'
|
name: 'JitterSystem'
|
||||||
description: 'This System makes all idle, movable Thangs jitter around.'
|
description: 'This System makes all idle, movable Thangs jitter around.'
|
||||||
code: jitterSystemCode
|
code: jitterSystemCode
|
||||||
codeLanguage: 'coffeescript'
|
codeLanguage: 'coffeescript'
|
||||||
dependencies: [] # TODO: should depend on something by default
|
dependencies: [] # TODO: should depend on something by default
|
||||||
propertyDocumentation: []
|
configSchema: {}
|
||||||
}
|
}
|
||||||
c.extendNamedProperties LevelSystemSchema # let's have the name be the first property
|
c.extendNamedProperties LevelSystemSchema # let's have the name be the first property
|
||||||
LevelSystemSchema.properties.name.pattern = c.classNamePattern
|
LevelSystemSchema.properties.name.pattern = c.classNamePattern
|
||||||
|
@ -83,7 +68,6 @@ _.extend LevelSystemSchema.properties,
|
||||||
type: 'string'
|
type: 'string'
|
||||||
format: 'hidden'
|
format: 'hidden'
|
||||||
dependencies: c.array {title: 'Dependencies', description: 'An array of Systems upon which this System depends.', uniqueItems: true}, DependencySchema
|
dependencies: c.array {title: 'Dependencies', description: 'An array of Systems upon which this System depends.', uniqueItems: true}, DependencySchema
|
||||||
propertyDocumentation: c.array {title: 'Property Documentation', description: 'An array of documentation entries for each notable property this System will add to its Level which other Systems might want to also use.'}, PropertyDocumentationSchema
|
|
||||||
configSchema: _.extend metaschema, {title: 'Configuration Schema', description: 'A schema for validating the arguments that can be passed to this System as configuration.', default: {type: 'object', additionalProperties: false}}
|
configSchema: _.extend metaschema, {title: 'Configuration Schema', description: 'A schema for validating the arguments that can be passed to this System as configuration.', default: {type: 'object', additionalProperties: false}}
|
||||||
official:
|
official:
|
||||||
type: 'boolean'
|
type: 'boolean'
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
#hour-of-code-view
|
|
||||||
|
|
||||||
hr
|
|
||||||
border-top: 1px solid grey
|
|
||||||
margin: 30px 20px
|
|
||||||
|
|
||||||
#site-content-area
|
|
||||||
padding: 20px 300px
|
|
||||||
|
|
||||||
h1
|
|
||||||
margin-bottom: 40px
|
|
||||||
p
|
|
||||||
margin: 20px
|
|
||||||
|
|
||||||
h3
|
|
||||||
margin-top: 50px
|
|
||||||
|
|
||||||
ul
|
|
||||||
margin-bottom: 50px
|
|
|
@ -1,7 +0,0 @@
|
||||||
#student-log-in-modal
|
|
||||||
#log-in-btn
|
|
||||||
min-width: 30%
|
|
||||||
margin-bottom: 10px
|
|
||||||
|
|
||||||
.form
|
|
||||||
margin: 0 25%
|
|
|
@ -1,10 +0,0 @@
|
||||||
#student-sign-up-modal
|
|
||||||
#sign-up-btn
|
|
||||||
min-width: 30%
|
|
||||||
margin-bottom: 10px
|
|
||||||
|
|
||||||
.form
|
|
||||||
margin: 0 25%
|
|
||||||
|
|
||||||
.modal-dialog
|
|
||||||
margin-top: 0
|
|
|
@ -143,7 +143,7 @@
|
||||||
|
|
||||||
//- Primary auth button
|
//- Primary auth button
|
||||||
|
|
||||||
#login-button
|
#login-btn
|
||||||
position: absolute
|
position: absolute
|
||||||
top: 186px
|
top: 186px
|
||||||
height: 70px
|
height: 70px
|
||||||
|
|
|
@ -139,6 +139,13 @@ block content
|
||||||
small(data-i18n="about.elliot_title")
|
small(data-i18n="about.elliot_title")
|
||||||
br
|
br
|
||||||
|
|
||||||
|
li
|
||||||
|
img(src="/images/pages/about/lisa_small.png").img-thumbnail
|
||||||
|
.team-bio
|
||||||
|
h6.label.team-name Lisa Wu
|
||||||
|
small Marketing Development Rep
|
||||||
|
br
|
||||||
|
|
||||||
// Part time / contract
|
// Part time / contract
|
||||||
li
|
li
|
||||||
a(href="http://floor.is/lava/" rel="external")
|
a(href="http://floor.is/lava/" rel="external")
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
img(src="/images/pages/modal/auth/login-background.png", draggable="false").auth-modal-background
|
img(src="/images/pages/modal/auth/login-background.png", draggable="false").auth-modal-background
|
||||||
h1(data-i18n="login.log_in")
|
h1(data-i18n="login.log_in")
|
||||||
|
|
||||||
div#close-modal
|
#close-modal
|
||||||
span.glyphicon.glyphicon-remove
|
span.glyphicon.glyphicon-remove
|
||||||
|
|
||||||
.auth-form-content
|
.auth-form-content
|
||||||
|
@ -11,24 +11,40 @@
|
||||||
if showRequiredError
|
if showRequiredError
|
||||||
.alert.alert-success
|
.alert.alert-success
|
||||||
span(data-i18n="signup.required")
|
span(data-i18n="signup.required")
|
||||||
|
|
||||||
|
#unknown-error-alert.alert.alert-danger.hide(data-i18n="loading_error.unknown")
|
||||||
|
|
||||||
form.form
|
form.form
|
||||||
.form-group
|
.form-group
|
||||||
label.control-label(for="email")
|
label.control-label(for="username-or-email-input")
|
||||||
span(data-i18n="general.email")
|
span(data-i18n="login.email_or_username")
|
||||||
| :
|
| :
|
||||||
.input-border
|
.input-border
|
||||||
input#email.input-large.form-control(name="email", type="email", value=formValues.email)
|
input#username-or-email-input.input-large.form-control(
|
||||||
|
name="emailOrUsername"
|
||||||
|
value=view.previousFormInputs.email
|
||||||
|
)
|
||||||
.form-group
|
.form-group
|
||||||
div#recover-account-wrapper
|
#recover-account-wrapper
|
||||||
a(data-toggle="coco-modal", data-target="core/RecoverModal", data-i18n="login.forgot_password")#link-to-recover
|
a#link-to-recover(
|
||||||
|
data-toggle="coco-modal"
|
||||||
|
data-target="core/RecoverModal"
|
||||||
|
data-i18n="login.forgot_password"
|
||||||
|
)
|
||||||
label.control-label(for="password")
|
label.control-label(for="password")
|
||||||
span(data-i18n="general.password")
|
span(data-i18n="general.password")
|
||||||
| :
|
| :
|
||||||
.input-border
|
.input-border
|
||||||
input#password.input-large.form-control(name="password", type="password", value=formValues.password)
|
input#password-input.input-large.form-control(
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
value=view.previousFormInputs.password
|
||||||
|
)
|
||||||
|
|
||||||
input.btn.btn-lg.btn-illustrated.btn-block.btn-success#login-button(value=translate("login.log_in"), type="submit")
|
input#login-btn.btn.btn-lg.btn-illustrated.btn-block.btn-success(
|
||||||
|
value=translate("login.log_in")
|
||||||
|
type="submit"
|
||||||
|
)
|
||||||
|
|
||||||
.wait.secret
|
.wait.secret
|
||||||
h3(data-i18n="login.logging_in")
|
h3(data-i18n="login.logging_in")
|
|
@ -1,74 +0,0 @@
|
||||||
extends /templates/base
|
|
||||||
|
|
||||||
block content
|
|
||||||
.pull-right
|
|
||||||
if me.isAnonymous()
|
|
||||||
a(href="/teachers")
|
|
||||||
span(data-i18n="courses.teachers_click")
|
|
||||||
span !
|
|
||||||
else
|
|
||||||
a(href="/teachers/classes")
|
|
||||||
span(data-i18n="courses.teachers_click")
|
|
||||||
span !
|
|
||||||
br
|
|
||||||
|
|
||||||
h1.text-center(data-i18n="courses.welcome_to_hoc")
|
|
||||||
|
|
||||||
#main-content
|
|
||||||
.well.text-center
|
|
||||||
if !me.isAnonymous()
|
|
||||||
p
|
|
||||||
strong.spr(data-i18n="courses.logged_in_as")
|
|
||||||
strong= me.get('name') || me.get('email')
|
|
||||||
|
|
||||||
p
|
|
||||||
span.spr(data-i18n="courses.not_you")
|
|
||||||
a#log-out-link(data-i18n="login.logout")
|
|
||||||
|
|
||||||
hr
|
|
||||||
|
|
||||||
if !view.lastLevel
|
|
||||||
p
|
|
||||||
strong(data-i18n="courses.ready_to_play")
|
|
||||||
|
|
||||||
p
|
|
||||||
button#start-new-game-btn.btn.btn-success.btn-lg(data-i18n="courses.start_new_game")
|
|
||||||
else
|
|
||||||
|
|
||||||
p
|
|
||||||
strong(data-i18n="courses.welcome_back")
|
|
||||||
p
|
|
||||||
button#continue-playing-btn.btn.btn-success.btn-lg(data-i18n="courses.continue_playing")
|
|
||||||
p
|
|
||||||
em.spr
|
|
||||||
span(data-i18n="clans.last_played")
|
|
||||||
span.spr :
|
|
||||||
span= view.lastLevel.get('name').replace('Course :', '')
|
|
||||||
|
|
||||||
if me.isAnonymous()
|
|
||||||
p
|
|
||||||
strong(data-i18n="courses.more_options")
|
|
||||||
p
|
|
||||||
button#start-new-game-btn.btn.btn-default.btn-lg(data-i18n="courses.start_new_game")
|
|
||||||
|
|
||||||
if me.isAnonymous()
|
|
||||||
p
|
|
||||||
span.spr -
|
|
||||||
span.text-uppercase(data-i18n="general.or")
|
|
||||||
span.spl -
|
|
||||||
|
|
||||||
p
|
|
||||||
button#log-in-btn.btn.btn-default.btn-lg(data-i18n="login.log_in")
|
|
||||||
|
|
||||||
#begin-hoc-area.hide
|
|
||||||
h2.text-center(data-i18n="common.loading")
|
|
||||||
.progress.progress-striped.active
|
|
||||||
.progress-bar(style="width: 100%")
|
|
||||||
|
|
||||||
|
|
||||||
h3.text-center.text-uppercase(data-i18n="courses.play_now_learn_header")
|
|
||||||
ul
|
|
||||||
li(data-i18n="courses.play_now_learn_1")
|
|
||||||
li(data-i18n="courses.play_now_learn_2")
|
|
||||||
li(data-i18n="courses.play_now_learn_3")
|
|
||||||
li(data-i18n="courses.play_now_learn_4")
|
|
|
@ -1,30 +0,0 @@
|
||||||
extends /templates/core/modal-base
|
|
||||||
|
|
||||||
block modal-header-content
|
|
||||||
.clearfix
|
|
||||||
|
|
||||||
|
|
||||||
block modal-body-content
|
|
||||||
.text-center
|
|
||||||
h2.modal-title(data-i18n="login.log_in")
|
|
||||||
|
|
||||||
form.form
|
|
||||||
.form-group
|
|
||||||
label.control-label(for="email")
|
|
||||||
span(data-i18n="general.email")
|
|
||||||
input#email.input-large.form-control(name="email", type="email")
|
|
||||||
.form-group
|
|
||||||
label.control-label(for="password")
|
|
||||||
span(data-i18n="general.password")
|
|
||||||
input#password.input-large.form-control(name="password", type="password")
|
|
||||||
|
|
||||||
#errors-alert.alert.alert-danger.hide
|
|
||||||
|
|
||||||
.text-center
|
|
||||||
input#log-in-btn.btn.btn-default(data-i18n="[value]login.log_in", type="submit")
|
|
||||||
p
|
|
||||||
a#create-new-account-link(data-i18n="login.signup_switch")
|
|
||||||
p
|
|
||||||
a(data-toggle="coco-modal", data-target="core/RecoverModal", data-i18n="login.forgot_password")
|
|
||||||
|
|
||||||
block modal-footer-content
|
|
|
@ -1,46 +0,0 @@
|
||||||
extends /templates/core/modal-base
|
|
||||||
|
|
||||||
block modal-header-content
|
|
||||||
.clearfix
|
|
||||||
|
|
||||||
|
|
||||||
block modal-body-content
|
|
||||||
.text-center
|
|
||||||
h2.modal-title(data-i18n="signup.sign_up")
|
|
||||||
|
|
||||||
form.form
|
|
||||||
.form-group
|
|
||||||
label.control-label(for="email")
|
|
||||||
span(data-i18n="general.email")
|
|
||||||
input#email.input-large.form-control(name="email", type="email")
|
|
||||||
.help-block(data-i18n="courses.use_school_email")
|
|
||||||
.form-group
|
|
||||||
label.control-label(for="name")
|
|
||||||
span(data-i18n="general.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="e.g. Alex W the Skater")
|
|
||||||
.help-block(data-i18n="courses.unique_name")
|
|
||||||
.form-group
|
|
||||||
label.control-label(for="password")
|
|
||||||
span(data-i18n="general.password")
|
|
||||||
input#password.input-large.form-control(name="password", type="password")
|
|
||||||
.help-block(data-i18n="courses.pick_something")
|
|
||||||
.form-group
|
|
||||||
label.control-label(for="class-code-input")
|
|
||||||
span(data-i18n="courses.class_code")
|
|
||||||
input#class-code-input.input-large.form-control(name="classCode", value=view.classCode)
|
|
||||||
.help-block(data-i18n="courses.optional_ask")
|
|
||||||
.form-group
|
|
||||||
label.control-label(for="school-input")
|
|
||||||
span(data-i18n="signup.school_name")
|
|
||||||
input#school-input.input-large.form-control(name="schoolName", data-i18n="[placeholder]signup.school_name_placeholder")
|
|
||||||
.help-block(data-i18n="courses.optional_school")
|
|
||||||
|
|
||||||
#errors-alert.alert.alert-danger.hide
|
|
||||||
|
|
||||||
.text-center
|
|
||||||
input#sign-up-btn.btn.btn-default(data-i18n="[value]signup.sign_up", type="submit")
|
|
||||||
|
|
||||||
block modal-footer-content
|
|
|
@ -1,6 +1,5 @@
|
||||||
ModalView = require 'views/core/ModalView'
|
ModalView = require 'views/core/ModalView'
|
||||||
template = require 'templates/core/auth'
|
template = require 'templates/core/auth-modal'
|
||||||
{loginUser, createUser, me} = require 'core/auth'
|
|
||||||
forms = require 'core/forms'
|
forms = require 'core/forms'
|
||||||
User = require 'models/User'
|
User = require 'models/User'
|
||||||
application = require 'core/application'
|
application = require 'core/application'
|
||||||
|
@ -12,15 +11,11 @@ module.exports = class AuthModal extends ModalView
|
||||||
|
|
||||||
events:
|
events:
|
||||||
'click #switch-to-signup-btn': 'onSignupInstead'
|
'click #switch-to-signup-btn': 'onSignupInstead'
|
||||||
'click #github-login-button': 'onGitHubLoginClicked'
|
|
||||||
'submit form': 'onSubmitForm'
|
'submit form': 'onSubmitForm'
|
||||||
'keyup #name': 'onNameChange'
|
'keyup #name': 'onNameChange'
|
||||||
'click #gplus-login-btn': 'onClickGPlusLoginButton'
|
'click #gplus-login-btn': 'onClickGPlusLoginButton'
|
||||||
'click #facebook-login-btn': 'onClickFacebookLoginButton'
|
'click #facebook-login-btn': 'onClickFacebookLoginButton'
|
||||||
'click #close-modal': 'hide'
|
'click #close-modal': 'hide'
|
||||||
|
|
||||||
subscriptions:
|
|
||||||
'errors:server-error': 'onServerError'
|
|
||||||
|
|
||||||
|
|
||||||
# Initialization
|
# Initialization
|
||||||
|
@ -32,15 +27,6 @@ module.exports = class AuthModal extends ModalView
|
||||||
application.gplusHandler.loadAPI({ success: => _.defer => @$('#gplus-login-btn').attr('disabled', false) })
|
application.gplusHandler.loadAPI({ success: => _.defer => @$('#gplus-login-btn').attr('disabled', false) })
|
||||||
application.facebookHandler.loadAPI({ success: => _.defer => @$('#facebook-login-btn').attr('disabled', false) })
|
application.facebookHandler.loadAPI({ success: => _.defer => @$('#facebook-login-btn').attr('disabled', false) })
|
||||||
|
|
||||||
getRenderData: ->
|
|
||||||
c = super()
|
|
||||||
c.showRequiredError = @options.showRequiredError
|
|
||||||
c.showSignupRationale = @options.showSignupRationale
|
|
||||||
c.mode = @mode
|
|
||||||
c.formValues = @previousFormInputs or {}
|
|
||||||
c.me = me
|
|
||||||
c
|
|
||||||
|
|
||||||
afterRender: ->
|
afterRender: ->
|
||||||
super()
|
super()
|
||||||
@playSound 'game-menu-open'
|
@playSound 'game-menu-open'
|
||||||
|
@ -58,16 +44,30 @@ module.exports = class AuthModal extends ModalView
|
||||||
@playSound 'menu-button-click'
|
@playSound 'menu-button-click'
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
forms.clearFormAlerts(@$el)
|
forms.clearFormAlerts(@$el)
|
||||||
|
@$('#unknown-error-alert').addClass('hide')
|
||||||
userObject = forms.formToObject @$el
|
userObject = forms.formToObject @$el
|
||||||
res = tv4.validateMultiple userObject, formSchema
|
res = tv4.validateMultiple userObject, formSchema
|
||||||
return forms.applyErrorsToForm(@$el, res.errors) unless res.valid
|
return forms.applyErrorsToForm(@$el, res.errors) unless res.valid
|
||||||
@enableModalInProgress(@$el) # TODO: part of forms
|
new Promise(me.loginPasswordUser(userObject.emailOrUsername, userObject.password).then)
|
||||||
loginUser userObject, null, window.nextURL
|
.then(->
|
||||||
|
if window.nextURL then window.location.href = window.nextURL else window.location.reload()
|
||||||
onServerError: (e) -> # TODO: work error handling into a separate forms system
|
)
|
||||||
@disableModalInProgress(@$el)
|
.catch((jqxhr) =>
|
||||||
|
showingError = false
|
||||||
|
if jqxhr.status is 401
|
||||||
|
errorID = jqxhr.responseJSON.errorID
|
||||||
|
if errorID is 'not-found'
|
||||||
|
forms.setErrorToProperty(@$el, 'emailOrUsername', $.i18n.t('loading_error.not_found'))
|
||||||
|
showingError = true
|
||||||
|
if errorID is 'wrong-password'
|
||||||
|
forms.setErrorToProperty(@$el, 'password', $.i18n.t('account_settings.wrong_password'))
|
||||||
|
showingError = true
|
||||||
|
|
||||||
|
if not showingError
|
||||||
|
@$('#unknown-error-alert').removeClass('hide')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Google Plus
|
# Google Plus
|
||||||
|
|
||||||
onClickGPlusLoginButton: ->
|
onClickGPlusLoginButton: ->
|
||||||
|
@ -136,6 +136,14 @@ module.exports = class AuthModal extends ModalView
|
||||||
|
|
||||||
formSchema = {
|
formSchema = {
|
||||||
type: 'object'
|
type: 'object'
|
||||||
properties: _.pick(User.schema.properties, 'email', 'password')
|
properties: {
|
||||||
required: ['email', 'password']
|
emailOrUsername: {
|
||||||
|
$or: [
|
||||||
|
User.schema.properties.name
|
||||||
|
User.schema.properties.email
|
||||||
|
]
|
||||||
|
}
|
||||||
|
password: User.schema.properties.password
|
||||||
|
}
|
||||||
|
required: ['emailOrUsername', 'password']
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
ModalView = require 'views/core/ModalView'
|
ModalView = require 'views/core/ModalView'
|
||||||
template = require 'templates/core/create-account-modal'
|
template = require 'templates/core/create-account-modal'
|
||||||
{loginUser, createUser, me} = require 'core/auth'
|
|
||||||
forms = require 'core/forms'
|
forms = require 'core/forms'
|
||||||
User = require 'models/User'
|
User = require 'models/User'
|
||||||
application = require 'core/application'
|
application = require 'core/application'
|
||||||
|
|
|
@ -1,108 +0,0 @@
|
||||||
app = require 'core/application'
|
|
||||||
CocoCollection = require 'collections/CocoCollection'
|
|
||||||
Course = require 'models/Course'
|
|
||||||
CourseInstance = require 'models/CourseInstance'
|
|
||||||
RootView = require 'views/core/RootView'
|
|
||||||
template = require 'templates/courses/hour-of-code-view'
|
|
||||||
utils = require 'core/utils'
|
|
||||||
LevelSession = require 'models/LevelSession'
|
|
||||||
Level = require 'models/Level'
|
|
||||||
ChooseLanguageModal = require 'views/courses/ChooseLanguageModal'
|
|
||||||
StudentLogInModal = require 'views/courses/StudentLogInModal'
|
|
||||||
StudentSignUpModal = require 'views/courses/StudentSignUpModal'
|
|
||||||
auth = require 'core/auth'
|
|
||||||
|
|
||||||
module.exports = class HourOfCodeView extends RootView
|
|
||||||
id: 'hour-of-code-view'
|
|
||||||
template: template
|
|
||||||
|
|
||||||
events:
|
|
||||||
'click #continue-playing-btn': 'onClickContinuePlayingButton'
|
|
||||||
'click #start-new-game-btn': 'onClickStartNewGameButton'
|
|
||||||
'click #log-in-btn': 'onClickLogInButton'
|
|
||||||
'click #log-out-link': 'onClickLogOutLink'
|
|
||||||
|
|
||||||
initialize: ->
|
|
||||||
@setUpHourOfCode()
|
|
||||||
@courseInstances = new CocoCollection([], { url: "/db/user/#{me.id}/course_instances", model: CourseInstance})
|
|
||||||
@listenToOnce @courseInstances, 'sync', @onCourseInstancesLoaded
|
|
||||||
@courseInstances.comparator = (ci) -> return ci.get('classroomID') + ci.get('courseID')
|
|
||||||
@supermodel.loadCollection(@courseInstances, 'course_instances', { cache: false })
|
|
||||||
|
|
||||||
onCourseInstancesLoaded: ->
|
|
||||||
@hourOfCodeCourseInstance = @courseInstances.findWhere({hourOfCode: true})
|
|
||||||
if @hourOfCodeCourseInstance
|
|
||||||
@sessions = new CocoCollection([], {
|
|
||||||
url: "/db/course_instance/#{@hourOfCodeCourseInstance.id}/level_sessions"
|
|
||||||
model: LevelSession
|
|
||||||
})
|
|
||||||
@sessions.comparator = 'created'
|
|
||||||
@listenTo @sessions, 'sync', @onSessionsLoaded
|
|
||||||
@supermodel.loadCollection(@sessions, 'sessions', { cache: false })
|
|
||||||
|
|
||||||
onSessionsLoaded: ->
|
|
||||||
@lastSession = @sessions.last()
|
|
||||||
if @lastSession
|
|
||||||
@lastLevel = new Level()
|
|
||||||
levelData = @lastSession.get('level')
|
|
||||||
@supermodel.loadModel(@lastLevel, {
|
|
||||||
url: "/db/level/#{levelData.original}/version/#{levelData.majorVersion}"
|
|
||||||
data: {
|
|
||||||
project: 'name,slug'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
setUpHourOfCode: ->
|
|
||||||
# If we haven't tracked this player as an hourOfCode player yet, and it's a new account, we do that now.
|
|
||||||
elapsed = new Date() - new Date(me.get('dateCreated'))
|
|
||||||
if not me.get('hourOfCode') and (elapsed < 5 * 60 * 1000 or me.get('anonymous'))
|
|
||||||
me.set('hourOfCode', true)
|
|
||||||
me.patch()
|
|
||||||
$('body').append($('<img src="https://code.org/api/hour/begin_codecombat.png" style="visibility: hidden;">'))
|
|
||||||
window.tracker?.trackEvent 'Hour of Code Begin'
|
|
||||||
|
|
||||||
onClickContinuePlayingButton: ->
|
|
||||||
url = @continuePlayingLink()
|
|
||||||
window.tracker?.trackEvent 'HoC continue playing ', category: 'HoC', label: url
|
|
||||||
app.router.navigate(url, { trigger: true })
|
|
||||||
|
|
||||||
afterRender: ->
|
|
||||||
super()
|
|
||||||
@onClickStartNewGameButton() if @getQueryVariable('go') and not @lastLevel
|
|
||||||
|
|
||||||
onClickStartNewGameButton: ->
|
|
||||||
# User without hour of code course instance, creates one, starts playing
|
|
||||||
modal = new ChooseLanguageModal({
|
|
||||||
logoutFirst: @hourOfCodeCourseInstance?
|
|
||||||
})
|
|
||||||
@openModalView(modal)
|
|
||||||
@listenToOnce modal, 'set-language', @startHourOfCodePlay
|
|
||||||
window.tracker?.trackEvent 'Start New Game', category: 'HoC', label: 'HoC Start New Game'
|
|
||||||
|
|
||||||
continuePlayingLink: ->
|
|
||||||
ci = @hourOfCodeCourseInstance
|
|
||||||
"/play/level/#{@lastLevel.get('slug')}?course=#{ci.get('courseID')}&course-instance=#{ci.id}"
|
|
||||||
|
|
||||||
startHourOfCodePlay: ->
|
|
||||||
@$('#main-content').hide()
|
|
||||||
@$('#begin-hoc-area').removeClass('hide')
|
|
||||||
hocCourseInstance = new CourseInstance()
|
|
||||||
hocCourseInstance.upsertForHOC({cache: false})
|
|
||||||
@listenToOnce hocCourseInstance, 'sync', ->
|
|
||||||
url = hocCourseInstance.firstLevelURL()
|
|
||||||
document.location.href = url
|
|
||||||
|
|
||||||
onClickLogInButton: ->
|
|
||||||
modal = new StudentLogInModal()
|
|
||||||
@openModalView(modal)
|
|
||||||
modal.on 'want-to-create-account', @onWantToCreateAccount, @
|
|
||||||
window.tracker?.trackEvent 'Started Login', category: 'HoC', label: 'HoC Login'
|
|
||||||
|
|
||||||
onWantToCreateAccount: ->
|
|
||||||
modal = new StudentSignUpModal()
|
|
||||||
@openModalView(modal)
|
|
||||||
window.tracker?.trackEvent 'Started Signup', category: 'HoC', label: 'HoC Sign Up'
|
|
||||||
|
|
||||||
onClickLogOutLink: ->
|
|
||||||
window.tracker?.trackEvent 'Log Out', category: 'HoC', label: 'HoC Log Out'
|
|
||||||
auth.logoutUser()
|
|
|
@ -1,43 +0,0 @@
|
||||||
ModalView = require 'views/core/ModalView'
|
|
||||||
template = require 'templates/courses/student-log-in-modal'
|
|
||||||
auth = require 'core/auth'
|
|
||||||
forms = require 'core/forms'
|
|
||||||
User = require 'models/User'
|
|
||||||
|
|
||||||
module.exports = class StudentLogInModal extends ModalView
|
|
||||||
id: 'student-log-in-modal'
|
|
||||||
template: template
|
|
||||||
|
|
||||||
events:
|
|
||||||
'click #log-in-btn': 'onClickLogInButton'
|
|
||||||
'submit form': 'onSubmitForm'
|
|
||||||
'click #create-new-account-link': 'onClickCreateNewAccountLink'
|
|
||||||
|
|
||||||
onSubmitForm: (e) ->
|
|
||||||
e.preventDefault()
|
|
||||||
@login()
|
|
||||||
|
|
||||||
onClickLogInButton: ->
|
|
||||||
@login()
|
|
||||||
|
|
||||||
login: ->
|
|
||||||
# TODO: doesn't track failed login
|
|
||||||
window.tracker?.trackEvent 'Finished Login', category: 'Courses', label: 'Courses Student Login'
|
|
||||||
data = forms.formToObject @$el
|
|
||||||
@enableModalInProgress(@$el)
|
|
||||||
auth.loginUser data, (jqxhr) =>
|
|
||||||
message = jqxhr.responseText
|
|
||||||
if jqxhr.status is 401
|
|
||||||
message = 'Wrong username or password. Try again!'
|
|
||||||
# TODO: Make the server return better error message
|
|
||||||
message = _.string.capitalize(message)
|
|
||||||
@disableModalInProgress(@$el)
|
|
||||||
@$('#errors-alert').text(message).removeClass('hide')
|
|
||||||
|
|
||||||
onClickCreateNewAccountLink: ->
|
|
||||||
@trigger 'want-to-create-account'
|
|
||||||
@hide?()
|
|
||||||
|
|
||||||
afterInsert: ->
|
|
||||||
super()
|
|
||||||
_.delay (=> @$('input:visible:first').focus()), 500
|
|
|
@ -1,101 +0,0 @@
|
||||||
ModalView = require 'views/core/ModalView'
|
|
||||||
template = require 'templates/courses/student-sign-up-modal'
|
|
||||||
auth = require 'core/auth'
|
|
||||||
forms = require 'core/forms'
|
|
||||||
User = require 'models/User'
|
|
||||||
Classroom = require 'models/Classroom'
|
|
||||||
utils = require 'core/utils'
|
|
||||||
|
|
||||||
module.exports = class StudentSignUpModal extends ModalView
|
|
||||||
id: 'student-sign-up-modal'
|
|
||||||
template: template
|
|
||||||
|
|
||||||
events:
|
|
||||||
'submit form': 'onSubmitForm'
|
|
||||||
'click #skip-link': 'onClickSkipLink'
|
|
||||||
|
|
||||||
initialize: (options) ->
|
|
||||||
options ?= {}
|
|
||||||
@willPlay = options.willPlay
|
|
||||||
@classCode = utils.getQueryVariable('_cc') or ''
|
|
||||||
|
|
||||||
afterInsert: ->
|
|
||||||
super()
|
|
||||||
_.delay (=> @$('input:visible:first').focus()), 500
|
|
||||||
|
|
||||||
onClickSkipLink: ->
|
|
||||||
@trigger 'click-skip-link' # defer to view that opened this modal
|
|
||||||
@hide?()
|
|
||||||
|
|
||||||
onSubmitForm: (e) ->
|
|
||||||
e.preventDefault()
|
|
||||||
@signupClassroomPrecheck()
|
|
||||||
|
|
||||||
emailCheck: ->
|
|
||||||
email = @$('#email').val()
|
|
||||||
filter = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,63}$/i # https://news.ycombinator.com/item?id=5763990
|
|
||||||
unless filter.test(email)
|
|
||||||
@$('#errors-alert').text($.i18n.t('share_progress_modal.email_invalid')).removeClass('hide')
|
|
||||||
return false
|
|
||||||
return true
|
|
||||||
|
|
||||||
signupClassroomPrecheck: ->
|
|
||||||
if not _.all([@$('#email').val(), @$('#password').val(), @$('#name').val()])
|
|
||||||
@$('#errors-alert').text('Enter email, username and password').removeClass('hide')
|
|
||||||
return
|
|
||||||
classCode = @$('#class-code-input').val()
|
|
||||||
if not classCode
|
|
||||||
return @signup()
|
|
||||||
classroom = new Classroom()
|
|
||||||
classroom.fetch({ url: '/db/classroom?code='+classCode })
|
|
||||||
classroom.once 'sync', @signup, @
|
|
||||||
classroom.once 'error', @onClassroomFetchError, @
|
|
||||||
@enableModalInProgress(@$el)
|
|
||||||
|
|
||||||
onClassroomFetchError: ->
|
|
||||||
@disableModalInProgress(@$el)
|
|
||||||
@$('#errors-alert').text('Classroom code could not be found').removeClass('hide')
|
|
||||||
|
|
||||||
signup: ->
|
|
||||||
return unless @emailCheck()
|
|
||||||
# TODO: consolidate with AuthModal logic, or make user creation process less magical, more RESTful
|
|
||||||
data = forms.formToObject @$el
|
|
||||||
delete data.classCode
|
|
||||||
for key, val of me.attributes when key in ['preferredLanguage', 'testGroupNumber', 'dateCreated', 'wizardColor1', 'name', 'music', 'volume', 'emails', 'schoolName']
|
|
||||||
data[key] ?= val
|
|
||||||
Backbone.Mediator.publish "auth:signed-up", {}
|
|
||||||
data.emails ?= {}
|
|
||||||
data.emails.generalNews ?= {}
|
|
||||||
data.emails.generalNews.enabled = false
|
|
||||||
# TODO: Doesn't handle failed user creation. Double posts when placed in onCreateUserSuccess.
|
|
||||||
window.tracker?.trackEvent 'Finished Student Signup', category: 'Courses', label: 'Courses Student Signup'
|
|
||||||
@enableModalInProgress(@$el)
|
|
||||||
user = new User(data)
|
|
||||||
user.notyErrors = false
|
|
||||||
user.save({}, {
|
|
||||||
validate: false # make server deal with everything
|
|
||||||
error: @onCreateUserError
|
|
||||||
success: @onCreateUserSuccess
|
|
||||||
})
|
|
||||||
|
|
||||||
onCreateUserError: (model, jqxhr) =>
|
|
||||||
# really need to make our server errors uniform
|
|
||||||
if jqxhr.responseJSON
|
|
||||||
error = jqxhr.responseJSON
|
|
||||||
error = error[0] if _.isArray(error)
|
|
||||||
message = _.filter([error.property, error.message]).join(' ')
|
|
||||||
else
|
|
||||||
message = jqxhr.responseText
|
|
||||||
@disableModalInProgress(@$el)
|
|
||||||
@$('#errors-alert').text(message).removeClass('hide')
|
|
||||||
|
|
||||||
onCreateUserSuccess: =>
|
|
||||||
classCode = @$('#class-code-input').val()
|
|
||||||
if classCode
|
|
||||||
url = "/courses?_cc="+classCode
|
|
||||||
document.location.href = url
|
|
||||||
# This was a terrible hack to make navigating trigger when just adding query params
|
|
||||||
# application.router.navigate('/thisisahack')
|
|
||||||
# application.router.navigate(url, { trigger: true })
|
|
||||||
else
|
|
||||||
window.location.reload()
|
|
|
@ -154,16 +154,15 @@ module.exports = class CampaignEditorView extends RootView
|
||||||
|
|
||||||
propagateCampaignIndexes: ->
|
propagateCampaignIndexes: ->
|
||||||
campaignLevels = $.extend({}, @campaign.get('levels'))
|
campaignLevels = $.extend({}, @campaign.get('levels'))
|
||||||
|
|
||||||
index = 0
|
index = 0
|
||||||
for levelOriginal, campaignLevel of campaignLevels
|
for levelOriginal, campaignLevel of campaignLevels
|
||||||
level = @levels.findWhere({original: levelOriginal})
|
if @campaign.get('type') is 'course'
|
||||||
if level and level.get('campaignIndex') isnt index
|
level = @levels.findWhere({original: levelOriginal})
|
||||||
level.set('campaignIndex', index)
|
if level and level.get('campaignIndex') isnt index
|
||||||
|
level.set('campaignIndex', index)
|
||||||
campaignLevel.campaignIndex = index
|
campaignLevel.campaignIndex = index
|
||||||
index += 1
|
index += 1
|
||||||
|
@campaign.set('levels', campaignLevels)
|
||||||
@campaign.set('levels', campaignLevels)
|
|
||||||
|
|
||||||
onClickPatches: (e) ->
|
onClickPatches: (e) ->
|
||||||
@patchesView = @insertSubView(new PatchesView(@campaign), @$el.find('.patches-view'))
|
@patchesView = @insertSubView(new PatchesView(@campaign), @$el.find('.patches-view'))
|
||||||
|
|
|
@ -46,7 +46,7 @@ module.exports = class ThangComponentConfigView extends CocoView
|
||||||
schema.default ?= {}
|
schema.default ?= {}
|
||||||
_.merge schema.default, @additionalDefaults if @additionalDefaults
|
_.merge schema.default, @additionalDefaults if @additionalDefaults
|
||||||
|
|
||||||
if @level?.get('type', true) in ['hero', 'hero-ladder', 'hero-coop']
|
if @level?.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']
|
||||||
schema.required = []
|
schema.required = []
|
||||||
treemaOptions =
|
treemaOptions =
|
||||||
supermodel: @supermodel
|
supermodel: @supermodel
|
||||||
|
|
|
@ -20,7 +20,7 @@ module.exports = class NewLevelSystemModal extends ModalView
|
||||||
name = @$el.find('#level-system-name').val()
|
name = @$el.find('#level-system-name').val()
|
||||||
system = new LevelSystem()
|
system = new LevelSystem()
|
||||||
system.set 'name', name
|
system.set 'name', name
|
||||||
system.set 'code', system.get('code').replace(/Jitter/g, name)
|
system.set 'code', system.get('code', true).replace(/Jitter/g, name)
|
||||||
system.set 'permissions', [{access: 'owner', target: me.id}] # Private until saved in a published Level
|
system.set 'permissions', [{access: 'owner', target: me.id}] # Private until saved in a published Level
|
||||||
res = system.save(null, {type: 'POST'}) # Override PUT so we can trigger postFirstVersion logic
|
res = system.save(null, {type: 'POST'}) # Override PUT so we can trigger postFirstVersion logic
|
||||||
return unless res
|
return unless res
|
||||||
|
|
|
@ -41,7 +41,7 @@ module.exports = class LevelThangEditView extends CocoView
|
||||||
level: @level
|
level: @level
|
||||||
world: @world
|
world: @world
|
||||||
|
|
||||||
if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] then options.thangType = thangType
|
if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] then options.thangType = thangType
|
||||||
|
|
||||||
@thangComponentEditView = new ThangComponentsEditView options
|
@thangComponentEditView = new ThangComponentsEditView options
|
||||||
@listenTo @thangComponentEditView, 'components-changed', @onComponentsChanged
|
@listenTo @thangComponentEditView, 'components-changed', @onComponentsChanged
|
||||||
|
|
|
@ -585,14 +585,14 @@ module.exports = class ThangsTabView extends CocoView
|
||||||
if batchInsert
|
if batchInsert
|
||||||
if thangType.get('name') is 'Hero Placeholder'
|
if thangType.get('name') is 'Hero Placeholder'
|
||||||
thangID = 'Hero Placeholder'
|
thangID = 'Hero Placeholder'
|
||||||
return if not (@level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder']) or @getThangByID(thangID)
|
return if not (@level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']) or @getThangByID(thangID)
|
||||||
else
|
else
|
||||||
thangID = "Random #{thangType.get('name')} #{@thangsBatch.length}"
|
thangID = "Random #{thangType.get('name')} #{@thangsBatch.length}"
|
||||||
else
|
else
|
||||||
thangID = Thang.nextID(thangType.get('name'), @world) until thangID and not @getThangByID(thangID)
|
thangID = Thang.nextID(thangType.get('name'), @world) until thangID and not @getThangByID(thangID)
|
||||||
if @cloneSourceThang
|
if @cloneSourceThang
|
||||||
components = _.cloneDeep @getThangByID(@cloneSourceThang.id).components
|
components = _.cloneDeep @getThangByID(@cloneSourceThang.id).components
|
||||||
else if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder']
|
else if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']
|
||||||
components = [] # Load them all from default ThangType Components
|
components = [] # Load them all from default ThangType Components
|
||||||
else
|
else
|
||||||
components = _.cloneDeep thangType.get('components') ? []
|
components = _.cloneDeep thangType.get('components') ? []
|
||||||
|
|
|
@ -404,7 +404,7 @@ module.exports = class CampaignView extends RootView
|
||||||
particleKey.push 'hero' if level.unlocksHero and not level.unlockedHero
|
particleKey.push 'hero' if level.unlocksHero and not level.unlockedHero
|
||||||
#particleKey.push 'item' if level.slug is 'robot-ragnarok' # TODO: generalize
|
#particleKey.push 'item' if level.slug is 'robot-ragnarok' # TODO: generalize
|
||||||
continue if particleKey.length is 2 # Don't show basic levels
|
continue if particleKey.length is 2 # Don't show basic levels
|
||||||
continue unless level.hidden or _.intersection(particleKey, ['item', 'hero-ladder', 'replayable']).length
|
continue unless level.hidden or _.intersection(particleKey, ['item', 'hero-ladder', 'replayable', 'game-dev']).length
|
||||||
@particleMan.addEmitter level.position.x / 100, level.position.y / 100, particleKey.join('-')
|
@particleMan.addEmitter level.position.x / 100, level.position.y / 100, particleKey.join('-')
|
||||||
|
|
||||||
onMouseEnterPortals: (e) ->
|
onMouseEnterPortals: (e) ->
|
||||||
|
|
|
@ -61,7 +61,7 @@ module.exports = class ControlBarView extends CocoView
|
||||||
getRenderData: (c={}) ->
|
getRenderData: (c={}) ->
|
||||||
super c
|
super c
|
||||||
c.worldName = @worldName
|
c.worldName = @worldName
|
||||||
c.campaignIndex = @level.get('campaignIndex') + 1 if @level.get('type') is 'course' and @level.get('campaignIndex')?
|
c.campaignIndex = @level.get('campaignIndex') + 1 if @level.get('type') is 'course' and @level.get('campaignIndex')? # TODO: support 'game-dev' levels in courses
|
||||||
c.multiplayerEnabled = @session.get('multiplayer')
|
c.multiplayerEnabled = @session.get('multiplayer')
|
||||||
c.ladderGame = @level.get('type') in ['ladder', 'hero-ladder', 'course-ladder']
|
c.ladderGame = @level.get('type') in ['ladder', 'hero-ladder', 'course-ladder']
|
||||||
if c.isMultiplayerLevel = @isMultiplayerLevel
|
if c.isMultiplayerLevel = @isMultiplayerLevel
|
||||||
|
@ -104,6 +104,7 @@ module.exports = class ControlBarView extends CocoView
|
||||||
if @courseInstanceID
|
if @courseInstanceID
|
||||||
@homeLink += "/#{@courseInstanceID}"
|
@homeLink += "/#{@courseInstanceID}"
|
||||||
@homeViewArgs.push @courseInstanceID
|
@homeViewArgs.push @courseInstanceID
|
||||||
|
#else if @level.get('type', true) is 'game-dev' # TODO
|
||||||
else
|
else
|
||||||
@homeLink = '/'
|
@homeLink = '/'
|
||||||
@homeViewClass = 'views/HomeView'
|
@homeViewClass = 'views/HomeView'
|
||||||
|
|
|
@ -203,7 +203,7 @@ module.exports = class PlayLevelView extends RootView
|
||||||
@session = @levelLoader.session
|
@session = @levelLoader.session
|
||||||
@world = @levelLoader.world
|
@world = @levelLoader.world
|
||||||
@level = @levelLoader.level
|
@level = @levelLoader.level
|
||||||
@$el.addClass 'hero' if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder']
|
@$el.addClass 'hero' if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']
|
||||||
@$el.addClass 'flags' if _.any(@world.thangs, (t) -> (t.programmableProperties and 'findFlags' in t.programmableProperties) or t.inventory?.flag) or @level.get('slug') is 'sky-span'
|
@$el.addClass 'flags' if _.any(@world.thangs, (t) -> (t.programmableProperties and 'findFlags' in t.programmableProperties) or t.inventory?.flag) or @level.get('slug') is 'sky-span'
|
||||||
# TODO: Update terminology to always be opponentSession or otherSession
|
# TODO: Update terminology to always be opponentSession or otherSession
|
||||||
# TODO: E.g. if it's always opponent right now, then variable names should be opponentSession until we have coop play
|
# TODO: E.g. if it's always opponent right now, then variable names should be opponentSession until we have coop play
|
||||||
|
@ -463,7 +463,7 @@ module.exports = class PlayLevelView extends RootView
|
||||||
return false if $.browser?.msie or $.browser?.msedge
|
return false if $.browser?.msie or $.browser?.msedge
|
||||||
return false if $.browser.linux
|
return false if $.browser.linux
|
||||||
return false if me.level() < 8
|
return false if me.level() < 8
|
||||||
if levelType is 'course'
|
if levelType in ['course', 'game-dev']
|
||||||
return false
|
return false
|
||||||
else if levelType is 'hero' and gamesSimulated
|
else if levelType is 'hero' and gamesSimulated
|
||||||
return false if stillBuggy
|
return false if stillBuggy
|
||||||
|
@ -536,7 +536,7 @@ module.exports = class PlayLevelView extends RootView
|
||||||
onDonePressed: -> @showVictory()
|
onDonePressed: -> @showVictory()
|
||||||
|
|
||||||
onShowVictory: (e) ->
|
onShowVictory: (e) ->
|
||||||
$('#level-done-button').show() unless @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder']
|
$('#level-done-button').show() unless @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']
|
||||||
@showVictory() if e.showModal
|
@showVictory() if e.showModal
|
||||||
return if @victorySeen
|
return if @victorySeen
|
||||||
@victorySeen = true
|
@victorySeen = true
|
||||||
|
@ -554,7 +554,7 @@ module.exports = class PlayLevelView extends RootView
|
||||||
return if @level.hasLocalChanges() # Don't award achievements when beating level changed in level editor
|
return if @level.hasLocalChanges() # Don't award achievements when beating level changed in level editor
|
||||||
@endHighlight()
|
@endHighlight()
|
||||||
options = {level: @level, supermodel: @supermodel, session: @session, hasReceivedMemoryWarning: @hasReceivedMemoryWarning, courseID: @courseID, courseInstanceID: @courseInstanceID, world: @world}
|
options = {level: @level, supermodel: @supermodel, session: @session, hasReceivedMemoryWarning: @hasReceivedMemoryWarning, courseID: @courseID, courseInstanceID: @courseInstanceID, world: @world}
|
||||||
ModalClass = if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] then HeroVictoryModal else VictoryModal
|
ModalClass = if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] then HeroVictoryModal else VictoryModal
|
||||||
ModalClass = CourseVictoryModal if @isCourseMode() or me.isSessionless()
|
ModalClass = CourseVictoryModal if @isCourseMode() or me.isSessionless()
|
||||||
ModalClass = PicoCTFVictoryModal if window.serverConfig.picoCTF
|
ModalClass = PicoCTFVictoryModal if window.serverConfig.picoCTF
|
||||||
victoryModal = new ModalClass(options)
|
victoryModal = new ModalClass(options)
|
||||||
|
|
|
@ -49,7 +49,7 @@ module.exports = class HeroVictoryModal extends ModalView
|
||||||
@session = options.session
|
@session = options.session
|
||||||
@level = options.level
|
@level = options.level
|
||||||
@thangTypes = {}
|
@thangTypes = {}
|
||||||
if @level.get('type', true) in ['hero', 'hero-ladder', 'course', 'course-ladder']
|
if @level.get('type', true) in ['hero', 'hero-ladder', 'course', 'course-ladder', 'game-dev']
|
||||||
achievements = new CocoCollection([], {
|
achievements = new CocoCollection([], {
|
||||||
url: "/db/achievement?related=#{@session.get('level').original}"
|
url: "/db/achievement?related=#{@session.get('level').original}"
|
||||||
model: Achievement
|
model: Achievement
|
||||||
|
@ -73,6 +73,7 @@ module.exports = class HeroVictoryModal extends ModalView
|
||||||
if @level.get('type', true) in ['course', 'course-ladder']
|
if @level.get('type', true) in ['course', 'course-ladder']
|
||||||
@saveReviewEventually = _.debounce(@saveReviewEventually, 2000)
|
@saveReviewEventually = _.debounce(@saveReviewEventually, 2000)
|
||||||
@loadExistingFeedback()
|
@loadExistingFeedback()
|
||||||
|
# TODO: support game-dev
|
||||||
|
|
||||||
destroy: ->
|
destroy: ->
|
||||||
clearInterval @sequentialAnimationInterval
|
clearInterval @sequentialAnimationInterval
|
||||||
|
@ -153,7 +154,8 @@ module.exports = class HeroVictoryModal extends ModalView
|
||||||
getRenderData: ->
|
getRenderData: ->
|
||||||
c = super()
|
c = super()
|
||||||
c.levelName = utils.i18n @level.attributes, 'name'
|
c.levelName = utils.i18n @level.attributes, 'name'
|
||||||
if @level.get('type', true) isnt 'hero'
|
# TODO: support 'game-dev'
|
||||||
|
if @level.get('type', true) not in ['hero', 'game-dev']
|
||||||
c.victoryText = utils.i18n @level.get('victory') ? {}, 'body'
|
c.victoryText = utils.i18n @level.get('victory') ? {}, 'body'
|
||||||
earnedAchievementMap = _.indexBy(@newEarnedAchievements or [], (ea) -> ea.get('achievement'))
|
earnedAchievementMap = _.indexBy(@newEarnedAchievements or [], (ea) -> ea.get('achievement'))
|
||||||
for achievement in (@achievements?.models or [])
|
for achievement in (@achievements?.models or [])
|
||||||
|
@ -221,7 +223,7 @@ module.exports = class HeroVictoryModal extends ModalView
|
||||||
|
|
||||||
afterRender: ->
|
afterRender: ->
|
||||||
super()
|
super()
|
||||||
@$el.toggleClass 'with-achievements', @level.get('type', true) in ['hero', 'hero-ladder']
|
@$el.toggleClass 'with-achievements', @level.get('type', true) in ['hero', 'hero-ladder', 'game-dev'] # TODO: support game-dev
|
||||||
return unless @supermodel.finished()
|
return unless @supermodel.finished()
|
||||||
@playSelectionSound hero, true for original, hero of @thangTypes # Preload them
|
@playSelectionSound hero, true for original, hero of @thangTypes # Preload them
|
||||||
@updateSavingProgressStatus()
|
@updateSavingProgressStatus()
|
||||||
|
@ -231,7 +233,7 @@ module.exports = class HeroVictoryModal extends ModalView
|
||||||
@insertSubView @ladderSubmissionView, @$el.find('.ladder-submission-view')
|
@insertSubView @ladderSubmissionView, @$el.find('.ladder-submission-view')
|
||||||
|
|
||||||
initializeAnimations: ->
|
initializeAnimations: ->
|
||||||
return @endSequentialAnimations() unless @level.get('type', true) in ['hero', 'hero-ladder']
|
return @endSequentialAnimations() unless @level.get('type', true) in ['hero', 'hero-ladder', 'game-dev'] # TODO: support game-dev
|
||||||
@updateXPBars 0
|
@updateXPBars 0
|
||||||
#playVictorySound = => @playSound 'victory-title-appear' # TODO: actually add this
|
#playVictorySound = => @playSound 'victory-title-appear' # TODO: actually add this
|
||||||
@$el.find('#victory-header').delay(250).queue(->
|
@$el.find('#victory-header').delay(250).queue(->
|
||||||
|
@ -262,7 +264,7 @@ module.exports = class HeroVictoryModal extends ModalView
|
||||||
|
|
||||||
beginSequentialAnimations: ->
|
beginSequentialAnimations: ->
|
||||||
return if @destroyed
|
return if @destroyed
|
||||||
return unless @level.get('type', true) in ['hero', 'hero-ladder']
|
return unless @level.get('type', true) in ['hero', 'hero-ladder', 'game-dev'] # TODO: support game-dev
|
||||||
@sequentialAnimatedPanels = _.map(@animatedPanels.find('.reward-panel'), (panel) -> {
|
@sequentialAnimatedPanels = _.map(@animatedPanels.find('.reward-panel'), (panel) -> {
|
||||||
number: $(panel).data('number')
|
number: $(panel).data('number')
|
||||||
previousNumber: $(panel).data('previous-number')
|
previousNumber: $(panel).data('previous-number')
|
||||||
|
|
|
@ -165,7 +165,7 @@ module.exports = class Spell
|
||||||
writable = @permissions.readwrite.length > 0
|
writable = @permissions.readwrite.length > 0
|
||||||
skipProtectAPI = @skipProtectAPI or not writable
|
skipProtectAPI = @skipProtectAPI or not writable
|
||||||
problemContext = @createProblemContext thang
|
problemContext = @createProblemContext thang
|
||||||
includeFlow = (@levelType in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder']) and not skipProtectAPI
|
includeFlow = (@levelType in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']) and not skipProtectAPI
|
||||||
aetherOptions = createAetherOptions
|
aetherOptions = createAetherOptions
|
||||||
functionName: @name
|
functionName: @name
|
||||||
codeLanguage: @language
|
codeLanguage: @language
|
||||||
|
|
|
@ -37,7 +37,7 @@ module.exports = class SpellListEntryView extends CocoView
|
||||||
context
|
context
|
||||||
|
|
||||||
createMethodSignature: ->
|
createMethodSignature: ->
|
||||||
return @spell.name if @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder']
|
return @spell.name if @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']
|
||||||
parameters = (@spell.parameters or []).slice()
|
parameters = (@spell.parameters or []).slice()
|
||||||
if @spell.language in ['python', 'lua']
|
if @spell.language in ['python', 'lua']
|
||||||
parameters.unshift 'self'
|
parameters.unshift 'self'
|
||||||
|
|
|
@ -84,7 +84,7 @@ module.exports = class SpellPaletteEntryView extends CocoView
|
||||||
Backbone.Mediator.publish 'tome:palette-pin-toggled', entry: @, pinned: @popoverPinned
|
Backbone.Mediator.publish 'tome:palette-pin-toggled', entry: @, pinned: @popoverPinned
|
||||||
|
|
||||||
onClick: (e) =>
|
onClick: (e) =>
|
||||||
if true or @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder']
|
if true or @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']
|
||||||
# Jiggle instead of pin for hero levels
|
# Jiggle instead of pin for hero levels
|
||||||
# Actually, do it all the time, because we recently busted the pin CSS. TODO: restore pinning
|
# Actually, do it all the time, because we recently busted the pin CSS. TODO: restore pinning
|
||||||
jigglyPopover = $('.spell-palette-popover.popover')
|
jigglyPopover = $('.spell-palette-popover.popover')
|
||||||
|
|
|
@ -163,7 +163,7 @@ module.exports = class SpellPaletteView extends CocoView
|
||||||
else
|
else
|
||||||
propStorage =
|
propStorage =
|
||||||
'this': ['apiProperties', 'apiMethods']
|
'this': ['apiProperties', 'apiMethods']
|
||||||
if not (@options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder']) or not @options.programmable
|
if not (@options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']) or not @options.programmable
|
||||||
@organizePalette propStorage, allDocs, excludedDocs
|
@organizePalette propStorage, allDocs, excludedDocs
|
||||||
else
|
else
|
||||||
@organizePaletteHero propStorage, allDocs, excludedDocs
|
@organizePaletteHero propStorage, allDocs, excludedDocs
|
||||||
|
@ -205,7 +205,7 @@ module.exports = class SpellPaletteView extends CocoView
|
||||||
if tabbify and _.find @entries, ((entry) -> entry.doc.owner isnt 'this')
|
if tabbify and _.find @entries, ((entry) -> entry.doc.owner isnt 'this')
|
||||||
@entryGroups = _.groupBy @entries, groupForEntry
|
@entryGroups = _.groupBy @entries, groupForEntry
|
||||||
else
|
else
|
||||||
i18nKey = if @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] then 'play_level.tome_your_skills' else 'play_level.tome_available_spells'
|
i18nKey = if @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] then 'play_level.tome_your_skills' else 'play_level.tome_available_spells'
|
||||||
defaultGroup = $.i18n.t i18nKey
|
defaultGroup = $.i18n.t i18nKey
|
||||||
@entryGroups = {}
|
@entryGroups = {}
|
||||||
@entryGroups[defaultGroup] = @entries
|
@entryGroups[defaultGroup] = @entries
|
||||||
|
|
|
@ -635,7 +635,7 @@ module.exports = class SpellView extends CocoView
|
||||||
@createToolbarView()
|
@createToolbarView()
|
||||||
|
|
||||||
createDebugView: ->
|
createDebugView: ->
|
||||||
return if @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] # We'll turn this on later, maybe, but not yet.
|
return if @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] # We'll turn this on later, maybe, but not yet.
|
||||||
@debugView = new SpellDebugView ace: @ace, thang: @thang, spell:@spell
|
@debugView = new SpellDebugView ace: @ace, thang: @thang, spell:@spell
|
||||||
@$el.append @debugView.render().$el.hide()
|
@$el.append @debugView.render().$el.hide()
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@ module.exports = class TomeView extends CocoView
|
||||||
@worker = @createWorker()
|
@worker = @createWorker()
|
||||||
programmableThangs = _.filter @options.thangs, (t) -> t.isProgrammable and t.programmableMethods
|
programmableThangs = _.filter @options.thangs, (t) -> t.isProgrammable and t.programmableMethods
|
||||||
@createSpells programmableThangs, programmableThangs[0]?.world # Do before spellList, thangList, and castButton
|
@createSpells programmableThangs, programmableThangs[0]?.world # Do before spellList, thangList, and castButton
|
||||||
unless @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder']
|
unless @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']
|
||||||
@spellList = @insertSubView new SpellListView spells: @spells, supermodel: @supermodel, level: @options.level
|
@spellList = @insertSubView new SpellListView spells: @spells, supermodel: @supermodel, level: @options.level
|
||||||
@castButton = @insertSubView new CastButtonView spells: @spells, level: @options.level, session: @options.session, god: @options.god
|
@castButton = @insertSubView new CastButtonView spells: @spells, level: @options.level, session: @options.session, god: @options.god
|
||||||
@teamSpellMap = @generateTeamSpellMap(@spells)
|
@teamSpellMap = @generateTeamSpellMap(@spells)
|
||||||
|
@ -193,7 +193,7 @@ module.exports = class TomeView extends CocoView
|
||||||
@castButton?.$el.hide()
|
@castButton?.$el.hide()
|
||||||
|
|
||||||
onSpriteSelected: (e) ->
|
onSpriteSelected: (e) ->
|
||||||
return if @spellView and @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] # Never deselect the hero in the Tome.
|
return if @spellView and @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] # Never deselect the hero in the Tome.
|
||||||
thang = e.thang
|
thang = e.thang
|
||||||
spellName = e.spellName
|
spellName = e.spellName
|
||||||
@spellList?.$el.hide()
|
@spellList?.$el.hide()
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
"firepad": "~0.1.2",
|
"firepad": "~0.1.2",
|
||||||
"marked": "~0.3.0",
|
"marked": "~0.3.0",
|
||||||
"moment": "~2.5.0",
|
"moment": "~2.5.0",
|
||||||
"aether": "~0.5.0",
|
"aether": "~0.5.6",
|
||||||
"underscore.string": "~2.3.3",
|
"underscore.string": "~2.3.3",
|
||||||
"firebase": "~1.0.2",
|
"firebase": "~1.0.2",
|
||||||
"d3": "~3.4.4",
|
"d3": "~3.4.4",
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"JQDeferred": "~2.1.0",
|
"JQDeferred": "~2.1.0",
|
||||||
"ace-builds": "https://github.com/ajaxorg/ace-builds/archive/3fb55e8e374ab02ce47c1ae55ffb60a1835f3055.tar.gz",
|
"ace-builds": "https://github.com/ajaxorg/ace-builds/archive/3fb55e8e374ab02ce47c1ae55ffb60a1835f3055.tar.gz",
|
||||||
"aether": "~0.5.0",
|
"aether": "~0.5.6",
|
||||||
"async": "0.2.x",
|
"async": "0.2.x",
|
||||||
"aws-sdk": "~2.0.0",
|
"aws-sdk": "~2.0.0",
|
||||||
"bayesian-battle": "0.0.7",
|
"bayesian-battle": "0.0.7",
|
||||||
|
|
|
@ -17,16 +17,17 @@ module.exports.setup = ->
|
||||||
authentication.use(new LocalStrategy(
|
authentication.use(new LocalStrategy(
|
||||||
(username, password, done) ->
|
(username, password, done) ->
|
||||||
|
|
||||||
# kind of a hacky way to make it possible for iPads to 'log in' with their unique device id
|
# TODO: Add special iPad login endpoint. There was some logic here for the old, hacky method,
|
||||||
if username.length is 36 and '@' not in username # must be an identifier for vendor
|
# but was removed for username login
|
||||||
q = { iosIdentifierForVendor: username }
|
q = { $or: [
|
||||||
else
|
{ emailLower: username.toLowerCase() }
|
||||||
q = { emailLower: username.toLowerCase() }
|
{ slug: _.str.slugify(username) }
|
||||||
|
]}
|
||||||
|
|
||||||
User.findOne(q).exec((err, user) ->
|
User.findOne(q).exec((err, user) ->
|
||||||
return done(err) if err
|
return done(err) if err
|
||||||
if not user
|
if not user
|
||||||
return done(new errors.Unauthorized('not found', { property: 'email' }))
|
return done(new errors.Unauthorized('not found', { errorID: 'not-found' }))
|
||||||
passwordReset = (user.get('passwordReset') or '').toLowerCase()
|
passwordReset = (user.get('passwordReset') or '').toLowerCase()
|
||||||
if passwordReset and password.toLowerCase() is passwordReset
|
if passwordReset and password.toLowerCase() is passwordReset
|
||||||
User.update {_id: user.get('_id')}, {$unset: {passwordReset: ''}}, {}, ->
|
User.update {_id: user.get('_id')}, {$unset: {passwordReset: ''}}, {}, ->
|
||||||
|
@ -34,7 +35,7 @@ module.exports.setup = ->
|
||||||
|
|
||||||
hash = User.hashPassword(password)
|
hash = User.hashPassword(password)
|
||||||
unless user.get('passwordHash') is hash
|
unless user.get('passwordHash') is hash
|
||||||
return done(new errors.Unauthorized('is wrong', { property: 'password' }))
|
return done(new errors.Unauthorized('is wrong', { errorID: 'wrong-password' }))
|
||||||
return done(null, user)
|
return done(null, user)
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
|
|
|
@ -92,6 +92,10 @@ errorResponseSchema = {
|
||||||
type: 'string'
|
type: 'string'
|
||||||
description: 'Provided for /auth/name.' # TODO: refactor out
|
description: 'Provided for /auth/name.' # TODO: refactor out
|
||||||
}
|
}
|
||||||
|
errorID: {
|
||||||
|
type: 'string'
|
||||||
|
description: 'Error id to be used by the client to handle specific errors'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
errorProps = _.keys(errorResponseSchema.properties)
|
errorProps = _.keys(errorResponseSchema.properties)
|
||||||
|
|
|
@ -65,7 +65,7 @@ module.exports =
|
||||||
activities = JSON.parse(body)
|
activities = JSON.parse(body)
|
||||||
return done("Unexpected activities format: " + body) unless activities.data?
|
return done("Unexpected activities format: " + body) unless activities.data?
|
||||||
for activity in activities.data when activity._type is 'Email'
|
for activity in activities.data when activity._type is 'Email'
|
||||||
if /@codecombat\.(?:com)|(?:nl)$/ig.test(activity.sender) and not activity.sender?.indexOf(config.mail.username) >= 0
|
if /@codecombat\.(?:com)|(?:nl)/ig.test(activity.sender) and not activity.sender?.indexOf(config.mail.username) >= 0
|
||||||
return done(null, activity.sender, lead.id)
|
return done(null, activity.sender, lead.id)
|
||||||
return done(null, config.mail.supportSchools, lead.id)
|
return done(null, config.mail.supportSchools, lead.id)
|
||||||
catch error
|
catch error
|
||||||
|
|
|
@ -24,18 +24,6 @@ describe 'POST /auth/login', ->
|
||||||
yield utils.becomeAnonymous()
|
yield utils.becomeAnonymous()
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it 'allows logging in by iosIdentifierForVendor', utils.wrap (done) ->
|
|
||||||
yield utils.initUser({
|
|
||||||
'iosIdentifierForVendor': '012345678901234567890123456789012345'
|
|
||||||
'password': '12345'
|
|
||||||
})
|
|
||||||
[res, body] = yield request.postAsync({uri: urlLogin, json: {
|
|
||||||
username: '012345678901234567890123456789012345'
|
|
||||||
password: '12345'
|
|
||||||
}})
|
|
||||||
expect(res.statusCode).toBe(200)
|
|
||||||
done()
|
|
||||||
|
|
||||||
it 'returns 401 when the user does not exist', utils.wrap (done) ->
|
it 'returns 401 when the user does not exist', utils.wrap (done) ->
|
||||||
[res, body] = yield request.postAsync({uri: urlLogin, json: {
|
[res, body] = yield request.postAsync({uri: urlLogin, json: {
|
||||||
username: 'some@email.com'
|
username: 'some@email.com'
|
||||||
|
@ -55,6 +43,19 @@ describe 'POST /auth/login', ->
|
||||||
}})
|
}})
|
||||||
expect(res.statusCode).toBe(200)
|
expect(res.statusCode).toBe(200)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
it 'allows login by username', utils.wrap (done) ->
|
||||||
|
yield utils.initUser({
|
||||||
|
name: 'Some name that will be lowercased...'
|
||||||
|
'email': 'some@email.com'
|
||||||
|
'password': '12345'
|
||||||
|
})
|
||||||
|
[res, body] = yield request.postAsync({uri: urlLogin, json: {
|
||||||
|
username: 'Some name that will be lowercased...'
|
||||||
|
password: '12345'
|
||||||
|
}})
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
done()
|
||||||
|
|
||||||
it 'rejects wrong passwords', utils.wrap (done) ->
|
it 'rejects wrong passwords', utils.wrap (done) ->
|
||||||
yield utils.initUser({
|
yield utils.initUser({
|
||||||
|
|
|
@ -15,7 +15,6 @@ describe 'LevelSystem', ->
|
||||||
codeLanguage: 'coffeescript'
|
codeLanguage: 'coffeescript'
|
||||||
permissions: simplePermissions
|
permissions: simplePermissions
|
||||||
dependencies: []
|
dependencies: []
|
||||||
propertyDocumentation: []
|
|
||||||
|
|
||||||
systems = {}
|
systems = {}
|
||||||
|
|
||||||
|
@ -80,7 +79,6 @@ describe 'LevelSystem', ->
|
||||||
expect(body.original).toBeDefined()
|
expect(body.original).toBeDefined()
|
||||||
expect(body.created).toBeDefined()
|
expect(body.created).toBeDefined()
|
||||||
expect(body.dependencies).toBeDefined()
|
expect(body.dependencies).toBeDefined()
|
||||||
expect(body.propertyDocumentation).toBeDefined()
|
|
||||||
expect(body.version.isLatestMajor).toBe(true)
|
expect(body.version.isLatestMajor).toBe(true)
|
||||||
expect(body.version.isLatestMinor).toBe(true)
|
expect(body.version.isLatestMinor).toBe(true)
|
||||||
expect(body.permissions).toBeDefined()
|
expect(body.permissions).toBeDefined()
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
HeroSelectModal = require 'views/courses/HeroSelectModal'
|
HeroSelectModal = require 'views/courses/HeroSelectModal'
|
||||||
auth = require 'core/auth'
|
|
||||||
factories = require 'test/app/factories'
|
factories = require 'test/app/factories'
|
||||||
|
|
||||||
describe 'HeroSelectModal', ->
|
describe 'HeroSelectModal', ->
|
||||||
|
@ -14,7 +13,6 @@ describe 'HeroSelectModal', ->
|
||||||
|
|
||||||
beforeEach (done) ->
|
beforeEach (done) ->
|
||||||
window.me = user = factories.makeUser({ heroConfig: { thangType: hero1.get('original') } })
|
window.me = user = factories.makeUser({ heroConfig: { thangType: hero1.get('original') } })
|
||||||
auth.loginUser(user.attributes)
|
|
||||||
modal = new HeroSelectModal({ currentHeroID: hero1.id })
|
modal = new HeroSelectModal({ currentHeroID: hero1.id })
|
||||||
modal.heroes.fakeRequests[0].respondWith({ status: 200, responseText: heroesResponse })
|
modal.heroes.fakeRequests[0].respondWith({ status: 200, responseText: heroesResponse })
|
||||||
jasmine.demoModal(modal)
|
jasmine.demoModal(modal)
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
StudentLoginModal = require 'views/courses/StudentLogInModal'
|
|
||||||
RecoverModal = require 'views/core/RecoverModal'
|
|
||||||
auth = require 'core/auth'
|
|
||||||
|
|
||||||
describe 'StudentLoginModal', ->
|
|
||||||
|
|
||||||
modal = null
|
|
||||||
|
|
||||||
beforeEach ->
|
|
||||||
modal = new StudentLoginModal()
|
|
||||||
modal.render()
|
|
||||||
|
|
||||||
afterEach ->
|
|
||||||
modal.stopListening()
|
|
||||||
|
|
||||||
it 'displays an error when you submit an empty login form', ->
|
|
||||||
spyOn(auth, 'loginUser').and.callFake (data, callback) ->
|
|
||||||
callback { status: 401, responseText: "Unauthorized" }
|
|
||||||
modal.$el.find('#log-in-btn').click()
|
|
||||||
expect(modal.$el.html()).toContain('Wrong username or password. Try again!')
|
|
||||||
|
|
||||||
jasmine.demoModal(modal)
|
|
Loading…
Reference in a new issue