From 870ae9a8a1b40139ba30c952fe8e9c75476a8a86 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Wed, 25 May 2016 15:24:51 -0700 Subject: [PATCH 01/12] Add hero selector for courses mode Use selected hero in Course mode play Show selected hero on Courses (in progress) Add hero select modal Use short names, only show warriors Use box-shadow instead of borders Add tests for HeroSelectModal Refactor modal opening test Address code review feedback --- app/collections/ThangTypes.coffee | 7 +++- app/lib/LevelLoader.coffee | 7 ++++ app/locale/en.coffee | 3 ++ app/models/Level.coffee | 5 +++ app/models/ThangType.coffee | 19 +++++++++ app/styles/courses/courses-view.sass | 19 +++++++++ app/styles/courses/hero-select-modal.sass | 40 ++++++++++++++++++ app/templates/courses/courses-view.jade | 12 ++++++ app/templates/courses/hero-select-modal.jade | 27 ++++++++++++ app/views/courses/CoursesView.coffee | 25 ++++++++++- app/views/courses/HeroSelectModal.coffee | 42 +++++++++++++++++++ .../app/views/courses/CoursesView.spec.coffee | 32 ++++++++++++++ .../views/courses/HeroSelectModal.spec.coffee | 38 +++++++++++++++++ 13 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 app/styles/courses/hero-select-modal.sass create mode 100644 app/templates/courses/hero-select-modal.jade create mode 100644 app/views/courses/HeroSelectModal.coffee create mode 100644 test/app/views/courses/CoursesView.spec.coffee create mode 100644 test/app/views/courses/HeroSelectModal.spec.coffee diff --git a/app/collections/ThangTypes.coffee b/app/collections/ThangTypes.coffee index 8dd041307..0b030bf66 100644 --- a/app/collections/ThangTypes.coffee +++ b/app/collections/ThangTypes.coffee @@ -3,4 +3,9 @@ ThangType = require 'models/ThangType' module.exports = class ThangTypeCollection extends CocoCollection url: '/db/thang.type' - model: ThangType \ No newline at end of file + model: ThangType + + fetchHeroes: -> + @fetch { + url: '/db/thang.type?view=heroes' + } diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee index 29a201235..34548f6dc 100644 --- a/app/lib/LevelLoader.coffee +++ b/app/lib/LevelLoader.coffee @@ -168,6 +168,13 @@ module.exports = class LevelLoader extends CocoClass @consolidateFlagHistory() if @opponentSession?.loaded else if session is @opponentSession @consolidateFlagHistory() if @session.loaded + if @level.get('type', true) in ['course'] # course-ladder is hard to handle because there's 2 sessions + heroConfig = me.get('heroConfig') + return if not heroConfig + url = "/db/thang.type/#{heroConfig.thangType}/version" + if heroResource = @maybeLoadURL(url, ThangType, 'thang') + @worldNecessities.push heroResource + return return unless @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'] heroConfig = session.get('heroConfig') heroConfig ?= me.get('heroConfig') if session is @session and not @headless diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 15b00dba5..3e409016e 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -1314,6 +1314,9 @@ sent_verification: "We've sent a verification email to:" you_can_edit: "You can edit your email address in " account_settings: "Account Settings" + select_your_hero: "Select Your Hero" + select_your_hero_description: "You can always change your hero by going to your Courses page and clicking \"Select Hero\"" + select_this_hero: "Select this Hero" teacher: teacher_dashboard: "Teacher Dashboard" # Navbar diff --git a/app/models/Level.coffee b/app/models/Level.coffee index 975e34309..166b7b11d 100644 --- a/app/models/Level.coffee +++ b/app/models/Level.coffee @@ -145,6 +145,11 @@ module.exports = class Level extends CocoModel for original, placeholderComponent of placeholders when not placeholdersUsed[original] levelThang.components.push placeholderComponent + # Load the user's chosen hero AFTER getting stats from default char + if /Hero Placeholder/.test(levelThang.id) and @get('type', true) in ['course', 'course-ladder'] + heroThangType = me.get('heroConfig')?.thangType + levelThang.thangType = heroThangType if heroThangType + sortSystems: (levelSystems, systemModels) -> [sorted, originalsSeen] = [[], {}] visit = (system) -> diff --git a/app/models/ThangType.coffee b/app/models/ThangType.coffee index fe9ec9b1d..5e1dd3a6f 100644 --- a/app/models/ThangType.coffee +++ b/app/models/ThangType.coffee @@ -239,6 +239,25 @@ module.exports = class ThangType extends CocoModel portraitOnly = !!options.portraitOnly "#{@get('name')} - #{options.resolutionFactor} - #{colorConfigs} - #{portraitOnly}" + getHeroShortName: -> + map = { + "Assassin": "Ritic" + "Captain": "Anya" + "Forest Archer": "Naria" + "Goliath": "Okar" + "Guardian": "Illia" + "Knight": "Tharin" + "Librarian": "Hushbaum" + "Necromancer": "Nalfar" + "Ninja": "Amara" + "Potion Master": "Omarn" + "Raider": "Arryn" + "Samurai": "Hattori" + "Sorcerer": "Pender" + "Trapper": "Senick" + } + map[@get('name')] + getPortraitImage: (spriteOptionsOrKey, size=100) -> src = @getPortraitSource(spriteOptionsOrKey, size) return null unless src diff --git a/app/styles/courses/courses-view.sass b/app/styles/courses/courses-view.sass index ce74620d9..0189b033d 100644 --- a/app/styles/courses/courses-view.sass +++ b/app/styles/courses/courses-view.sass @@ -41,3 +41,22 @@ #join-class-form .alert, .progress margin-top: 20px + + // Hero display + .current-hero-container + display: flex + justify-content: center + + .current-hero-text + font-size: 16pt + + .hero-avatar + background-color: #f8f8f8 + box-shadow: 0 0 0 1px gray + margin-right: 25px + + .current-hero-right-col + display: flex + flex-direction: column + justify-content: space-between + align-items: flex-start diff --git a/app/styles/courses/hero-select-modal.sass b/app/styles/courses/hero-select-modal.sass new file mode 100644 index 000000000..de1e84f09 --- /dev/null +++ b/app/styles/courses/hero-select-modal.sass @@ -0,0 +1,40 @@ +@import "app/styles/style-flat-variables" + +#hero-select-modal + .modal-dialog + width: auto + max-width: 900px + + .modal-header, .modal-body:not(.secret), .modal-footer + display: flex + flex-direction: column + align-items: center + + .modal-footer + margin: 30px + + h4 + max-width: 500px + + .hero-list + display: flex + flex-wrap: wrap + justify-content: center + margin-bottom: -50px + + .hero-option + display: flex + flex-direction: column + align-items: center + margin: 0 50px 50px + + .hero-avatar + margin: 6px + background-color: #f8f8f8 + box-shadow: 0 0 0 1px gray + + .current .hero-avatar + box-shadow: 0 0 0 6px gray + + .selected .hero-avatar + box-shadow: 0 0 0 6px $gold diff --git a/app/templates/courses/courses-view.jade b/app/templates/courses/courses-view.jade index 0200ae6b6..11a03df1e 100644 --- a/app/templates/courses/courses-view.jade +++ b/app/templates/courses/courses-view.jade @@ -39,6 +39,18 @@ block content .text-center h1(data-i18n="courses.welcome_to_page") Welcome to your Courses page! + + .current-hero-container.text-center.row + .hero-avatar + img(src=view.hero.getPortraitURL()) + .current-hero-right-col + .semibold.current-hero-text + span.spr(data-i18n="TODO") + | Current Hero: + span.current-hero-name= view.hero.getHeroShortName() + button.change-hero-btn.btn.btn-lg.btn-forest + span(data-i18n="TODO") + | Change Hero if view.classrooms.size() h3.text-uppercase(data-i18n="courses.my_classes") diff --git a/app/templates/courses/hero-select-modal.jade b/app/templates/courses/hero-select-modal.jade new file mode 100644 index 000000000..9d918bb41 --- /dev/null +++ b/app/templates/courses/hero-select-modal.jade @@ -0,0 +1,27 @@ +extends /templates/core/modal-base-flat + +block modal-header-content + .text-center + h3(data-i18n="courses.select_your_hero") + h4(data-i18n="courses.select_your_hero_description") + +block modal-body-content + .hero-list + if view.heroes.loaded + each hero in view.heroes.models + if hero.get('heroClass') === 'Warrior' + +heroOption(hero) + +mixin heroOption(hero) + - var heroID = hero.id + - var selectedState = (state.get('selectedHeroID') === heroID ? 'selected' : (state.get('currentHeroID') === heroID ? 'current' : '')) + .hero-option(data-hero-id=heroID class=selectedState) + .hero-avatar + img(src=hero.getPortraitURL()) + .text-h5.hero-name + span= hero.getHeroShortName() + +block modal-footer-content + .select-hero-btn.btn.btn-lg.btn-forest + span(data-i18n="courses.select_this_hero") + diff --git a/app/views/courses/CoursesView.coffee b/app/views/courses/CoursesView.coffee index d255beb9d..0cf3d2d90 100644 --- a/app/views/courses/CoursesView.coffee +++ b/app/views/courses/CoursesView.coffee @@ -4,6 +4,7 @@ template = require 'templates/courses/courses-view' AuthModal = require 'views/core/AuthModal' CreateAccountModal = require 'views/core/CreateAccountModal' ChangeCourseLanguageModal = require 'views/courses/ChangeCourseLanguageModal' +HeroSelectModal = require 'views/courses/HeroSelectModal' ChooseLanguageModal = require 'views/courses/ChooseLanguageModal' JoinClassModal = require 'views/courses/JoinClassModal' CourseInstance = require 'models/CourseInstance' @@ -13,6 +14,7 @@ Classroom = require 'models/Classroom' Classrooms = require 'collections/Classrooms' LevelSession = require 'models/LevelSession' Campaign = require 'models/Campaign' +ThangType = require 'models/ThangType' utils = require 'core/utils' # TODO: Test everything @@ -24,6 +26,7 @@ module.exports = class CoursesView extends RootView events: 'click #log-in-btn': 'onClickLogInButton' 'click #start-new-game-btn': 'openSignUpModal' + 'click .change-hero-btn': 'onClickChangeHeroButton' 'click #join-class-btn': 'onClickJoinClassButton' 'submit #join-class-form': 'onSubmitJoinClassForm' 'click #change-language-link': 'onClickChangeLanguageLink' @@ -43,6 +46,16 @@ module.exports = class CoursesView extends RootView @courses = new CocoCollection([], { url: "/db/course", model: Course}) @supermodel.loadCollection(@courses) + # TODO: Trim this section for only what's necessary + @hero = new ThangType + defaultHeroOriginal = ThangType.heroes.captain + heroOriginal = me.get('heroConfig')?.thangType or defaultHeroOriginal + @hero.url = "/db/thang.type/#{heroOriginal}/version" + # @hero.setProjection ['name','slug','soundTriggers','featureImages','gems','heroClass','description','components','extendedName','unlockLevelName','i18n'] + @supermodel.loadModel(@hero, 'hero') + @listenTo @hero, 'all', -> + @render() + onCourseInstancesLoaded: -> map = {} for courseInstance in @courseInstances.models @@ -76,6 +89,16 @@ module.exports = class CoursesView extends RootView @openModalView(modal) application.tracker?.trackEvent 'Started Student Signup', category: 'Courses' + onClickChangeHeroButton: -> + modal = new HeroSelectModal({ currentHeroID: @hero.id }) + @openModalView(modal) + @listenTo modal, 'hero-select:success', (newHero) => + # @hero.url = "/db/thang.type/#{me.get('heroConfig').thangType}/version" + # @hero.fetch() + @hero.set(newHero.attributes) + @listenTo modal, 'hide', -> + @stopListening modal + onSubmitJoinClassForm: (e) -> e.preventDefault() @joinClass() @@ -136,7 +159,7 @@ module.exports = class CoursesView extends RootView classroomCourseInstances.fetch({ data: {classroomID: newClassroom.id} }) @listenToOnce classroomCourseInstances, 'sync', -> # TODO: Smoother system for joining a classroom and course instances, without requiring page reload, - # and showing which class was just joined. + # and showing which class was just joined. document.location.search = '' # Using document.location.reload() causes an infinite loop of reloading onClickChangeLanguageLink: -> diff --git a/app/views/courses/HeroSelectModal.coffee b/app/views/courses/HeroSelectModal.coffee new file mode 100644 index 000000000..c658b6941 --- /dev/null +++ b/app/views/courses/HeroSelectModal.coffee @@ -0,0 +1,42 @@ +ModalView = require 'views/core/ModalView' +template = require 'templates/courses/hero-select-modal' +Classroom = require 'models/Classroom' +ThangTypes = require 'collections/ThangTypes' +State = require 'models/State' +ThangType = require 'models/ThangType' +User = require 'models/User' + +module.exports = class HeroSelectModal extends ModalView + id: 'hero-select-modal' + template: template + + events: + 'click .select-hero-btn': 'onClickSelectHeroButton' + 'click .hero-option': 'onClickHeroOption' + + initialize: ({ currentHeroID }) -> + @debouncedRender = _.debounce @render, 0 + + @state = new State({ + currentHeroID + selectedHeroID: currentHeroID + }) + + @heroes = new ThangTypes({}, { project: ['original', 'name', 'heroClass'] }) + @supermodel.trackRequest @heroes.fetchHeroes() + + @listenTo @state, 'all', -> @debouncedRender() + @listenTo @heroes, 'all', -> @debouncedRender() + + onClickHeroOption: (e) -> + heroID = $(e.currentTarget).data('hero-id') + @state.set selectedHeroID: heroID + hero = @heroes.get(heroID) + me.set(heroConfig: {}) unless me.get('heroConfig') + heroConfig = _.assign me.get('heroConfig'), { thangType: hero.get('original') } + me.set({ heroConfig }) + me.save().then => + @trigger 'hero-select:success', hero + + onClickSelectHeroButton: () -> + @hide() diff --git a/test/app/views/courses/CoursesView.spec.coffee b/test/app/views/courses/CoursesView.spec.coffee new file mode 100644 index 000000000..195111232 --- /dev/null +++ b/test/app/views/courses/CoursesView.spec.coffee @@ -0,0 +1,32 @@ +CoursesView = require 'views/courses/CoursesView' +HeroSelectModal = require 'views/courses/HeroSelectModal' +Classrooms = require 'collections/Classrooms' +CourseInstances = require 'collections/CourseInstances' +Courses = require 'collections/Courses' +auth = require 'core/auth' +factories = require 'test/app/factories' + +describe 'CoursesView', -> + + modal = null + view = null + + describe 'Change Hero button', -> + beforeEach (done) -> + view = new CoursesView() + classrooms = new Classrooms([factories.makeClassroom()]) + courseInstances = new CourseInstances([factories.makeCourseInstance()]) + courses = new Courses([factories.makeCourse()]) + view.classrooms.fakeRequests[0].respondWith({ status: 200, responseText: classrooms.stringify() }) + view.ownedClassrooms.fakeRequests[0].respondWith({ status: 200, responseText: classrooms.stringify() }) + view.courseInstances.fakeRequests[0].respondWith({ status: 200, responseText: courseInstances.stringify() }) + view.render() + jasmine.demoEl(view.$el) + done() + + it 'opens the modal when you click Change Hero', -> + spyOn(view, 'openModalView') + view.$('.change-hero-btn').click() + expect(view.openModalView).toHaveBeenCalled() + args = view.openModalView.calls.argsFor(0) + expect(args[0] instanceof HeroSelectModal).toBe(true) diff --git a/test/app/views/courses/HeroSelectModal.spec.coffee b/test/app/views/courses/HeroSelectModal.spec.coffee new file mode 100644 index 000000000..b38861f4e --- /dev/null +++ b/test/app/views/courses/HeroSelectModal.spec.coffee @@ -0,0 +1,38 @@ +HeroSelectModal = require 'views/courses/HeroSelectModal' +auth = require 'core/auth' +factories = require 'test/app/factories' + +describe 'HeroSelectModal', -> + + modal = null + coursesView = null + user = null + + hero1 = factories.makeThangType({ original: "hero1original", _id: "hero1id", heroClass: "Warrior", name: "Hero 1" }) + hero2 = factories.makeThangType({ original: "hero2original", _id: "hero2id", heroClass: "Warrior", name: "Hero 2" }) + heroesResponse = JSON.stringify([hero1, hero2]) + + 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) + _.defer -> + modal.render() + done() + + afterEach -> + modal.stopListening() + + it 'highlights the current hero', -> + expect(modal.$(".hero-option[data-hero-id='#{hero1.id}']")[0].className.split(" ")).toContain('selected') + + it 'saves when you change heroes', (done) -> + modal.$(".hero-option[data-hero-id='#{hero2.id}']").click() + _.defer -> + expect(user.fakeRequests.length).toBe(1) + request = user.fakeRequests[0] + expect(request.method).toBe("PUT") + expect(JSON.parse(request.params).heroConfig?.thangType).toBe(hero2.get('original')) + done() From 705463615be9854e2a4637c0f1d7a7a71fda4152 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Thu, 2 Jun 2016 15:56:14 -0700 Subject: [PATCH 02/12] Fix some intermittent client erroring Some tests are triggering achievement polling. Prevent that from happening. --- app/models/CocoModel.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee index 8c8510857..4a5c84fb5 100644 --- a/app/models/CocoModel.coffee +++ b/app/models/CocoModel.coffee @@ -368,6 +368,7 @@ class CocoModel extends Backbone.Model return if _.isString @url then @url else @url() @pollAchievements: -> + return if application.testing CocoCollection = require 'collections/CocoCollection' EarnedAchievement = require 'models/EarnedAchievement' From b3663196d759ce2fa5323fe9f46746ef0823895b Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Thu, 2 Jun 2016 16:08:35 -0700 Subject: [PATCH 03/12] Fix CourseVictoryModal.spec --- .../views/play/level/modal/CourseVictoryModal.spec.coffee | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/app/views/play/level/modal/CourseVictoryModal.spec.coffee b/test/app/views/play/level/modal/CourseVictoryModal.spec.coffee index 09e5e21f2..3ca11cad2 100644 --- a/test/app/views/play/level/modal/CourseVictoryModal.spec.coffee +++ b/test/app/views/play/level/modal/CourseVictoryModal.spec.coffee @@ -40,6 +40,12 @@ describe 'CourseVictoryModal', -> modal.classroom.fakeRequests[0].respondWith({ status: 200, responseText: factories.makeClassroom().stringify() }) + if me.fakeRequests + lastRequest = _.last(me.fakeRequests) + if not lastRequest.response + lastRequest.respondWith({ + status: 200, responseText: factories.makeUser().stringify() + }) nextLevelRequest = modal.nextLevel.fakeRequests[0] describe 'given a course level with a next level and no item or hero rewards', -> From 2ef10f58b34b53ea5d77d121ad6f24ce33a93bfb Mon Sep 17 00:00:00 2001 From: Rob Date: Fri, 3 Jun 2016 14:01:37 -0700 Subject: [PATCH 04/12] Fix bug where visual indents couldn't nest. --- app/views/play/level/tome/SpellView.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee index 2745e4777..dbddb0de3 100644 --- a/app/views/play/level/tome/SpellView.coffee +++ b/app/views/play/level/tome/SpellView.coffee @@ -316,7 +316,7 @@ module.exports = class SpellView extends CocoView xstart = startOfRow(row) if language is 'python' - requiredIndent = new RegExp '^' + new Array(xstart / 4 + 2).join '( |\t)' + '(\\S|\\s*$)' + requiredIndent = new RegExp '^' + new Array(xstart / 4 + 1).join('( |\t)') + '( |\t)+(\\S|\\s*$)' for crow in [docRange.start.row+1..docRange.end.row] unless requiredIndent.test lines[crow] docRange.end.row = crow - 1 From 8e64a3b244484792f36213c54c227829ed013c48 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Fri, 3 Jun 2016 14:41:48 -0700 Subject: [PATCH 05/12] Add Promise polyfill To allow using Promises while still supporting IE11 --- bower.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bower.json b/bower.json index 064a9dbc2..0f69fe4a9 100644 --- a/bower.json +++ b/bower.json @@ -52,7 +52,8 @@ "esper.js": "http://files.codecombat.com/esper.tar.gz", "algoliasearch": "^3.13.1", "algolia-autocomplete.js": "^0.17.0", - "algolia-autocomplete-no-conflict": "1.0.0" + "algolia-autocomplete-no-conflict": "1.0.0", + "promise-polyfill": "^5.2.1" }, "overrides": { "algolia-autocomplete.js": { From 3f8272afe9b0ebea838ea65c6fed0a8101cdc97f Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Fri, 3 Jun 2016 15:57:30 -0700 Subject: [PATCH 06/12] Switch Promise polyfill to bluebird --- bower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bower.json b/bower.json index 0f69fe4a9..50b1b9e16 100644 --- a/bower.json +++ b/bower.json @@ -53,7 +53,7 @@ "algoliasearch": "^3.13.1", "algolia-autocomplete.js": "^0.17.0", "algolia-autocomplete-no-conflict": "1.0.0", - "promise-polyfill": "^5.2.1" + "bluebird": "^3.4.0" }, "overrides": { "algolia-autocomplete.js": { From 7a0fb967f01a22e405851e13b2e052028ecff44e Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Fri, 3 Jun 2016 16:26:03 -0700 Subject: [PATCH 07/12] Add clearer reports to client TestView --- app/styles/{test.sass => test-view.sass} | 4 ++ app/templates/test-view.jade | 14 ++++++ app/views/TestView.coffee | 60 +++++++++++++++++------- 3 files changed, 60 insertions(+), 18 deletions(-) rename app/styles/{test.sass => test-view.sass} (78%) diff --git a/app/styles/test.sass b/app/styles/test-view.sass similarity index 78% rename from app/styles/test.sass rename to app/styles/test-view.sass index f32c27af2..4beda86e2 100644 --- a/app/styles/test.sass +++ b/app/styles/test-view.sass @@ -7,3 +7,7 @@ font-family: Arial, Geneva, sans-serif padding: 20px font-weight: bold + + .alert-report + font-size: 20px + diff --git a/app/templates/test-view.jade b/app/templates/test-view.jade index bb520438f..97e155141 100644 --- a/app/templates/test-view.jade +++ b/app/templates/test-view.jade @@ -11,6 +11,20 @@ ol.breadcrumb .container-fluid .row .col-md-8 + #failure-reports + for report in view.failureReports + .alert.alert-danger.alert-report + ul.suite-list + for description in report.suiteDescriptions + li= description + li + strong ... #{report.testDescription} + hr + ol.error-list + for message in report.failMessages + li + strong= message + #test-wrapper.well #testing-area diff --git a/app/views/TestView.coffee b/app/views/TestView.coffee index 631a3374f..5475b6791 100644 --- a/app/views/TestView.coffee +++ b/app/views/TestView.coffee @@ -13,7 +13,7 @@ module.exports = TestView = class TestView extends RootView id: 'test-view' template: template reloadOnClose: true - loadedFileIDs: [] + className: 'style-flat' events: 'click #show-demos-btn': 'onClickShowDemosButton' @@ -24,11 +24,13 @@ module.exports = TestView = class TestView extends RootView initialize: (options, @subPath='') -> @subPath = @subPath[1..] if @subPath[0] is '/' @demosOn = storage.load('demos-on') + @failureReports = [] + @loadedFileIDs = [] afterInsert: -> @initSpecFiles() @render() - TestView.runTests(@specFiles, @demosOn) + TestView.runTests(@specFiles, @demosOn, @) window.runJasmine() # EVENTS @@ -59,7 +61,30 @@ module.exports = TestView = class TestView extends RootView prefix = TEST_REQUIRE_PREFIX + @subPath @specFiles = (f for f in @specFiles when _.string.startsWith f, prefix) - @runTests: (specFiles, demosOn=false) -> + @runTests: (specFiles, demosOn=false, view) -> + + jasmine.getEnv().addReporter({ + suiteStack: [] + + specDone: (result) -> + if result.status is 'failed' + console.log 'result', result + report = { + suiteDescriptions: _.clone(@suiteStack) + failMessages: (fe.message for fe in result.failedExpectations) + testDescription: result.description + } + view.failureReports.push(report) + view.renderSelectors('#failure-reports') + + suiteStarted: (result) -> + @suiteStack.push(result.description) + + suiteDone: (result) -> + @suiteStack.pop() + + }) + application.testing = true specFiles ?= @getAllSpecFiles() if demosOn @@ -71,23 +96,22 @@ module.exports = TestView = class TestView extends RootView jasmine.demoEl = _.noop jasmine.demoModal = _.noop - describe 'CodeCombat Client', => - jasmine.Ajax.install() - beforeEach -> - jasmine.Ajax.requests.reset() - Backbone.Mediator.init() - Backbone.Mediator.setValidationEnabled false - spyOn(application.tracker, 'trackEvent') - # TODO Stubbify more things - # * document.location - # * firebase - # * all the services that load in main.html + jasmine.Ajax.install() + beforeEach -> + jasmine.Ajax.requests.reset() + Backbone.Mediator.init() + Backbone.Mediator.setValidationEnabled false + spyOn(application.tracker, 'trackEvent') + # TODO Stubbify more things + # * document.location + # * firebase + # * all the services that load in main.html - afterEach -> - # TODO Clean up more things - # * Events + afterEach -> + # TODO Clean up more things + # * Events - require f for f in specFiles # runs the tests + require f for f in specFiles # runs the tests @getAllSpecFiles = -> allFiles = window.require.list() From 0d5ad789e54166a85d9104ab5aaab5b649e88c0e Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Fri, 3 Jun 2016 10:18:41 -0700 Subject: [PATCH 08/12] Add time played to level progress tooltips --- app/lib/coursesHelper.coffee | 2 ++ app/templates/courses/teacher-class-view.jade | 4 ++-- .../hovers/progress-dot-single-student-level.jade | 9 +++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/lib/coursesHelper.coffee b/app/lib/coursesHelper.coffee index 0866d95e7..693234fa2 100644 --- a/app/lib/coursesHelper.coffee +++ b/app/lib/coursesHelper.coffee @@ -156,6 +156,8 @@ module.exports = courseProgress[levelID][userID] = { completed: true, started: false } # These don't matter, will always be set session = _.find classroom.sessions.models, (session) -> session.get('creator') is userID and session.get('level').original is levelID + + courseProgress[levelID][userID].session = session if not session # haven't gotten to this level yet, but might have completed others before courseProgress.started ||= false #no-op diff --git a/app/templates/courses/teacher-class-view.jade b/app/templates/courses/teacher-class-view.jade index e0e6713da..3f135c235 100644 --- a/app/templates/courses/teacher-class-view.jade +++ b/app/templates/courses/teacher-class-view.jade @@ -321,7 +321,7 @@ mixin studentLevelsRow(student) - var levels = view.classroom.getLevels({courseID: course.id, withoutLadderLevels: true}).models each level, index in levels - var progress = state.get('progressData').get({ classroom: view.classroom, course: course, level: level, user: student }) - +studentLevelProgressDot(progress, level, index+1) + +studentLevelProgressDot(progress, level, index+1, session) mixin studentCourseProgressDot(progress, levelsTotal, level, label) //- TODO: Refactor with TeacherClassesView jade @@ -342,7 +342,7 @@ mixin studentLevelProgressDot(progress, level, levelNumber) //- TODO: Refactor with TeacherClassesView jade - dotClass = progress.completed ? 'forest' : (progress.started ? 'gold' : ''); - levelName = level.get('name') - - context = _.merge(progress, { levelName: levelName, levelNumber: levelNumber }) + - context = _.merge(progress, { levelName: levelName, levelNumber: levelNumber, moment: moment }) .progress-dot.level-progress-dot(class=dotClass, data-html='true', data-title=view.singleStudentLevelProgressDotTemplate(context)) +progressDotLabel(levelNumber) diff --git a/app/templates/teachers/hovers/progress-dot-single-student-level.jade b/app/templates/teachers/hovers/progress-dot-single-student-level.jade index 3922be2ed..0a8433a48 100644 --- a/app/templates/teachers/hovers/progress-dot-single-student-level.jade +++ b/app/templates/teachers/hovers/progress-dot-single-student-level.jade @@ -1,3 +1,10 @@ +mixin timePlayed() + if session.get('playtime') > 0 + .small-details.nowrap + span.spr(data-i18n='teacher.time_played') + | Played for + span= moment.duration({ seconds: session.get('playtime') }).humanize() + if completed .small-details.nowrap span= levelNumber @@ -7,6 +14,7 @@ if completed span.spr(data-i18n='teacher.completed') | Completed span= new Date(dateFirstCompleted).toLocaleString() + +timePlayed //- .small-details //- i(data-i18n='teacher.click_to_view_solution') //- | click to view solution @@ -19,6 +27,7 @@ else if started span.spr(data-i18n='teacher.last_played') | Last played span= new Date(lastPlayed).toLocaleString() + +timePlayed //- .small-details //- i(data-i18n='teacher.click_to_view_progress') //- | click to view progress From a6bb706cf295e2680fe77ff13e91d3da59487b53 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Fri, 3 Jun 2016 20:20:16 -0700 Subject: [PATCH 09/12] Update licenses needed form email contacts to include NL --- 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 cb4825109..ed9fd9aa2 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/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 189f9fa7aff324b2596b64a53f57c282a3d160a4 Mon Sep 17 00:00:00 2001 From: JurianLock Date: Sun, 5 Jun 2016 15:46:19 +0200 Subject: [PATCH 10/12] Update nl-NL.coffee (#3713) Some UX updates and some minor spelling checks. --- app/locale/nl-NL.coffee | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/app/locale/nl-NL.coffee b/app/locale/nl-NL.coffee index e466d1c4d..6ae868591 100644 --- a/app/locale/nl-NL.coffee +++ b/app/locale/nl-NL.coffee @@ -5,8 +5,8 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription no_mobile: "CodeCombat is niet gemaakt voor mobiele apparaten en werkt misschien niet!" # Warning that shows up on mobile devices play: "Speel" # The big play button that opens up the campaign view. play_campaign_version: "Speel de Verhaallijn" # Shows up under big play button if you only play /courses - old_browser: "Oh oh, jouw browser is te oud om CodeCombat te kunnen spelen, Sorry!" # Warning that shows up on really old Firefox/Chrome/Safari - old_browser_suffix: "Je kan toch proberen, maar het zal waarschijnlijk niet werken!" + old_browser: "uh-oh, jouw browser is te oud om CodeCombat te kunnen spelen, Sorry!" # Warning that shows up on really old Firefox/Chrome/Safari + old_browser_suffix: "Je kan alsnog proberen, maar het zal waarschijnlijk niet werken!" ipad_browser: "Slecht nieuws: CodeCombat draait niet in je browser op de iPad. Goed nieuws: onze iPad-app wordt op het moment beoordeeld door Apple." campaign: "Verhaallijn" for_beginners: "Voor Beginners" @@ -24,25 +24,25 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription im_a_teacher: "Ik ben een leraar" im_a_student: "Ik ben een leerling" learn_more: "Lees verder" - classroom_in_a_box: "Een kant-en-klare digitale klas voor programmeerlessen." + classroom_in_a_box: "Kant-en-klare programmeerlessen." codecombat_is: "CodeCombat is een platform waarmee leerlingen leren programmeren door het spelen van een spel." # {change} our_courses: "Onze lessen zijn specifiek ontwikkeld voor een klasomgeving, zelfs voor leraren zonder programmeerervaring." # {change} - top_screenshots_hint: "Leerlingen schrijven code en zien direct het resultaat van de verandering." + top_screenshots_hint: "Leerlingen schrijven code en zien direct resultaat." designed_with: "Gemaakt voor leraren" real_code: "Echte, getypte code" from_the_first_level: "vanaf het eerste level" - getting_students: "Leerlingen zo snel mogelijk echte code laten schrijven is noodzakelijk voor het leren van programmeer syntax en correcte structuur." + getting_students: "Doordat leerlingen code schrijven in 'echte programmeertaal', leren ze niet alleen hoe computers denken, maar kunnen ze het ook echt toepassen." educator_resources: "Lesbrieven voor docenten" course_guides: "en ondersteuningsmateriaal" teaching_computer_science: "Je hebt geen informatica diploma nodig om te kunnen programmeren, wij verschaffen de materialen waarmee elke docent programmeerles kan geven." - accessible_to: "Bereikbaar voor" + accessible_to: "Toegankelijk voor" everyone: "iedereen" democratizing: "Programmeerles toegankelijk maken is onze filosofie. Iedereen moet de kans krijgen om te leren programmeren." forgot_learning: "Volgens mij hadden ze niet meer door dat ze eigenlijk bezig waren met leren." wanted_to_do: " Ik wilde altijd al leren programmeren, maar op school was hier nooit aandacht voor." why_games: "Waarom is spelenderwijs leren belangrijk?" games_reward: "Games vergroten de productiviteit." - encourage: "Gaming is een middel dat interactie, nieuwschierigheid, en trial-and-error aanmoedigt. Een goed spel daagt de speler uit zijn vaardigheden te perfectioneren, wat hetzelfde noodzakelijke proces is waar leerlingen doorheen gaan wanneer zij iets leren." + encourage: "Iedereen wordt geboren als klein onderzoekertje dat de wereld ontdekt door vallen en opstaan. Deze intrinsieke motivatie om te leren ziet men ook terug in een spelomgeving. Voor de speler wordt 'leren' een middel om het spel te winnen, in plaats van een doel op zich." excel: "Games helpen bij de" struggle: "productiviteit-strijd" kind_of_struggle: "het soort worsteling dat uitmondt in een leerproces dat uitdagend is en " @@ -58,7 +58,7 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription great_game: "Een goed spel is meer dan alleen medailles en prestaties - het gaat om een reis, nauwkeurig ontworpen puzzels, en de mogelijkheid om uitdagingen vol zelfvertrouwen aan te pakken." agency: "CodeCombat is een game die de mogelijkheid en het zelfvertrouwen geeft om met echte code te werken, wat zowel beginners als gevorderde helpt bij het schrijven van goede, valide code." request_demo_title: "Laat je leerlingen vandaag nog starten!" - request_demo_subtitle: "Vraag een demo aan en start binnen een uur met programmeerlessen." + request_demo_subtitle: "Vraag een demo aan en start met programmeerlessen." get_started_title: "Maak vandaag nog een klas aan!" get_started_subtitle: "Maak een klas aan, voeg je leerlingen toe, en monitor hun vooruitgang." request_demo: "Vraag een demo aan" @@ -74,7 +74,7 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription coming_soon: "Binnenkort beschikbaar!" courses_available_in: "Lessen zijn beschikbaar in JavaScript, Python, en Java (Java is binnenkort beschikbaar!)" boast: "Uitdagende raadsels die zowel gamers als fanatieke programmeurs weten te prikkelen." - winning: "Een gouden combinatie van spel-elementen en programmeerhuiswerk, dat samen zorgt voor kind-vriendelijk en oprecht aangenaam onderwijs." + winning: "Een gouden combinatie van spel-elementen en programmeerhuiswerk, dat samen zorgt voor kindvriendelijk en oprecht aangenaam onderwijs." run_class: "Alles wat je nodig hebt om vandaag nog programmeerlessen in jouw klas te geven, geen voorkennis vereist." teachers: "Docenten!" teachers_and_educators: "Docenten & Mentoren" @@ -92,7 +92,7 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription check_out_wiki: "Bekijk onze nieuwe leraren Wiki" want_coco: "Wil je CodeCombat op jouw school?" form_select_role: "Selecteer je rol" - form_select_range: "Selecteer klassengrootte" + form_select_range: "Selecteer klasomvang" nav: play: "Levels" # The top nav bar entry where players choose which levels to play @@ -221,19 +221,19 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription "/": "gedeeld door" "+": "plus" "-": "min" -# "+=": "add and assign" -# "-=": "subtract and assign" + "+=": "tel op en wijs toe" + "-=": "trek af en wijs toe" True: "Waar" true: "waar" False: "onwaar" false: "onwaar" undefined: "ongedefinieerd" -# null: "null" -# nil: "nil" + null: "nul" + nil: "nihil" None: "Geen" share_progress_modal: - blurb: "Je gaat snel vooruit! Vertel aan je ouders hoeveel je geleerd hebt van CodeCombat." + blurb: "Je gaat snel vooruit! Vertel je ouders hoeveel je geleerd hebt van CodeCombat." email_invalid: "E-mailadres klopt niet." form_blurb: "Vul het e-mailadres van je ouders hieronder in en we zullen het ze laten zien!" form_label: "E-mailadres" @@ -472,19 +472,19 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription tip_reticulating: "Paden aan het verknopen." tip_harry: "Je bent een tovenaar, " tip_great_responsibility: "Met een groots talent voor programmeren komt een grootse debug verantwoordelijkheid." - tip_munchkin: "Als je je groentjes niet opeet zal een munchkin je ontvoeren terwijl je slaapt." + tip_munchkin: "Als je je groenten niet opeet zal een munchkin je ontvoeren terwijl je slaapt." tip_binary: "Er zijn 10 soorten mensen in de wereld: Mensen die binair kunnen tellen en mensen die dat niet kunnen." tip_commitment_yoda: "Een programmeur moet de grootste inzet hebben, een meest serieuze geest. ~ Yoda" tip_no_try: "Doe het. Of doe het niet. Je kunt niet proberen. - Yoda" tip_patience: "Geduld moet je hebben, jonge Padawan. - Yoda" tip_documented_bug: "Een gedocumenteerde fout is geen fout; het is deel van het programma." - tip_impossible: "Het lijkt altijd onmogelijk tot het gedaan wordt. - Nelson Mandela" + tip_impossible: "Het lijkt altijd onmogelijk totdat iemand het doet. - Nelson Mandela" tip_talk_is_cheap: "Je kunt het goed uitleggen, maar toon me de code. - Linus Torvalds" tip_first_language: "Het ergste dat je kan leren is je eerste programmeertaal. - Alan Kay" tip_hardware_problem: "Q: Hoeveel programmeurs heb je nodig om een lampje te vervangen? A: Nul, het is een a hardware probleem." tip_hofstadters_law: "De Wet van Hofstadter: Het duurt altijd langer dan je verwacht, zelfs wanneer je rekening houdt met de Wet van Hofstadter." tip_premature_optimization: "vroegtijdig optimaliseren is de wortel van al het kwaad. - Donald Knuth" - tip_brute_force: "Wanneer je twijfelt, gebruik brute force. - Ken Thompson" + tip_brute_force: "Wanneer je twijfelt, gebruik dan brute force. - Ken Thompson" tip_extrapolation: "Er zijn twee soorten mensen: Zij die iets kunnen afleiden van onvolledige gegevens..." tip_superpower: "Van alle dingen komt programmeren het dichtst in de buurt van een superkracht." tip_control_destiny: "In echte open source, hebt je het recht om je eigen toekomst te bepalen. - Linus Torvalds" @@ -820,8 +820,8 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription teachers_quote: name: "Demo Formulier" title: "Demo aanvragen" - subtitle: "Haal CodeCombat jouw klaslokaal of club!" - email_exists: "Er bestaat al een gebruiker met dit email adres." + subtitle: "Gebruik CodeCombat voor jouw klas of programmeerclub!" + email_exists: "Er bestaat al een gebruiker met dit emailadres." phone_number: "Telefoonnummer" phone_number_help: "Waarop kunnen we je bereiken tijdens kantooruren?" primary_role_label: "Uw primaire rol" From 5da85621c6f90162395afba94fc26616546519a9 Mon Sep 17 00:00:00 2001 From: Ana Date: Sun, 5 Jun 2016 15:47:27 +0200 Subject: [PATCH 11/12] Update sr.coffee (#3714) - translation of contact, acc settings, community and clans sections + some other stuff --- app/locale/sr.coffee | 196 +++++++++++++++++++++---------------------- 1 file changed, 98 insertions(+), 98 deletions(-) diff --git a/app/locale/sr.coffee b/app/locale/sr.coffee index b7ec6630e..5e317d20e 100644 --- a/app/locale/sr.coffee +++ b/app/locale/sr.coffee @@ -686,21 +686,21 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian # writable: "writable" # Hover over "attack" in Your Skills while playing a level to see most of this # read_only: "read-only" action: "Aкција" -# spell: "Spell" -# action_name: "name" -# action_cooldown: "Takes" -# action_specific_cooldown: "Cooldown" -# action_damage: "Damage" -# action_range: "Range" -# action_radius: "Radius" -# action_duration: "Duration" -# example: "Example" -# ex: "ex" # Abbreviation of "example" -# current_value: "Current Value" -# default_value: "Default value" -# parameters: "Parameters" -# returns: "Returns" -# granted_by: "Granted by" + spell: "Магија" + action_name: "име" + action_cooldown: "Потребно" + action_specific_cooldown: "Хлађење" + action_damage: "Штета" + action_range: "Домет" + action_radius: "Опсег" + action_duration: "Трајање" + example: "Пример" + ex: "нпр." # Abbreviation of "example" + current_value: "Тренутна вредност" + default_value: "Подразумевана вредност" + parameters: "Параметри" + returns: "Враћа" + granted_by: "Додељено од" save_load: granularity_saved_games: "Сачувано" @@ -860,12 +860,12 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian finish_signup_p: "Направи налог да оснујеш разред, додаш своје ученике и пратиш њихов напредак док уче компјутерске науке." signup_with: "Пријави се са:" connect_with: "Повежи се са:" -# conversion_warning: "WARNING: Your current account is a Student Account. Once you submit this form, your account will be updated to a Teacher Account." -# learn_more_modal: "Teacher accounts on CodeCombat have the ability to monitor student progress, assign enrollments and manage classrooms. Teacher accounts cannot be a part of a classroom - if you are currently enrolled in a class using this account, you will no longer be able to access it once you update to a Teacher Account." + conversion_warning: "УПОЗОРЕЊЕ: Твој тренутни налог је Студентски Налог. Након што пошаљеш овај формулар, твој налог ће бити надограђен у Учитељски Налог." + learn_more_modal: "Учитељски налози на CodeCombat-у имају могућност посматрања напретка ученика, додељивања уписа и управљања учионицама. Учитељски налози не могу бити део учионице - ако си тренутно уписан у разред преко овог налога, нећеш више моћи да му приступиш кад ажурираш у Учитељски Налог." create_account: "Направи учитељски налог" -# create_account_subtitle: "Get access to teacher-only tools for using CodeCombat in the classroom. Set up a class, add your students, and monitor their progress!" -# convert_account_title: "Update to Teacher Account" -# not: "Not" + create_account_subtitle: "Добиј приступ алатима само за учитеље за коришћење CodeCombat-а у учионици. Подеси разред, додај своје ученике, и посматрај њихов напредак!" + convert_account_title: "Ажурирај у Учитељски Налог" + not: "Није" setup_a_class: "Подеси разред" versions: @@ -880,18 +880,18 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian contact: contact_us: "Контактирај CodeCombat" - welcome: "Драго нам је што нас контактираш! Искористи ову форму да нам пошаљеш мејл. " + welcome: "Драго нам је што нас контактираш! Искористи овај формулар да нам пошаљеш мејл. " forum_prefix: "За било шта јавно, посети " - forum_page: "наш форум." -# forum_suffix: " instead." -# faq_prefix: "There's also a" -# faq: "FAQ" -# subscribe_prefix: "If you need help figuring out a level, please" -# subscribe: "buy a CodeCombat subscription" -# subscribe_suffix: "and we'll be happy to help you with your code." -# subscriber_support: "Since you're a CodeCombat subscriber, your email will get our priority support." -# screenshot_included: "Screenshot included." -# where_reply: "Where should we reply?" + forum_page: "наш форум" + forum_suffix: " уместо тога." + faq_prefix: "Такође, ту је" + faq: "FAQ" + subscribe_prefix: "Ако ти треба помоћ да разумеш ниво, молимо да" + subscribe: "купиш CodeCombat претплату" + subscribe_suffix: "и радо ћемо ти помоћи у твом коду." + subscriber_support: "Пошто си CodeCombat претплатник, твој мејл ће имати приоритет у нашој подршци." + screenshot_included: "Снимак екрана укључен." + where_reply: "Где треба да одговоримо?" send: "Пошаљи повратну информацију" account_settings: @@ -910,24 +910,24 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian # god_mode: "God Mode" password_tab: "Шифра" emails_tab: "Мејлови" -# admin: "Admin" + admin: "Администратор" manage_subscription: "Кликни овде да би управљао својом претплатом." new_password: "Нова Шифра" new_password_verify: "Потврди" -# type_in_email: "Type in your email to confirm account deletion." -# type_in_email_progress: "Type in your email to confirm deleting your progress." -# type_in_password: "Also, type in your password." + type_in_email: "Упиши свој мејл да потврдиш брисање налога." + type_in_email_progress: "Упиши свој мејл да потврдиш брисање свог напретка." + type_in_password: "Такође, упиши своју шифру." email_subscriptions: "Мејл претплате" -# email_subscriptions_none: "No Email Subscriptions." + email_subscriptions_none: "Без мејл претплата." email_announcements: "Обавештења" email_announcements_description: "Прими мејл за најновије вести и достигнућа на CodeCombat-у" -# email_notifications: "Notifications" -# email_notifications_summary: "Controls for personalized, automatic email notifications related to your CodeCombat activity." -# email_any_notes: "Any Notifications" -# email_any_notes_description: "Disable to stop all activity notification emails." + email_notifications: "Обавештења" + email_notifications_summary: "Контроле за персонализована, аутоматска мејл обавештења вазана за твоју CodeCombat активност." + email_any_notes: "Сва обавештења" + email_any_notes_description: "Онемогући да би прекинуо сва мејл обавештења о активности." email_news: "Вести" email_recruit_notes: "Пословне могућности" -# email_recruit_notes_description: "If you play really well, we may contact you about getting you a (better) job." + email_recruit_notes_description: "Ако играш јако добро, можда ћемо те контактирати о томе да добијеш (бољи) посао." contributor_emails: "Мејлови реда сарадника" contribute_prefix: "Тражимо људе који би нам се придружили! Погледај " contribute_page: "страницу за сарадњу" @@ -961,13 +961,13 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian community: main_title: "CodeCombat Заједница" -# introduction: "Check out the ways you can get involved below and decide what sounds the most fun. We look forward to working with you!" -# level_editor_prefix: "Use the CodeCombat" -# level_editor_suffix: "to create and edit levels. Users have created levels for their classes, friends, hackathons, students, and siblings. If create a new level sounds intimidating you can start by forking one of ours!" -# thang_editor_prefix: "We call units within the game 'thangs'. Use the" -# thang_editor_suffix: "to modify the CodeCombat source artwork. Allow units to throw projectiles, alter the direction of an animation, change a unit's hit points, or upload your own vector sprites." -# article_editor_prefix: "See a mistake in some of our docs? Want to make some instructions for your own creations? Check out the" -# article_editor_suffix: "and help CodeCombat players get the most out of their playtime." + introduction: "Погледај испод како можеш да се укључиш и одлучи шта звучи најзанимљивије. Радујемо се прилици да радимо са тобом!" + level_editor_prefix: "Користи CodeCombat" + level_editor_suffix: "да правиш и уређујеш нивое. Корисници су направили нивое за њихове разреде, пријатеље, хакатоне, ученике и браћу и сестре. Ако прављење новог нивоа звучи застрашујуће, можеш да почнеш форковањем једног од наших!" + thang_editor_prefix: "Ми зовемо јединице у игри 'thangs'. Користи" + thang_editor_suffix: "да модификујеш CodeCombat изворне илустрације. Дозволи јединицама да бацају пројектиле, измени дирекцију анимације, промени хит поене јединице или отпреми сопствене векторске спрајтове." + article_editor_prefix: "Видиш грешку у неком од наших докумената? Желиш да направиш инструкције за сопствене креације? Погледај" + article_editor_suffix: "и помози CodeCombat играчима да добију највише од свог играња." find_us: "Нађи нас на овим сајтовима" social_github: "Погледај цео наш код на GitHub-у" social_blog: "Читај CodeCombat блог на Sett-у" @@ -986,57 +986,57 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian make_private: "Направи клан приватним" subs_only: "само за претплатнике" create_clan: "Направи нови клан" - private_preview: "Preview" + private_preview: "Приказ" private_clans: "Приватни кланови" public_clans: "Јавни кланови" my_clans: "Моји кланови" clan_name: "Име клана" name: "Име" -# chieftain: "Chieftain" -# type: "Type" + chieftain: "Поглавица" + type: "Врста" edit_clan_name: "Измени име клана" edit_clan_description: "Измени опис клана" edit_name: "измени име" edit_description: "измени опис" private: "(приватан)" -# summary: "Summary" -# average_level: "Average Level" -# average_achievements: "Average Achievements" + summary: "Преглед" + average_level: "Просечни ниво" + average_achievements: "Просечна достигнућа" delete_clan: "Избриши клан" leave_clan: "Напусти клан" join_clan: "Придружи се клану" invite_1: "Позови:" -# invite_2: "*Invite players to this Clan by sending them this link." + invite_2: "*Позови играче у овај Клан тако што ћеш им послати овај линк." members: "Чланови" progress: "Напредак" -# not_started_1: "not started" -# started_1: "started" -# complete_1: "complete" -# exp_levels: "Expand levels" -# rem_hero: "Remove Hero" + not_started_1: "није започето" + started_1: "започето" + complete_1: "заврши" + exp_levels: "Прошири нивое" + rem_hero: "Уклони Хероја" status: "Статус" -# complete_2: "Complete" -# started_2: "Started" -# not_started_2: "Not Started" -# view_solution: "Click to view solution." -# view_attempt: "Click to view attempt." -# latest_achievement: "Latest Achievement" -# playtime: "Playtime" -# last_played: "Last played" -# leagues_explanation: "Play in a league against other clan members in these multiplayer arena instances." -# track_concepts1: "Track concepts" -# track_concepts2a: "learned by each student" -# track_concepts2b: "learned by each member" -# track_concepts3a: "Track levels completed for each student" -# track_concepts3b: "Track levels completed for each member" -# track_concepts4a: "See your students'" -# track_concepts4b: "See your members'" -# track_concepts5: "solutions" -# track_concepts6a: "Sort students by name or progress" -# track_concepts6b: "Sort members by name or progress" -# track_concepts7: "Requires invitation" -# track_concepts8: "to join" -# private_require_sub: "Private clans require a subscription to create or join." + complete_2: "Заврши" + started_2: "Започето" + not_started_2: "Није започето" + view_solution: "Кликни да видиш решење." + view_attempt: "Кликни да видиш покушај." + latest_achievement: "Последње достигнуће" + playtime: "Време игања" + last_played: "Последњи пут играно" + leagues_explanation: "Играј у лиги против других чланова клана у овим мултиплејер инстанцама арене." + track_concepts1: "Прати концепте" + track_concepts2a: "научене од сваког ученика" + track_concepts2b: "научене од сваког члана" + track_concepts3a: "Прати завршене нивое за сваког ученика" + track_concepts3b: "Прати завршене нивое за сваког члана" + track_concepts4a: "Види од својих ученика" + track_concepts4b: "Види од својих чланова" + track_concepts5: "решења" + track_concepts6a: "Сортирај ученике према имену или напретку" + track_concepts6b: "Сортирај чланове према имену или напретку" + track_concepts7: "Захтева позив" + track_concepts8: "за придруживање" + private_require_sub: "Приватни кланови захтевају претплату да би могао да их направиш или да им се придружиш." # courses: # course: "Course" @@ -1488,12 +1488,12 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian # add_system_title: "Add Systems to Level" # done_adding: "Done Adding" -# article: -# edit_btn_preview: "Preview" -# edit_article_title: "Edit Article" + article: + edit_btn_preview: "Приказ" + edit_article_title: "Измени Чланак" -# polls: -# priority: "Priority" + polls: + priority: "Приоритет" # contribute: # page_title: "Contributing" @@ -1645,17 +1645,17 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian # favorite_postfix: "." # not_member_of_clans: "Not a member of any clans yet." -# achievements: -# last_earned: "Last Earned" -# amount_achieved: "Amount" -# achievement: "Achievement" -# current_xp_prefix: "" -# current_xp_postfix: " in total" -# new_xp_prefix: "" -# new_xp_postfix: " earned" -# left_xp_prefix: "" -# left_xp_infix: " until level " -# left_xp_postfix: "" + achievements: + last_earned: "Последње стечено" + amount_achieved: "Количина" + achievement: "Достигнуће" + current_xp_prefix: "" + current_xp_postfix: " укупно" + new_xp_prefix: "" + new_xp_postfix: " стечено" + left_xp_prefix: "" + left_xp_infix: " до нивоа " + left_xp_postfix: "" account: payments: "Уплате" From 0d4a88a957643f7b9493977f2d7db5f2efe2720c Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Fri, 27 May 2016 16:03:58 -0700 Subject: [PATCH 12/12] Strip spaces in classCode on fetch and join --- server/handlers/classroom_handler.coffee | 2 +- server/middleware/classrooms.coffee | 4 ++-- spec/server/functional/classrooms.spec.coffee | 24 +++++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/server/handlers/classroom_handler.coffee b/server/handlers/classroom_handler.coffee index 97daaac3d..d404183b5 100644 --- a/server/handlers/classroom_handler.coffee +++ b/server/handlers/classroom_handler.coffee @@ -84,7 +84,7 @@ ClassroomHandler = class ClassroomHandler extends Handler return @sendDatabaseError(res, err) if err return @sendSuccess(res, (@formatEntity(req, classroom) for classroom in classrooms)) else if code = req.query.code - code = code.toLowerCase() + code = code.toLowerCase().replace(/ /g, '') Classroom.findOne {code: code}, (err, classroom) => return @sendDatabaseError(res, err) if err return @sendNotFoundError(res) unless classroom diff --git a/server/middleware/classrooms.coffee b/server/middleware/classrooms.coffee index cb811859d..5cada7b30 100644 --- a/server/middleware/classrooms.coffee +++ b/server/middleware/classrooms.coffee @@ -22,7 +22,7 @@ module.exports = fetchByCode: wrap (req, res, next) -> code = req.query.code return next() unless code - classroom = yield Classroom.findOne({ code: code.toLowerCase() }).select('name ownerID aceConfig') + classroom = yield Classroom.findOne({ code: code.toLowerCase().replace(/ /g, '') }).select('name ownerID aceConfig') if not classroom log.debug("classrooms.fetchByCode: Couldn't find Classroom with code: #{code}") throw new errors.NotFound('Classroom not found.') @@ -170,7 +170,7 @@ module.exports = if req.user.isTeacher() log.debug("classrooms.join: Cannot join a classroom as a teacher: #{req.user.id}") throw new errors.Forbidden('Cannot join a classroom as a teacher') - code = req.body.code.toLowerCase() + code = req.body.code.toLowerCase().replace(/ /g, '') classroom = yield Classroom.findOne({code: code}) if not classroom log.debug("classrooms.join: Classroom not found with code #{code}") diff --git a/spec/server/functional/classrooms.spec.coffee b/spec/server/functional/classrooms.spec.coffee index 81c9f2964..b34cb3152 100644 --- a/spec/server/functional/classrooms.spec.coffee +++ b/spec/server/functional/classrooms.spec.coffee @@ -60,6 +60,18 @@ describe 'GET /db/classroom/:id', -> expect(body._id).toBe(classroomID = body._id) done() +describe 'GET /db/classroom by classCode', -> + it 'Returns the class if you include spaces', utils.wrap (done) -> + user = yield utils.initUser() + yield utils.loginUser(user) + teacher = yield utils.initUser() + classroom = new Classroom({ name: "some class", ownerID: teacher.id, camelCode: "FooBarBaz", code: "foobarbaz" }) + yield classroom.save() + [res, body] = yield request.getAsync(getURL('/db/classroom?code=foo bar baz'), { json: true }) + expect(res.statusCode).toBe(200) + expect(res.body.data?.name).toBe(classroom.get('name')) + done() + describe 'POST /db/classroom', -> beforeEach utils.wrap (done) -> @@ -295,6 +307,18 @@ describe 'POST /db/classroom/-/members', -> fail('student should be added to the free course instance.') done() + it 'joins the class even with spaces in the classcode', utils.wrap (done) -> + yield utils.loginUser(@student) + url = getURL("/db/classroom/anything-here/members") + code = @classroom.get('code') + codeWithSpaces = code.split("").join(" ") + [res, body] = yield request.postAsync { uri: url, json: { code: codeWithSpaces } } + expect(res.statusCode).toBe(200) + classroom = yield Classroom.findById(@classroom.id) + if classroom.get('members').length isnt 1 + fail 'expected classCode with spaces to work too' + done() + it 'returns 403 if the user is a teacher', utils.wrap (done) -> yield utils.loginUser(@teacher) url = getURL("/db/classroom/~/members")