diff --git a/app/assets/images/pages/about/lisa_small.png b/app/assets/images/pages/about/lisa_small.png new file mode 100644 index 000000000..08fba0a64 Binary files /dev/null and b/app/assets/images/pages/about/lisa_small.png differ diff --git a/app/core/ParticleMan.coffee b/app/core/ParticleMan.coffee index b63f5d8a4..bcaebd72b 100644 --- a/app/core/ParticleMan.coffee +++ b/app/core/ParticleMan.coffee @@ -239,6 +239,12 @@ particleKinds['level-dungeon-replayable'] = particleKinds['level-dungeon-replaya colorMiddle: hsl 0.17, 0.75, 0.5 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'], emitter: particleCount: 2000 @@ -288,6 +294,12 @@ particleKinds['level-forest-replayable'] = particleKinds['level-forest-replayabl colorMiddle: hsl 0.17, 0.75, 0.5 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'], emitter: particleCount: 2000 @@ -337,6 +349,12 @@ particleKinds['level-desert-replayable'] = particleKinds['level-desert-replayabl colorMiddle: hsl 0.17, 0.75, 0.5 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'], emitter: particleCount: 200 @@ -371,6 +389,12 @@ particleKinds['level-mountain-replayable'] = particleKinds['level-mountain-repla colorMiddle: hsl 0.17, 0.75, 0.5 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'], emitter: particleCount: 200 @@ -405,6 +429,12 @@ particleKinds['level-glacier-replayable'] = particleKinds['level-glacier-replaya colorMiddle: hsl 0.17, 0.75, 0.5 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'], emitter: particleCount: 200 @@ -438,3 +468,9 @@ particleKinds['level-volcano-replayable'] = particleKinds['level-volcano-replaya colorStart: hsl 0.17, 0.75, 0.7 colorMiddle: hsl 0.17, 0.75, 0.5 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 diff --git a/app/core/auth.coffee b/app/core/auth.coffee index 79c9f8ade..0056b91ad 100644 --- a/app/core/auth.coffee +++ b/app/core/auth.coffee @@ -14,40 +14,6 @@ init = -> 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 = -> # TODO: Refactor to use User.logout FB?.logout?() diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 3e409016e..60842638d 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -243,6 +243,7 @@ login: sign_up: "Create Account" + email_or_username: "Email or username" log_in: "Log In" logging_in: "Logging In" log_out: "Log Out" diff --git a/app/models/Level.coffee b/app/models/Level.coffee index 166b7b11d..b284f1dda 100644 --- a/app/models/Level.coffee +++ b/app/models/Level.coffee @@ -58,7 +58,7 @@ module.exports = class Level extends CocoModel denormalize: (supermodel, session, otherSession) -> 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')?) thangTypesByOriginal = _.indexBy thangTypesWithComponents, (tt) -> tt.get('original') # Optimization for levelThang in o.thangs diff --git a/app/models/User.coffee b/app/models/User.coffee index 678297279..6f7e98c2a 100644 --- a/app/models/User.coffee +++ b/app/models/User.coffee @@ -275,6 +275,13 @@ module.exports = class User extends CocoModel options.data.facebookAccessToken = application.facebookHandler.token() @fetch(options) + loginPasswordUser: (usernameOrEmail, password, options={}) -> + options.url = '/auth/login' + options.type = 'POST' + options.data ?= {} + _.extend(options.data, { username: usernameOrEmail, password }) + @fetch(options) + makeCoursePrepaid: -> coursePrepaid = @get('coursePrepaid') return null unless coursePrepaid diff --git a/app/schemas/models/campaign.schema.coffee b/app/schemas/models/campaign.schema.coffee index 770a2f81b..182d62771 100644 --- a/app/schemas/models/campaign.schema.coffee +++ b/app/schemas/models/campaign.schema.coffee @@ -61,7 +61,7 @@ _.extend CampaignSchema.properties, { i18n: { type: 'object', format: 'hidden' } requiresSubscription: { 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' } original: { type: 'string', format: 'hidden' } adventurer: { type: 'boolean' } diff --git a/app/schemas/models/level.coffee b/app/schemas/models/level.coffee index 70930526b..a6f7a42e7 100644 --- a/app/schemas/models/level.coffee +++ b/app/schemas/models/level.coffee @@ -306,7 +306,7 @@ _.extend LevelSchema.properties, icon: {type: 'string', format: 'image-file', title: 'Icon'} 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 - 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 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'} diff --git a/app/schemas/models/level_component.coffee b/app/schemas/models/level_component.coffee index 086b680bb..bb1e07f48 100644 --- a/app/schemas/models/level_component.coffee +++ b/app/schemas/models/level_component.coffee @@ -10,7 +10,7 @@ class AttacksSelf extends Component systems = [ 'action', 'ai', 'alliance', 'collision', 'combat', 'display', 'event', 'existence', 'hearing', 'inventory', 'movement', 'programming', 'targeting', 'ui', 'vision', 'misc', 'physics', 'effect', - 'magic' + 'magic', 'game' ] PropertyDocumentationSchema = c.object { diff --git a/app/schemas/models/level_system.coffee b/app/schemas/models/level_system.coffee index 218e4a0e0..7a85de552 100644 --- a/app/schemas/models/level_system.coffee +++ b/app/schemas/models/level_system.coffee @@ -18,21 +18,6 @@ class Jitter extends System 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 { title: 'System Dependency' description: 'A System upon which this System depends.' @@ -50,14 +35,14 @@ DependencySchema = c.object { LevelSystemSchema = c.object { title: 'System' description: 'A System which can affect Level behavior.' - required: ['name', 'description', 'code', 'dependencies', 'propertyDocumentation', 'codeLanguage'] + required: ['name', 'code'] default: name: 'JitterSystem' description: 'This System makes all idle, movable Thangs jitter around.' code: jitterSystemCode codeLanguage: 'coffeescript' dependencies: [] # TODO: should depend on something by default - propertyDocumentation: [] + configSchema: {} } c.extendNamedProperties LevelSystemSchema # let's have the name be the first property LevelSystemSchema.properties.name.pattern = c.classNamePattern @@ -83,7 +68,6 @@ _.extend LevelSystemSchema.properties, type: 'string' format: 'hidden' 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}} official: type: 'boolean' diff --git a/app/styles/courses/hour-of-code-view.sass b/app/styles/courses/hour-of-code-view.sass deleted file mode 100644 index 1f66367e6..000000000 --- a/app/styles/courses/hour-of-code-view.sass +++ /dev/null @@ -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 \ No newline at end of file diff --git a/app/styles/courses/student-log-in-modal.sass b/app/styles/courses/student-log-in-modal.sass deleted file mode 100644 index 0fd2e0cf0..000000000 --- a/app/styles/courses/student-log-in-modal.sass +++ /dev/null @@ -1,7 +0,0 @@ -#student-log-in-modal - #log-in-btn - min-width: 30% - margin-bottom: 10px - - .form - margin: 0 25% \ No newline at end of file diff --git a/app/styles/courses/student-sign-up-modal.sass b/app/styles/courses/student-sign-up-modal.sass deleted file mode 100644 index 6d06268f8..000000000 --- a/app/styles/courses/student-sign-up-modal.sass +++ /dev/null @@ -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 diff --git a/app/styles/modal/auth-modal.sass b/app/styles/modal/auth-modal.sass index a9ec10461..98805ab74 100644 --- a/app/styles/modal/auth-modal.sass +++ b/app/styles/modal/auth-modal.sass @@ -143,7 +143,7 @@ //- Primary auth button - #login-button + #login-btn position: absolute top: 186px height: 70px diff --git a/app/templates/about.jade b/app/templates/about.jade index e6693ee99..619dc241b 100644 --- a/app/templates/about.jade +++ b/app/templates/about.jade @@ -139,6 +139,13 @@ block content small(data-i18n="about.elliot_title") 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 li a(href="http://floor.is/lava/" rel="external") diff --git a/app/templates/core/auth.jade b/app/templates/core/auth-modal.jade similarity index 64% rename from app/templates/core/auth.jade rename to app/templates/core/auth-modal.jade index 1b65b9e09..8bcc27620 100644 --- a/app/templates/core/auth.jade +++ b/app/templates/core/auth-modal.jade @@ -3,7 +3,7 @@ img(src="/images/pages/modal/auth/login-background.png", draggable="false").auth-modal-background h1(data-i18n="login.log_in") - div#close-modal + #close-modal span.glyphicon.glyphicon-remove .auth-form-content @@ -11,24 +11,40 @@ if showRequiredError .alert.alert-success span(data-i18n="signup.required") + + #unknown-error-alert.alert.alert-danger.hide(data-i18n="loading_error.unknown") form.form .form-group - label.control-label(for="email") - span(data-i18n="general.email") + label.control-label(for="username-or-email-input") + span(data-i18n="login.email_or_username") | : .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 - div#recover-account-wrapper - a(data-toggle="coco-modal", data-target="core/RecoverModal", data-i18n="login.forgot_password")#link-to-recover + #recover-account-wrapper + a#link-to-recover( + data-toggle="coco-modal" + data-target="core/RecoverModal" + data-i18n="login.forgot_password" + ) label.control-label(for="password") span(data-i18n="general.password") | : .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 h3(data-i18n="login.logging_in") diff --git a/app/templates/courses/hour-of-code-view.jade b/app/templates/courses/hour-of-code-view.jade deleted file mode 100644 index c37ecdce1..000000000 --- a/app/templates/courses/hour-of-code-view.jade +++ /dev/null @@ -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") diff --git a/app/templates/courses/student-log-in-modal.jade b/app/templates/courses/student-log-in-modal.jade deleted file mode 100644 index 60ce36877..000000000 --- a/app/templates/courses/student-log-in-modal.jade +++ /dev/null @@ -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 diff --git a/app/templates/courses/student-sign-up-modal.jade b/app/templates/courses/student-sign-up-modal.jade deleted file mode 100644 index 8454e228d..000000000 --- a/app/templates/courses/student-sign-up-modal.jade +++ /dev/null @@ -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 diff --git a/app/views/core/AuthModal.coffee b/app/views/core/AuthModal.coffee index 0791d396c..13609655a 100644 --- a/app/views/core/AuthModal.coffee +++ b/app/views/core/AuthModal.coffee @@ -1,6 +1,5 @@ ModalView = require 'views/core/ModalView' -template = require 'templates/core/auth' -{loginUser, createUser, me} = require 'core/auth' +template = require 'templates/core/auth-modal' forms = require 'core/forms' User = require 'models/User' application = require 'core/application' @@ -12,15 +11,11 @@ module.exports = class AuthModal extends ModalView events: 'click #switch-to-signup-btn': 'onSignupInstead' - 'click #github-login-button': 'onGitHubLoginClicked' 'submit form': 'onSubmitForm' 'keyup #name': 'onNameChange' 'click #gplus-login-btn': 'onClickGPlusLoginButton' 'click #facebook-login-btn': 'onClickFacebookLoginButton' 'click #close-modal': 'hide' - - subscriptions: - 'errors:server-error': 'onServerError' # Initialization @@ -32,15 +27,6 @@ module.exports = class AuthModal extends ModalView application.gplusHandler.loadAPI({ success: => _.defer => @$('#gplus-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: -> super() @playSound 'game-menu-open' @@ -58,16 +44,30 @@ module.exports = class AuthModal extends ModalView @playSound 'menu-button-click' e.preventDefault() forms.clearFormAlerts(@$el) + @$('#unknown-error-alert').addClass('hide') userObject = forms.formToObject @$el res = tv4.validateMultiple userObject, formSchema return forms.applyErrorsToForm(@$el, res.errors) unless res.valid - @enableModalInProgress(@$el) # TODO: part of forms - loginUser userObject, null, window.nextURL - - onServerError: (e) -> # TODO: work error handling into a separate forms system - @disableModalInProgress(@$el) - - + new Promise(me.loginPasswordUser(userObject.emailOrUsername, userObject.password).then) + .then(-> + if window.nextURL then window.location.href = window.nextURL else window.location.reload() + ) + .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 onClickGPlusLoginButton: -> @@ -136,6 +136,14 @@ module.exports = class AuthModal extends ModalView formSchema = { type: 'object' - properties: _.pick(User.schema.properties, 'email', 'password') - required: ['email', 'password'] + properties: { + emailOrUsername: { + $or: [ + User.schema.properties.name + User.schema.properties.email + ] + } + password: User.schema.properties.password + } + required: ['emailOrUsername', 'password'] } diff --git a/app/views/core/CreateAccountModal.coffee b/app/views/core/CreateAccountModal.coffee index c7150695c..440fe1a1b 100644 --- a/app/views/core/CreateAccountModal.coffee +++ b/app/views/core/CreateAccountModal.coffee @@ -1,6 +1,5 @@ ModalView = require 'views/core/ModalView' template = require 'templates/core/create-account-modal' -{loginUser, createUser, me} = require 'core/auth' forms = require 'core/forms' User = require 'models/User' application = require 'core/application' diff --git a/app/views/courses/HourOfCodeView.coffee b/app/views/courses/HourOfCodeView.coffee deleted file mode 100644 index 3f1ce8464..000000000 --- a/app/views/courses/HourOfCodeView.coffee +++ /dev/null @@ -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($('')) - 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() diff --git a/app/views/courses/StudentLogInModal.coffee b/app/views/courses/StudentLogInModal.coffee deleted file mode 100644 index 38ceda440..000000000 --- a/app/views/courses/StudentLogInModal.coffee +++ /dev/null @@ -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 diff --git a/app/views/courses/StudentSignUpModal.coffee b/app/views/courses/StudentSignUpModal.coffee deleted file mode 100644 index 76d2307ad..000000000 --- a/app/views/courses/StudentSignUpModal.coffee +++ /dev/null @@ -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() diff --git a/app/views/editor/campaign/CampaignEditorView.coffee b/app/views/editor/campaign/CampaignEditorView.coffee index 17b182574..4cff9c845 100644 --- a/app/views/editor/campaign/CampaignEditorView.coffee +++ b/app/views/editor/campaign/CampaignEditorView.coffee @@ -154,16 +154,15 @@ module.exports = class CampaignEditorView extends RootView propagateCampaignIndexes: -> campaignLevels = $.extend({}, @campaign.get('levels')) - index = 0 for levelOriginal, campaignLevel of campaignLevels - level = @levels.findWhere({original: levelOriginal}) - if level and level.get('campaignIndex') isnt index - level.set('campaignIndex', index) + if @campaign.get('type') is 'course' + level = @levels.findWhere({original: levelOriginal}) + if level and level.get('campaignIndex') isnt index + level.set('campaignIndex', index) campaignLevel.campaignIndex = index index += 1 - - @campaign.set('levels', campaignLevels) + @campaign.set('levels', campaignLevels) onClickPatches: (e) -> @patchesView = @insertSubView(new PatchesView(@campaign), @$el.find('.patches-view')) diff --git a/app/views/editor/component/ThangComponentConfigView.coffee b/app/views/editor/component/ThangComponentConfigView.coffee index ba95de63c..dc5498b41 100644 --- a/app/views/editor/component/ThangComponentConfigView.coffee +++ b/app/views/editor/component/ThangComponentConfigView.coffee @@ -46,7 +46,7 @@ module.exports = class ThangComponentConfigView extends CocoView schema.default ?= {} _.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 = [] treemaOptions = supermodel: @supermodel diff --git a/app/views/editor/level/systems/NewLevelSystemModal.coffee b/app/views/editor/level/systems/NewLevelSystemModal.coffee index 193a50c5d..e44996ab5 100644 --- a/app/views/editor/level/systems/NewLevelSystemModal.coffee +++ b/app/views/editor/level/systems/NewLevelSystemModal.coffee @@ -20,7 +20,7 @@ module.exports = class NewLevelSystemModal extends ModalView name = @$el.find('#level-system-name').val() system = new LevelSystem() 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 res = system.save(null, {type: 'POST'}) # Override PUT so we can trigger postFirstVersion logic return unless res diff --git a/app/views/editor/level/thangs/LevelThangEditView.coffee b/app/views/editor/level/thangs/LevelThangEditView.coffee index bf7d6b162..84429d644 100644 --- a/app/views/editor/level/thangs/LevelThangEditView.coffee +++ b/app/views/editor/level/thangs/LevelThangEditView.coffee @@ -41,7 +41,7 @@ module.exports = class LevelThangEditView extends CocoView level: @level 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 @listenTo @thangComponentEditView, 'components-changed', @onComponentsChanged diff --git a/app/views/editor/level/thangs/ThangsTabView.coffee b/app/views/editor/level/thangs/ThangsTabView.coffee index 351da5032..c5ff8c65f 100644 --- a/app/views/editor/level/thangs/ThangsTabView.coffee +++ b/app/views/editor/level/thangs/ThangsTabView.coffee @@ -585,14 +585,14 @@ module.exports = class ThangsTabView extends CocoView if batchInsert if thangType.get('name') is '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 thangID = "Random #{thangType.get('name')} #{@thangsBatch.length}" else thangID = Thang.nextID(thangType.get('name'), @world) until thangID and not @getThangByID(thangID) if @cloneSourceThang 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 else components = _.cloneDeep thangType.get('components') ? [] diff --git a/app/views/play/CampaignView.coffee b/app/views/play/CampaignView.coffee index 5852514fd..a484db9d9 100644 --- a/app/views/play/CampaignView.coffee +++ b/app/views/play/CampaignView.coffee @@ -404,7 +404,7 @@ module.exports = class CampaignView extends RootView particleKey.push 'hero' if level.unlocksHero and not level.unlockedHero #particleKey.push 'item' if level.slug is 'robot-ragnarok' # TODO: generalize 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('-') onMouseEnterPortals: (e) -> diff --git a/app/views/play/level/ControlBarView.coffee b/app/views/play/level/ControlBarView.coffee index fd649ef8e..515a20f2b 100644 --- a/app/views/play/level/ControlBarView.coffee +++ b/app/views/play/level/ControlBarView.coffee @@ -61,7 +61,7 @@ module.exports = class ControlBarView extends CocoView getRenderData: (c={}) -> super c 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.ladderGame = @level.get('type') in ['ladder', 'hero-ladder', 'course-ladder'] if c.isMultiplayerLevel = @isMultiplayerLevel @@ -104,6 +104,7 @@ module.exports = class ControlBarView extends CocoView if @courseInstanceID @homeLink += "/#{@courseInstanceID}" @homeViewArgs.push @courseInstanceID + #else if @level.get('type', true) is 'game-dev' # TODO else @homeLink = '/' @homeViewClass = 'views/HomeView' diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee index e5e3507a0..8911ba832 100644 --- a/app/views/play/level/PlayLevelView.coffee +++ b/app/views/play/level/PlayLevelView.coffee @@ -203,7 +203,7 @@ module.exports = class PlayLevelView extends RootView @session = @levelLoader.session @world = @levelLoader.world @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' # 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 @@ -463,7 +463,7 @@ module.exports = class PlayLevelView extends RootView return false if $.browser?.msie or $.browser?.msedge return false if $.browser.linux return false if me.level() < 8 - if levelType is 'course' + if levelType in ['course', 'game-dev'] return false else if levelType is 'hero' and gamesSimulated return false if stillBuggy @@ -536,7 +536,7 @@ module.exports = class PlayLevelView extends RootView onDonePressed: -> @showVictory() 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 return if @victorySeen @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 @endHighlight() 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 = PicoCTFVictoryModal if window.serverConfig.picoCTF victoryModal = new ModalClass(options) diff --git a/app/views/play/level/modal/HeroVictoryModal.coffee b/app/views/play/level/modal/HeroVictoryModal.coffee index b01808aa3..1f7a46672 100644 --- a/app/views/play/level/modal/HeroVictoryModal.coffee +++ b/app/views/play/level/modal/HeroVictoryModal.coffee @@ -49,7 +49,7 @@ module.exports = class HeroVictoryModal extends ModalView @session = options.session @level = options.level @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([], { url: "/db/achievement?related=#{@session.get('level').original}" model: Achievement @@ -73,6 +73,7 @@ module.exports = class HeroVictoryModal extends ModalView if @level.get('type', true) in ['course', 'course-ladder'] @saveReviewEventually = _.debounce(@saveReviewEventually, 2000) @loadExistingFeedback() + # TODO: support game-dev destroy: -> clearInterval @sequentialAnimationInterval @@ -153,7 +154,8 @@ module.exports = class HeroVictoryModal extends ModalView getRenderData: -> c = super() 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' earnedAchievementMap = _.indexBy(@newEarnedAchievements or [], (ea) -> ea.get('achievement')) for achievement in (@achievements?.models or []) @@ -221,7 +223,7 @@ module.exports = class HeroVictoryModal extends ModalView afterRender: -> 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() @playSelectionSound hero, true for original, hero of @thangTypes # Preload them @updateSavingProgressStatus() @@ -231,7 +233,7 @@ module.exports = class HeroVictoryModal extends ModalView @insertSubView @ladderSubmissionView, @$el.find('.ladder-submission-view') 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 #playVictorySound = => @playSound 'victory-title-appear' # TODO: actually add this @$el.find('#victory-header').delay(250).queue(-> @@ -262,7 +264,7 @@ module.exports = class HeroVictoryModal extends ModalView beginSequentialAnimations: -> 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) -> { number: $(panel).data('number') previousNumber: $(panel).data('previous-number') diff --git a/app/views/play/level/tome/Spell.coffee b/app/views/play/level/tome/Spell.coffee index 0e6642d67..aabfa4c96 100644 --- a/app/views/play/level/tome/Spell.coffee +++ b/app/views/play/level/tome/Spell.coffee @@ -165,7 +165,7 @@ module.exports = class Spell writable = @permissions.readwrite.length > 0 skipProtectAPI = @skipProtectAPI or not writable 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 functionName: @name codeLanguage: @language diff --git a/app/views/play/level/tome/SpellListEntryView.coffee b/app/views/play/level/tome/SpellListEntryView.coffee index eb6b93cf6..7dc00f9b6 100644 --- a/app/views/play/level/tome/SpellListEntryView.coffee +++ b/app/views/play/level/tome/SpellListEntryView.coffee @@ -37,7 +37,7 @@ module.exports = class SpellListEntryView extends CocoView context 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() if @spell.language in ['python', 'lua'] parameters.unshift 'self' diff --git a/app/views/play/level/tome/SpellPaletteEntryView.coffee b/app/views/play/level/tome/SpellPaletteEntryView.coffee index 31f75fcaf..cbada6610 100644 --- a/app/views/play/level/tome/SpellPaletteEntryView.coffee +++ b/app/views/play/level/tome/SpellPaletteEntryView.coffee @@ -84,7 +84,7 @@ module.exports = class SpellPaletteEntryView extends CocoView Backbone.Mediator.publish 'tome:palette-pin-toggled', entry: @, pinned: @popoverPinned 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 # Actually, do it all the time, because we recently busted the pin CSS. TODO: restore pinning jigglyPopover = $('.spell-palette-popover.popover') diff --git a/app/views/play/level/tome/SpellPaletteView.coffee b/app/views/play/level/tome/SpellPaletteView.coffee index c7ac5174f..be4abdc59 100644 --- a/app/views/play/level/tome/SpellPaletteView.coffee +++ b/app/views/play/level/tome/SpellPaletteView.coffee @@ -163,7 +163,7 @@ module.exports = class SpellPaletteView extends CocoView else propStorage = '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 else @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') @entryGroups = _.groupBy @entries, groupForEntry 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 @entryGroups = {} @entryGroups[defaultGroup] = @entries diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee index dbddb0de3..75a800446 100644 --- a/app/views/play/level/tome/SpellView.coffee +++ b/app/views/play/level/tome/SpellView.coffee @@ -635,7 +635,7 @@ module.exports = class SpellView extends CocoView @createToolbarView() 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 @$el.append @debugView.render().$el.hide() diff --git a/app/views/play/level/tome/TomeView.coffee b/app/views/play/level/tome/TomeView.coffee index 3e1df0292..f15233079 100644 --- a/app/views/play/level/tome/TomeView.coffee +++ b/app/views/play/level/tome/TomeView.coffee @@ -60,7 +60,7 @@ module.exports = class TomeView extends CocoView @worker = @createWorker() programmableThangs = _.filter @options.thangs, (t) -> t.isProgrammable and t.programmableMethods @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 @castButton = @insertSubView new CastButtonView spells: @spells, level: @options.level, session: @options.session, god: @options.god @teamSpellMap = @generateTeamSpellMap(@spells) @@ -193,7 +193,7 @@ module.exports = class TomeView extends CocoView @castButton?.$el.hide() 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 spellName = e.spellName @spellList?.$el.hide() diff --git a/bower.json b/bower.json index 0f69fe4a9..454979b4e 100644 --- a/bower.json +++ b/bower.json @@ -32,7 +32,7 @@ "firepad": "~0.1.2", "marked": "~0.3.0", "moment": "~2.5.0", - "aether": "~0.5.0", + "aether": "~0.5.6", "underscore.string": "~2.3.3", "firebase": "~1.0.2", "d3": "~3.4.4", diff --git a/package.json b/package.json index fc326df34..80047b905 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "dependencies": { "JQDeferred": "~2.1.0", "ace-builds": "https://github.com/ajaxorg/ace-builds/archive/3fb55e8e374ab02ce47c1ae55ffb60a1835f3055.tar.gz", - "aether": "~0.5.0", + "aether": "~0.5.6", "async": "0.2.x", "aws-sdk": "~2.0.0", "bayesian-battle": "0.0.7", diff --git a/server/commons/auth.coffee b/server/commons/auth.coffee index 323b71d30..18e958c87 100644 --- a/server/commons/auth.coffee +++ b/server/commons/auth.coffee @@ -17,16 +17,17 @@ module.exports.setup = -> authentication.use(new LocalStrategy( (username, password, done) -> - # kind of a hacky way to make it possible for iPads to 'log in' with their unique device id - if username.length is 36 and '@' not in username # must be an identifier for vendor - q = { iosIdentifierForVendor: username } - else - q = { emailLower: username.toLowerCase() } + # TODO: Add special iPad login endpoint. There was some logic here for the old, hacky method, + # but was removed for username login + q = { $or: [ + { emailLower: username.toLowerCase() } + { slug: _.str.slugify(username) } + ]} User.findOne(q).exec((err, user) -> return done(err) if err 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() if passwordReset and password.toLowerCase() is passwordReset User.update {_id: user.get('_id')}, {$unset: {passwordReset: ''}}, {}, -> @@ -34,7 +35,7 @@ module.exports.setup = -> hash = User.hashPassword(password) 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) ) )) diff --git a/server/commons/errors.coffee b/server/commons/errors.coffee index a82eb7b0f..24276645b 100644 --- a/server/commons/errors.coffee +++ b/server/commons/errors.coffee @@ -92,6 +92,10 @@ errorResponseSchema = { type: 'string' 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) diff --git a/server/lib/closeIO.coffee b/server/lib/closeIO.coffee index ed9fd9aa2..c72c4b8c4 100644 --- a/server/lib/closeIO.coffee +++ b/server/lib/closeIO.coffee @@ -65,7 +65,7 @@ module.exports = activities = JSON.parse(body) return done("Unexpected activities format: " + body) unless activities.data? 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, config.mail.supportSchools, lead.id) catch error diff --git a/spec/server/functional/auth.spec.coffee b/spec/server/functional/auth.spec.coffee index ffb173131..886471d37 100644 --- a/spec/server/functional/auth.spec.coffee +++ b/spec/server/functional/auth.spec.coffee @@ -24,18 +24,6 @@ describe 'POST /auth/login', -> yield utils.becomeAnonymous() 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) -> [res, body] = yield request.postAsync({uri: urlLogin, json: { username: 'some@email.com' @@ -55,6 +43,19 @@ describe 'POST /auth/login', -> }}) expect(res.statusCode).toBe(200) 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) -> yield utils.initUser({ diff --git a/spec/server/functional/level_system.spec.coffee b/spec/server/functional/level_system.spec.coffee index cf53c59e4..df5ed836a 100644 --- a/spec/server/functional/level_system.spec.coffee +++ b/spec/server/functional/level_system.spec.coffee @@ -15,7 +15,6 @@ describe 'LevelSystem', -> codeLanguage: 'coffeescript' permissions: simplePermissions dependencies: [] - propertyDocumentation: [] systems = {} @@ -80,7 +79,6 @@ describe 'LevelSystem', -> expect(body.original).toBeDefined() expect(body.created).toBeDefined() expect(body.dependencies).toBeDefined() - expect(body.propertyDocumentation).toBeDefined() expect(body.version.isLatestMajor).toBe(true) expect(body.version.isLatestMinor).toBe(true) expect(body.permissions).toBeDefined() diff --git a/test/app/views/courses/HeroSelectModal.spec.coffee b/test/app/views/courses/HeroSelectModal.spec.coffee index b38861f4e..7a933bf51 100644 --- a/test/app/views/courses/HeroSelectModal.spec.coffee +++ b/test/app/views/courses/HeroSelectModal.spec.coffee @@ -1,5 +1,4 @@ HeroSelectModal = require 'views/courses/HeroSelectModal' -auth = require 'core/auth' factories = require 'test/app/factories' describe 'HeroSelectModal', -> @@ -14,7 +13,6 @@ describe 'HeroSelectModal', -> beforeEach (done) -> window.me = user = factories.makeUser({ heroConfig: { thangType: hero1.get('original') } }) - auth.loginUser(user.attributes) modal = new HeroSelectModal({ currentHeroID: hero1.id }) modal.heroes.fakeRequests[0].respondWith({ status: 200, responseText: heroesResponse }) jasmine.demoModal(modal) diff --git a/test/app/views/courses/StudentLogInModal.spec.coffee b/test/app/views/courses/StudentLogInModal.spec.coffee deleted file mode 100644 index 0161264fe..000000000 --- a/test/app/views/courses/StudentLogInModal.spec.coffee +++ /dev/null @@ -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)