From 69f3ee3a5b794b5bdb0fa6947e7fbf854bed1b87 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Wed, 8 Jun 2016 13:45:25 -0700 Subject: [PATCH 1/5] Allow username login, tear out a bunch of related stuff in change * Switch from auth.loginUser to User.loginPasswordUser with Promise * Remove a cascade of unused views that were using auth.loginUser: StudentLogInModal, StudentSignupModal, HourOfCodeView * Also remove auth.createUser --- app/core/auth.coffee | 34 ------ app/locale/en.coffee | 1 + app/models/User.coffee | 7 ++ app/styles/courses/hour-of-code-view.sass | 19 --- app/styles/courses/student-log-in-modal.sass | 7 -- app/styles/courses/student-sign-up-modal.sass | 10 -- app/styles/modal/auth-modal.sass | 2 +- .../core/{auth.jade => auth-modal.jade} | 32 ++++-- app/templates/courses/hour-of-code-view.jade | 74 ------------ .../courses/student-log-in-modal.jade | 30 ----- .../courses/student-sign-up-modal.jade | 46 -------- app/views/core/AuthModal.coffee | 56 +++++---- app/views/core/CreateAccountModal.coffee | 1 - app/views/courses/HourOfCodeView.coffee | 108 ------------------ app/views/courses/StudentLogInModal.coffee | 43 ------- app/views/courses/StudentSignUpModal.coffee | 101 ---------------- server/commons/auth.coffee | 15 +-- server/commons/errors.coffee | 4 + spec/server/functional/auth.spec.coffee | 25 ++-- .../views/courses/HeroSelectModal.spec.coffee | 2 - .../courses/StudentLogInModal.spec.coffee | 22 ---- 21 files changed, 90 insertions(+), 549 deletions(-) delete mode 100644 app/styles/courses/hour-of-code-view.sass delete mode 100644 app/styles/courses/student-log-in-modal.sass delete mode 100644 app/styles/courses/student-sign-up-modal.sass rename app/templates/core/{auth.jade => auth-modal.jade} (64%) delete mode 100644 app/templates/courses/hour-of-code-view.jade delete mode 100644 app/templates/courses/student-log-in-modal.jade delete mode 100644 app/templates/courses/student-sign-up-modal.jade delete mode 100644 app/views/courses/HourOfCodeView.coffee delete mode 100644 app/views/courses/StudentLogInModal.coffee delete mode 100644 app/views/courses/StudentSignUpModal.coffee delete mode 100644 test/app/views/courses/StudentLogInModal.spec.coffee 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/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/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/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/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/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/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) From 6019beac25e5929f5ca7e58e0fd66e8e29a476cd Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Wed, 8 Jun 2016 14:53:27 -0700 Subject: [PATCH 2/5] :bug:Fix license request email parsing Caused by https://github.com/codecombat/codecombat/commit/a6bb706cf295e2680fe77ff1 3e91d3da59487b53 --- server/lib/closeIO.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 546598396dcbb820d9006309ecced761bce14833 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Wed, 8 Jun 2016 15:46:21 -0700 Subject: [PATCH 3/5] Add Lisa to /about page --- app/assets/images/pages/about/lisa_small.png | Bin 0 -> 20467 bytes app/templates/about.jade | 7 +++++++ 2 files changed, 7 insertions(+) create mode 100644 app/assets/images/pages/about/lisa_small.png 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 0000000000000000000000000000000000000000..08fba0a64f0b5194532c1e056ea725d96fb99d9b GIT binary patch literal 20467 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 literal 0 HcmV?d00001 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") From c57fd6f460a0485c8bce342fd44f286f8e7d497c Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Wed, 8 Jun 2016 15:48:22 -0700 Subject: [PATCH 4/5] Do not set campaignIndex for levels except for course campaigns --- app/views/editor/campaign/CampaignEditorView.coffee | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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')) From b0fcddac6860138522011831f3541a058cb0c8bd Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Wed, 8 Jun 2016 16:57:00 -0700 Subject: [PATCH 5/5] Add game-dev level type (#3725) * Initial pass adding new game-dev level type. * Fix a failing test with updated LevelSystem required properties * Bring back normal Angel worker timeout times * Fix another failing LevelSystem test since removing propertyDocumentation --- app/core/ParticleMan.coffee | 36 +++++++++++++++++++ app/models/Level.coffee | 2 +- app/schemas/models/campaign.schema.coffee | 2 +- app/schemas/models/level.coffee | 2 +- app/schemas/models/level_component.coffee | 2 +- app/schemas/models/level_system.coffee | 20 ++--------- .../component/ThangComponentConfigView.coffee | 2 +- .../level/systems/NewLevelSystemModal.coffee | 2 +- .../level/thangs/LevelThangEditView.coffee | 2 +- .../editor/level/thangs/ThangsTabView.coffee | 4 +-- app/views/play/CampaignView.coffee | 2 +- app/views/play/level/ControlBarView.coffee | 3 +- app/views/play/level/PlayLevelView.coffee | 8 ++--- .../play/level/modal/HeroVictoryModal.coffee | 12 ++++--- app/views/play/level/tome/Spell.coffee | 2 +- .../play/level/tome/SpellListEntryView.coffee | 2 +- .../level/tome/SpellPaletteEntryView.coffee | 2 +- .../play/level/tome/SpellPaletteView.coffee | 4 +-- app/views/play/level/tome/SpellView.coffee | 2 +- app/views/play/level/tome/TomeView.coffee | 4 +-- bower.json | 2 +- package.json | 2 +- .../functional/level_system.spec.coffee | 2 -- 23 files changed, 71 insertions(+), 50 deletions(-) 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/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/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/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/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()