Merge remote-tracking branch 'codecombat/master'

This commit is contained in:
Cat Sync 2016-06-21 15:23:36 -04:00
commit 168cdc800d
78 changed files with 1446 additions and 966 deletions
.travis.ymlREADME.md
app
scripts
server
server_setup.coffee
spec
test/app

View file

@ -28,7 +28,7 @@ before_script:
- "sleep 15" # to give node a chance to start
script:
- "./node_modules/karma/bin/karma start --browsers Firefox --single-run --reporters progress"
- "./node_modules/karma/bin/karma start --browsers Firefox --single-run --reporters dots"
- "npm run jasmine"
notifications:

160
README.md
View file

@ -1,84 +1,106 @@
#CodeCombat
# CodeCombat
<div style="text-align:center"><a href="http://codecombat.com/"><img src ="https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/readme_00.png" /></a></div>
<div style="text-align:center">
<a href="http://codecombat.com/">
<img src ="https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/readme_00.png" />
</a>
</div>
[![Build Status](https://travis-ci.org/codecombat/codecombat.png?branch=master)](https://travis-ci.org/codecombat/codecombat)
CodeCombat is a multiplayer programming game for learning how to code. **See the [Archmage (coder) developer wiki](https://github.com/codecombat/codecombat/wiki/Archmage-Home) for a dev setup guide, extensive documentation, and much more. Every new person that wants to start contributing the project coding should start there.**
CodeCombat is a multiplayer programming game for learning how to code.
**See the [Archmage (coder) developer wiki](../../wiki/Archmage-Home) for a dev
setup guide, extensive documentation, and much more. Every new person that wants
to start contributing the project coding should start there.**
It's both a startup and a community project, completely open source under the [MIT and Creative Commons licenses](http://codecombat.com/legal). It's the largest open source [CoffeeScript](http://coffeescript.org/) project by lines of code, and since it's a game (with [really cool tech](https://github.com/codecombat/codecombat/wiki/Third-party-software-and-services)), it's really fun to hack on. Join us in teaching the world to code! Your contribution will go on to show millions of players how cool programming can be.
It's both a startup and a community project, completely open source under the
[MIT and Creative Commons licenses](http://codecombat.com/legal). It's the
largest open source [CoffeeScript](http://coffeescript.org/) project by lines of
code, and since it's a game (with [really cool tech](../../wiki/Third-party-software-and-services)),
it's really fun to hack on. Join us in teaching the world to code! Your
contribution will go on to show millions of players how cool programming can be.
### [Getting Started](https://github.com/codecombat/codecombat/wiki/Dev-Setup:-General-Information)
### [Getting Started](../../wiki/Dev-Setup:-General-Information)
We've made it easy to fork the project, run a simple script that'll install all the dependencies, and get a local copy of CodeCombat running right away on [Mac](https://github.com/codecombat/codecombat/wiki/Dev-Setup:-Mac), [Linux](https://github.com/codecombat/codecombat/wiki/Dev-Setup:-Linux), [Windows](https://github.com/codecombat/codecombat/wiki/Dev-Setup:-Windows), or
[Vagrant](https://github.com/codecombat/codecombat/wiki/Dev-Setup:-Vagrant). See [the docs for details](https://github.com/codecombat/codecombat/wiki/Dev-Setup:-General-Information).
We've made it easy to fork the project, run a simple script that'll install all
the dependencies, and get a local copy of CodeCombat running right away on
[Mac](../../wiki/Dev-Setup:-Mac), [Linux](../../wiki/Dev-Setup:-Linux),
[Windows](../../wiki/Dev-Setup:-Windows), or [Vagrant](../../wiki/Dev-Setup:-Vagrant).
See [the docs for details](../../wiki/Dev-Setup:-General-Information).
### [Getting In Touch](https://github.com/codecombat/codecombat/wiki/Developer-organization)
### [Getting In Touch](../../wiki/Developer-organization)
Whether you're novice or pro, the CodeCombat team is ready to help you implement your ideas. Reach out on our [forum](http://discourse.codecombat.com), our [issue tracker](https://github.com/codecombat/codecombat/issues), or [our developer chat room on Slack](https://coco-slack-invite.herokuapp.com/), or see the docs for [more on how to contribute](https://github.com/codecombat/codecombat/wiki/Developer-organization).
Whether you're novice or pro, the CodeCombat team is ready to help you implement
your ideas. Reach out on our [forum](http://discourse.codecombat.com), our
[issue tracker](../../issues), or
[our developer chat room on Slack](https://coco-slack-invite.herokuapp.com/), or
see the docs for [more on how to contribute](../../wiki/Developer-organization).
[![Slack Status](https://coco-slack-invite.herokuapp.com/badge.svg)](https://coco-slack-invite.herokuapp.com/)
### [License](https://github.com/codecombat/codecombat/blob/master/LICENSE)
### [License](LICENSE)
[MIT](https://github.com/codecombat/codecombat/blob/master/LICENSE) for the code, and [CC-BY](http://codecombat.com/legal) for the art and music. Please also [sign the CodeCombat contributor license agreement](http://codecombat.com/cla) so we can accept your pull requests. It is easy.
[MIT](LICENSE) for the code, and [CC-BY](http://codecombat.com/legal) for the
art and music. Please also
[sign the CodeCombat contributor license agreement](http://codecombat.com/cla)
so we can accept your pull requests. It is easy.
### [Join Us!](http://blog.codecombat.com/why-you-should-open-source-your-startup)
![Nick Winter](http://codecombat.com/images/pages/about/nick_small.png)
![George Saines](http://codecombat.com/images/pages/about/george_small.png)
![Scott Erickson](http://codecombat.com/images/pages/about/scott_small.png)
![Matt Lott](http://codecombat.com/images/pages/about/matt_small.png)
![Catherine Weresow](http://codecombat.com/images/pages/about/cat_small.png)
![Maka Gradin](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Maka%20Gradin/maka_gradin_100.png)
![Rob Blanckaert](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Rob%20Blanckaert/rob_blanckaert_100.png)
![Josh Callebaut](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Josh%20Callebaut/josh_callebaut_100.png)
![Michael Schmatz](http://codecombat.com/images/pages/about/michael_small.png)
![Josh Lee](http://codecombat.com/images/pages/about/josh_small.png)
![Alex Cotsarelis](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Alex%20Cotsarelis/alex_100.png)
![Alex Crooks](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Alex%20Crooks/alex_100.png)
![Alexandru Caciulescu](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Alexandru%20Caciulescu/alexandru_100.png)
![Andreas Linn](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Andreas%20Linn/andreas_100.png)
![Andrew Witcher](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Andrew%20Witcher/andrew_100.png)
![Axandre Oge](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Axandre%20Oge/axandre_100.png)
![Bang Honam](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Bang%20Honam/bang_100.png)
![Benjamin Stern](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Benjamin%20Stern/benjamin_100.png)
![Brad Dickason](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Brad%20Dickason/brad_100.png)
![Carlos Maia](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Carlos%20Maia/carlos_maia_100.png)
![Chloe Fan](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Chloe%20Fan/chloe_100.png)
![Dan Ristic](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Dan%20Ristic/dan_100.png)
![Danny Whittaker](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Danny%20Whittaker/danny_100.png)
![David Liu](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/David%20Liu/david_liu_100.png)
![David Pendray](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/David%20Pendray/david_100.png)
![Deepak1556](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Deepak1556/deepak_100.png)
![Derek Wong](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Derek%20Wong/derek_100.png)
![Dominik Kundel](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Dominik%20Kundel/dominik_k_100.png)
![Glen De Cauwsemaecker](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Glen%20de%20Cauwsemaecker/glen_100.png)
![Ian Li](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Ian%20Li/ian_100.png)
![Jeremy Arns](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Jeremy%20Arns/jeremy_100.png)
![Joachim Brehmer](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Joachim%20Brehmer/joachim_100.png)
![Jose Antonini](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Jose%20Antonini/jose_antonini_100.png)
![Katharine Chan](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Katharine%20Chan/katharine_100.png)
![Ken Stanley](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Ken%20Stanley/ken_100.png)
![Kevin Holland](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Kevin%20Holland/kevin_100.png)
![Laura Watiker](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Laura%20Watiker/laura_100.png)
![Michael Heasell](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Michael%20Heasell/michael_100.png)
![Michael Polyak](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Michael%20Polyak/michael_100.png)
![Mischa Lewis-Norelle](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Mischa%20Lewis-Norelle/mischa_100.png)
![Nathan Gosset](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Nathan%20Gosset/nathan_100.png)
![Oleg Ulyanicky](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Oleg%20Ulyanickiy/oleg_100.png)
![Paul Buser](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Paul%20Buser/paul_100.png)
![Pavel Konstantynov](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Pavel%20Konstantinov/pavel_100.png)
![Popey Gilbert](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Popey%20Gilbert/popey_100.png)
![Prabhsimran Baweja](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Prabhsimran%20Baweja/prabhsimran_100.png)
![Rachel Xiang](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Rachel%20Xiang/rachel_100.png)
![Rebecca Saines](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Rebecca%20Saines/rebecca_100.png)
![Robert Moreton](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Robert%20Moreton/robert_100.png)
![Ronnie Cheng](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Ronnie%20Cheng/ronnie_100.png)
![Ruben Vereecken](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Ruben%20Vereecken/ruben_100.png)
![Russ Fan](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Russ%20Fan/russ_100.png)
![Shiying Zheng](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Shying%20Zheng/shiyeng_100.png)
![Sébastien Moratinos](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Tom%20Steinbrecher/tom_100.png)
![Thanish Muhammed](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Thanish%20Muhammed/thanish_100.png)
![Tom Steinbrecher](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Tom%20Steinbrecher/tom_100.png)
![Yang Shun Tay](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Yang%20Shun%20Tay/yang_shun_tay_100.png)
![Zach Martin](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Zach%20Martin/zack_100.png)
![Nick Winter](http://codecombat.com/images/pages/about/nick_small.png "Nick Winter")
![George Saines](http://codecombat.com/images/pages/about/george_small.png "George Saines")
![Scott Erickson](http://codecombat.com/images/pages/about/scott_small.png "Scott Erickson")
![Matt Lott](http://codecombat.com/images/pages/about/matt_small.png "Matt Lott")
![Catherine Weresow](http://codecombat.com/images/pages/about/cat_small.png "Catherine Weresow")
![Maka Gradin](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Maka%20Gradin/maka_gradin_100.png "Maka Gradin")
![Rob Blanckaert](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Rob%20Blanckaert/rob_blanckaert_100.png "Rob Blanckaert")
![Josh Callebaut](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Josh%20Callebaut/josh_callebaut_100.png "Josh Callebaut")
![Michael Schmatz](http://codecombat.com/images/pages/about/michael_small.png "Michael Schmatz")
![Josh Lee](http://codecombat.com/images/pages/about/josh_small.png "Josh Lee")
![Alex Cotsarelis](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Alex%20Cotsarelis/alex_100.png "Alex Cotsarelis")
![Alex Crooks](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Alex%20Crooks/alex_100.png "Alex Crooks")
![Alexandru Caciulescu](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Alexandru%20Caciulescu/alexandru_100.png "Alexandru Caciulescu")
![Andreas Linn](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Andreas%20Linn/andreas_100.png "Andreas Linn")
![Andrew Witcher](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Andrew%20Witcher/andrew_100.png "Andrew Witcher")
![Axandre Oge](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Axandre%20Oge/axandre_100.png "Axandre Oge")
![Bang Honam](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Bang%20Honam/bang_100.png "Bang Honam")
![Benjamin Stern](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Benjamin%20Stern/benjamin_100.png "Benjamin Stern")
![Brad Dickason](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Brad%20Dickason/brad_100.png "Brad Dickason")
![Carlos Maia](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Carlos%20Maia/carlos_maia_100.png "Carlos Maia")
![Chloe Fan](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Chloe%20Fan/chloe_100.png "Chloe Fan")
![Dan Ristic](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Dan%20Ristic/dan_100.png "Dan Ristic")
![Danny Whittaker](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Danny%20Whittaker/danny_100.png "Danny Whittaker")
![David Liu](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/David%20Liu/david_liu_100.png "David Liu")
![David Pendray](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/David%20Pendray/david_100.png "David Pendray")
![Deepak1556](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Deepak1556/deepak_100.png "Deepak1556")
![Derek Wong](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Derek%20Wong/derek_100.png "Derek Wong")
![Dominik Kundel](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Dominik%20Kundel/dominik_k_100.png "Dominik Kundel")
![Glen De Cauwsemaecker](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Glen%20de%20Cauwsemaecker/glen_100.png "Glen De Cauwsemaecker")
![Ian Li](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Ian%20Li/ian_100.png "Ian Li")
![Jeremy Arns](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Jeremy%20Arns/jeremy_100.png "Jeremy Arns")
![Joachim Brehmer](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Joachim%20Brehmer/joachim_100.png "Joachim Brehmer")
![Jose Antonini](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Jose%20Antonini/jose_antonini_100.png "Jose Antonini")
![Katharine Chan](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Katharine%20Chan/katharine_100.png "Katharine Chan")
![Ken Stanley](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Ken%20Stanley/ken_100.png "Ken Stanley")
![Kevin Holland](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Kevin%20Holland/kevin_100.png "Kevin Holland")
![Laura Watiker](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Laura%20Watiker/laura_100.png "Laura Watiker")
![Michael Heasell](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Michael%20Heasell/michael_100.png "Michael Heasell")
![Michael Polyak](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Michael%20Polyak/michael_100.png "Michael Polyak")
![Mischa Lewis-Norelle](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Mischa%20Lewis-Norelle/mischa_100.png "Mischa Lewis-Norelle")
![Nathan Gosset](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Nathan%20Gosset/nathan_100.png "Nathan Gosset")
![Oleg Ulyanicky](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Oleg%20Ulyanickiy/oleg_100.png "Oleg Ulyanicky")
![Paul Buser](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Paul%20Buser/paul_100.png "Paul Buser")
![Pavel Konstantynov](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Pavel%20Konstantinov/pavel_100.png "Pavel Konstantynov")
![Popey Gilbert](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Popey%20Gilbert/popey_100.png "Popey Gilbert")
![Prabhsimran Baweja](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Prabhsimran%20Baweja/prabhsimran_100.png "Prabhsimran Baweja")
![Rachel Xiang](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Rachel%20Xiang/rachel_100.png "Rachel Xiang")
![Rebecca Saines](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Rebecca%20Saines/rebecca_100.png "Rebecca Saines")
![Robert Moreton](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Robert%20Moreton/robert_100.png "Robert Moreton")
![Ronnie Cheng](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Ronnie%20Cheng/ronnie_100.png "Ronnie Cheng")
![Ruben Vereecken](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Ruben%20Vereecken/ruben_100.png "Ruben Vereecken")
![Russ Fan](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Russ%20Fan/russ_100.png "Russ Fan")
![Shiying Zheng](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Shying%20Zheng/shiyeng_100.png "Shiying Zheng")
![Sébastien Moratinos](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Tom%20Steinbrecher/tom_100.png "Sébastien Moratinos")
![Thanish Muhammed](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Thanish%20Muhammed/thanish_100.png "Thanish Muhammed")
![Tom Steinbrecher](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Tom%20Steinbrecher/tom_100.png "Tom Steinbrecher")
![Yang Shun Tay](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Yang%20Shun%20Tay/yang_shun_tay_100.png "Yang Shun Tay")
![Zach Martin](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Zach%20Martin/zack_100.png "Zach Martin")

BIN
app/assets/images/pages/home/course_languages.png Executable file → Normal file

Binary file not shown.

Before

(image error) Size: 6.5 KiB

After

(image error) Size: 3.8 KiB

Before After
Before After

View file

@ -33,6 +33,7 @@ module.exports = class CocoRouter extends Backbone.Router
'admin/design-elements': go('admin/DesignElementsView')
'admin/files': go('admin/FilesView')
'admin/analytics': go('admin/AnalyticsView')
'admin/school-counts': go('admin/SchoolCountsView')
'admin/analytics/subscriptions': go('admin/AnalyticsSubscriptionsView')
'admin/level-sessions': go('admin/LevelSessionsView')
'admin/users': go('admin/UsersView')

View file

@ -12,6 +12,8 @@ app = require 'core/application'
World = require 'lib/world/world'
utils = require 'core/utils'
LOG = false
# This is an initial stab at unifying loading and setup into a single place which can
# monitor everything and keep a LoadingScreen visible overall progress.
#
@ -60,6 +62,8 @@ module.exports = class LevelLoader extends CocoClass
@listenToOnce @level, 'sync', @onLevelLoaded
onLevelLoaded: ->
if not @sessionless and @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course']
@sessionDependenciesRegistered = {}
if (@courseID and @level.get('type', true) not in ['course', 'course-ladder']) or window.serverConfig.picoCTF
# Because we now use original hero levels for both hero and course levels, we fake being a course level in this context.
originalGet = @level.get
@ -81,8 +85,6 @@ module.exports = class LevelLoader extends CocoClass
# Session Loading
loadFakeSession: ->
if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop']
@sessionDependenciesRegistered = {}
initVals =
level:
original: @level.get('original')
@ -111,9 +113,6 @@ module.exports = class LevelLoader extends CocoClass
@loadDependenciesForSession @session
loadSession: ->
if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course']
@sessionDependenciesRegistered = {}
if @sessionID
url = "/db/level.session/#{@sessionID}"
url += "?interpret=true" if @spectateMode
@ -147,7 +146,7 @@ module.exports = class LevelLoader extends CocoClass
@listenToOnce @opponentSession, 'sync', @loadDependenciesForSession
loadDependenciesForSession: (session) ->
console.log "Loading dependencies for session: ", session
console.log "Loading dependencies for session: ", session if LOG
if me.id isnt session.get 'creator'
session.patch = session.save = -> console.error "Not saving session, since we didn't create it."
else if codeLanguage = utils.getQueryVariable 'codeLanguage'
@ -171,12 +170,11 @@ module.exports = class LevelLoader extends CocoClass
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')
console.log "Course mode, loading custom hero: ", heroConfig
return if not heroConfig
url = "/db/thang.type/#{heroConfig.thangType}/version"
heroThangType = me.get('heroConfig')?.thangType or ThangType.heroes.captain
console.log "Course mode, loading custom hero: ", heroThangType if LOG
url = "/db/thang.type/#{heroThangType}/version"
if heroResource = @maybeLoadURL(url, ThangType, 'thang')
console.log "Pushing resource: ", heroResource
console.log "Pushing resource: ", heroResource if LOG
@worldNecessities.push heroResource
@sessionDependenciesRegistered[session.id] = true
return
@ -345,7 +343,7 @@ module.exports = class LevelLoader extends CocoClass
true
onWorldNecessitiesLoaded: ->
console.log "World necessities loaded."
console.log "World necessities loaded." if LOG
@initWorld()
@supermodel.clearMaxProgress()
@trigger 'world-necessities-loaded'
@ -374,7 +372,7 @@ module.exports = class LevelLoader extends CocoClass
onSupermodelLoaded: ->
return if @destroyed
console.log 'SuperModel for Level loaded in', new Date().getTime() - @t0, 'ms'
console.log 'SuperModel for Level loaded in', new Date().getTime() - @t0, 'ms' if LOG
@loadLevelSounds()
@denormalizeSession()
@ -482,7 +480,7 @@ module.exports = class LevelLoader extends CocoClass
@world.difficulty = Math.max 0, @world.difficulty - 1 # Show the difficulty they won, not the next one.
serializedLevel = @level.serialize(@supermodel, @session, @opponentSession)
@world.loadFromLevel serializedLevel, false
console.log 'World has been initialized from level loader.'
console.log 'World has been initialized from level loader.' if LOG
# Initial Sound Loading

View file

@ -12,20 +12,18 @@ module.exports =
continue if not instance
instance.numCompleted = 0
instance.started = false
levels = classroom.getLevels({courseID: course.id, withoutLadderLevels: true})
levels = classroom.getLevels({courseID: course.id})
for userID in instance.get('members')
instance.started ||= _.any levels.models, (level) ->
return false if level.isLadder()
session = _.find classroom.sessions.models, (session) ->
session.get('creator') is userID and session.get('level').original is level.get('original')
session?
levelCompletes = _.map levels.models, (level) ->
return true if level.isLadder()
#TODO: Hella slow! Do the mapping first!
session = _.find classroom.sessions.models, (session) ->
sessions = _.filter classroom.sessions.models, (session) ->
session.get('creator') is userID and session.get('level').original is level.get('original')
# sessionMap[userID][level].completed()
session?.completed()
_.find(sessions, (s) -> s.completed())
if _.every levelCompletes
instance.numCompleted += 1
@ -34,14 +32,14 @@ module.exports =
for course, courseIndex in courses.models
instance = courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id })
continue if not instance
levels = classroom.getLevels({courseID: course.id, withoutLadderLevels: true})
levels = classroom.getLevels({courseID: course.id})
for level, levelIndex in levels.models
userIDs = []
for user in students.models
userID = user.id
session = _.find classroom.sessions.models, (session) ->
sessions = _.filter classroom.sessions.models, (session) ->
session.get('creator') is userID and session.get('level').original is level.get('original')
if not session?.completed()
if not _.find(sessions, (s) -> s.completed())
userIDs.push userID
if userIDs.length > 0
users = _.map userIDs, (id) ->
@ -61,16 +59,16 @@ module.exports =
courseIndex = courses.models.length - courseIndex - 1 #compensate for reverse
instance = courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id })
continue if not instance
levels = classroom.getLevels({courseID: course.id, withoutLadderLevels: true})
levels = classroom.getLevels({courseID: course.id})
levelModels = levels.models.slice()
for level, levelIndex in levelModels.reverse() #
levelIndex = levelModels.length - levelIndex - 1 #compensate for reverse
userIDs = []
for user in students.models
userID = user.id
session = _.find classroom.sessions.models, (session) ->
sessions = _.filter classroom.sessions.models, (session) ->
session.get('creator') is userID and session.get('level').original is level.get('original')
if session?.completed() #
if _.find(sessions, (s) -> s.completed()) #
userIDs.push userID
if userIDs.length > 0
users = _.map userIDs, (id) ->
@ -91,7 +89,7 @@ module.exports =
conceptData[classroom.id] = {}
for course, courseIndex in courses.models
levels = classroom.getLevels({courseID: course.id, withoutLadderLevels: true})
levels = classroom.getLevels({courseID: course.id})
for level in levels.models
levelID = level.get('original')
@ -102,16 +100,16 @@ module.exports =
for concept in level.get('concepts')
for userID in classroom.get('members')
session = _.find classroom.sessions.models, (session) ->
sessions = _.filter classroom.sessions.models, (session) ->
session.get('creator') is userID and session.get('level').original is levelID
if not session # haven't gotten to this level yet, but might have completed others before
if _.size(sessions) is 0 # haven't gotten to this level yet, but might have completed others before
for concept in level.get('concepts')
conceptData[classroom.id][concept].completed = false
if session # have gotten to the level and at least started it
if _.size(sessions) > 0 # have gotten to the level and at least started it
for concept in level.get('concepts')
conceptData[classroom.id][concept].started = true
if not session?.completed() # level started but not completed
if not _.find(sessions, (s) -> s.completed()) # level started but not completed
for concept in level.get('concepts')
conceptData[classroom.id][concept].completed = false
conceptData
@ -139,7 +137,7 @@ module.exports =
continue
progressData[classroom.id][course.id] = { completed: true, started: false } # to be updated
levels = classroom.getLevels({courseID: course.id, withoutLadderLevels: true})
levels = classroom.getLevels({courseID: course.id})
for level in levels.models
levelID = level.get('original')
progressData[classroom.id][course.id][levelID] = {
@ -154,12 +152,12 @@ module.exports =
courseProgress = progressData[classroom.id][course.id]
courseProgress[userID] ?= { completed: true, started: false, levelsCompleted: 0 } # Only set it the first time through a user
courseProgress[levelID][userID] = { completed: true, started: false } # These don't matter, will always be set
session = _.find classroom.sessions.models, (session) ->
sessions = _.filter 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[levelID][userID].session = _.find(sessions, (s) -> s.completed()) or _.first(sessions)
if _.size(sessions) is 0 # haven't gotten to this level yet, but might have completed others before
courseProgress.started ||= false #no-op
courseProgress.completed = false
courseProgress[userID].started ||= false #no-op
@ -169,22 +167,23 @@ module.exports =
courseProgress[levelID][userID].started = false
courseProgress[levelID][userID].completed = false
if session # have gotten to the level and at least started it
if _.size(sessions) > 0 # have gotten to the level and at least started it
courseProgress.started = true
courseProgress[userID].started = true
courseProgress[levelID].started = true
courseProgress[levelID][userID].started = true
courseProgress[levelID][userID].lastPlayed = new Date(session.get('changed'))
courseProgress[levelID][userID].lastPlayed = new Date(Math.max(_.map(sessions, 'changed')))
courseProgress[levelID].numStarted += 1
if session?.completed() # have finished this level
if _.find(sessions, (s) -> s.completed()) # have finished this level
courseProgress.completed &&= true #no-op
courseProgress[userID].completed &&= true #no-op
courseProgress[userID].levelsCompleted += 1
courseProgress[levelID].completed &&= true #no-op
# courseProgress[levelID].numCompleted += 1
courseProgress[levelID][userID].completed = true
courseProgress[levelID][userID].dateFirstCompleted = new Date(session.get('dateFirstCompleted') || session.get('changed'))
dates = (s.get('dateFirstCompleted') || s.get('changed') for s in sessions)
courseProgress[levelID][userID].dateFirstCompleted = new Date(Math.max(dates...))
else # level started but not completed
courseProgress.completed = false
courseProgress[userID].completed = false

View file

@ -166,7 +166,7 @@ module.exports = class World
shouldUpdateProgress = @shouldUpdateRealTimePlayback t2
shouldDelayRealTimeSimulation = not shouldUpdateProgress and @shouldDelayRealTimeSimulation t2
else
shouldUpdateProgress = t2 - t1 > PROGRESS_UPDATE_INTERVAL
shouldUpdateProgress = t2 - t1 > PROGRESS_UPDATE_INTERVAL# and (@frames.length - @framesSerializedSoFar >= @frameRate or t2 - t1 > 1000)
shouldDelayRealTimeSimulation = false
return true unless shouldUpdateProgress or shouldDelayRealTimeSimulation
# Stop loading frames for now; continue in a moment.

View file

@ -71,7 +71,7 @@
curriculum: "Total curriculum hours:"
ffa: "Free for all students"
lesson_time: "Lesson time:"
coming_soon: "Coming soon!"
coming_soon: "Coming this fall!"
courses_available_in: "Courses are available in JavaScript, Python, and Java (coming soon!)"
boast: "Boasts riddles that are complex enough to fascinate gamers and coders alike."
winning: "A winning combination of RPG gameplay and programming homework that pulls off making kid-friendly education legitimately enjoyable."
@ -815,6 +815,7 @@
more_info_1: "Our"
more_info_2: "teachers forum"
more_info_3: "is a good place to connect with fellow educators who are using CodeCombat."
licenses_needed: "Licenses needed"
teachers_quote:
name: "Demo Form"

View file

@ -183,46 +183,46 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
campaign_old_multiplayer_description: "Reliquias de una era más civilizada. Ninguna simulación es ejecutada para estas arenas multijugador antiguas y sin héroes."
code:
# if: "if" # Keywords--these translations show up on hover, so please translate them all, even if it's kind of long. (In the code editor, they will still be in English.)
# else: "else"
# elif: "else if"
# while: "while"
if: "si" # Keywords--these translations show up on hover, so please translate them all, even if it's kind of long. (In the code editor, they will still be in English.)
else: "otro"
elif: "si no"
while: "mientras"
# loop: "loop"
# for: "for"
for: "por"
# break: "break"
# continue: "continue"
# pass: "pass"
continue: "continuar"
pass: "pasar"
# return: "return"
# then: "then"
# do: "do"
# end: "end"
# function: "function"
# def: "define"
# var: "variable"
then: "entonces"
do: "hacer"
end: "fin"
function: "función"
def: "define"
var: "variable"
# self: "self"
# hero: "hero"
# this: "this"
# or: "or"
# "||": "or"
or: "o"
"||": "o"
and: "y"
"&&": "y"
not: "no"
"!": "no"
# "=": "assign"
"=": "asigne a"
"==": "iguala"
"===": "iguala estrictamente"
"!=": "no iguala"
# "!==": "does not strictly equal"
# ">": "is greater than"
# ">=": "is greater than or equal"
# "<": "is less than"
# "<=": "is less than or equal"
# "*": "multiplied by"
# "/": "divided by"
"+": "mas"
">": "es mayor que"
">=": "es mayor que o igual"
"<": "es menor que"
"<=": "es menor que o igual"
"*": "multiplicado por"
"/": "dividido por"
"+": "más"
"-": "menos"
# "+=": "add and assign"
# "-=": "subtract and assign"
"+=": "añade y asigne"
"-=": "elimine y asigne"
True: "Verdadero"
true: "verdadero"
False: "Falso"
@ -425,7 +425,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
victory_new_item: "Objeto Nuevo"
victory_viking_code_school: "¡Changos macacos!, el nivel que acabas de pasar era dificil! Si todavía no eres un desarrollador de software, deberías serlo. Acabas de conseguir una aceptación por vía rápida con la Escuela Vikinga de Có, donde tú puedes llevar tus habilidades al siguiente nivel y convertirteen un desarrollador web profesional en 14 semanas."
victory_become_a_viking: "Conviértete en un Vikingo"
# victory_no_progress_for_teachers: "El progreso no es guardado para maestros. But, you can add a student account to your classroom for yourself."
victory_no_progress_for_teachers: "El progreso no es guardado para maestros. Pero puede añadir cuenta de estudiante a su aula, por su mismo."
guide_title: "Guía"
tome_cast_button_run: "Ejecutar"
tome_cast_button_running: "Ejecutando"
@ -512,14 +512,14 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
# tip_adding_orgres: "Rounding up ogros."
tip_sharpening_swords: "Afilando las espadas."
# tip_ratatouille: "You must not let anyone define your limits because of where you come from. Your only limit is your soul. - Gusteau, Ratatouille"
# tip_nemo: "Cuando la vida gets you down, want to know what you've gotta do? Just keep swimming, just keep swimming. - Dory, Finding Nemo"
tip_nemo: "¿Cuando huye la suerte, sabes que hay que hacer? Sigue nadando, sigue nadando. - Dory, Finding Nemo"
# tip_internet_weather: "Just move to the internet, it's great here. We get to live inside where the weather is always awesome. - John Green"
# tip_nerds: "Nerds are allowed to love stuff, like jump-up-and-down-in-the-chair-can't-control-yourself love it. - John Green"
# tip_self_taught: "I taught myself 90% of what I've learned. And that's normal! - Hank Green"
# tip_luna_lovegood: "No te preocupes, you're just as sane as I am. - Luna Lovegood"
tip_luna_lovegood: "No te preocupes, estas tan cuerdo como yo. - Luna Lovegood"
# tip_good_idea: "The best way to have a good idea is to have a lot of ideas. - Linus Pauling"
# tip_programming_not_about_computers: "La ciencia cpomputacional is no more about computers than astronomy is about telescopes. - Edsger Dijkstra"
tip_mulan: "Cree que puedes, y entonces lo harás. - Mulan"
tip_mulan: "Si crees que puedes, entonces lo harás. - Mulan"
game_menu:
inventory_tab: "Inventario"
@ -855,7 +855,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
thanks_header: "¡Gracias por solicitar un presupuesto!" # {change}
thanks_sub_header: "Gracias por el interés de su institución en CodeCombat" #"Gracias por expressing interest in CodeCombat for your school."
thanks_p: "Estaremos en contacto pronto. ¿Preguntas? Escríbenos:" # {change}
back_to_classes: "Volver a las clases"#"Back to Clases"
back_to_classes: "Volver a las clases" #"Back to Clases"
finish_signup: "Termine la creación de su cuenta de maestro:"
finish_signup_p: "Crear una cuenta para configurar la clase, agregar estudiante y monitorear su progreso a medida que aprenden programacioón"#"Create an account to set up a class, add your students, and monitor their progress as they learn computer science."
signup_with: "Registrarse con:"
@ -866,7 +866,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
# create_account_subtitle: "Get access to teacher-only tools for using CodeCombat in the classroom. <strong>Set up a class</strong>, add your students, and <strong>monitor their progress</strong>!"
# convert_account_title: "Update to Teacher Account"
not: "No"
# setup_a_class: "Set Up a Class"
setup_a_class: "Crear un clase"
versions:
save_version_title: "Guardar nueva versión"
@ -1266,11 +1266,11 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
# changelog: "View latest changes to course levels."
select_language: "Seleccione lenguaje"
select_level: "Seleccione nivel"
# play_level: "Play Nivel"
play_level: "Juga Nivel"
concepts_covered: "Conceptos Cubiertos"
# print_guide: "Print Guide (PDF)"
# view_guide_online: "View Guide Online (PDF)"
# last_updated: "Last updated:"
last_updated: "Ultima revisión:"
# grants_lifetime_access: "Grants access to all Courses."
# enrollment_credits_available: "Enrollment Credits Available:"
description: "Descripción" # ClassroomSettingsModal
@ -1287,19 +1287,19 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
# avg_student_exp_advanced: "Advanced - extensive experience with typed code"
# avg_student_exp_varied: "Varied Levels of Experience"
# student_age_range_label: "Student Age Range"
# student_age_range_younger: "Younger than 6"
# student_age_range_older: "Older than 18"
student_age_range_younger: "Menor que than 6"
student_age_range_older: "Mayor que 18"
# student_age_range_to: "to"
create_class: "Crear Grupo"
# class_name: "Class Name"
class_name: "Nombre de clase"
# teacher_account_restricted: "Your account is a teacher account, and so cannot access student content."
teacher:
# teacher_dashboard: "Teacher Dashboard" # Navbar
# my_classes: "My Classes"
# courses: "Courses"
# enrollments: "Enrollments"
# resources: "Resources"
enrollments: "Recursos"
resources: "Resources"
help: "Ayuda"
# students: "Students" # Shared
language: "Lenguaje"
@ -1307,9 +1307,9 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
# complete: "Complete"
# access_restricted: "Account Update Required"
# teacher_account_required: "A teacher account is required to access this content."
# create_teacher_account: "Create Teacher Account"
# what_is_a_teacher_account: "What's a Teacher Account?"
# teacher_account_explanation: "A CodeCombat Teacher account allows you to set up classrooms, monitor students progress as they work through courses, manage enrollments and access resources to aid in your curriculum-building."
create_teacher_account: "Crear Cuenta de Maestra"
what_is_a_teacher_account: "Cuál es una Cuenta de Maestra?"
# teacher_account_explanation: "Una Cuenta de Maestra en CodeCombat Teacher da permiso a crear grupo, monitor students progress as they work through courses, manage enrollments and access resources to aid in your curriculum-building."
# current_classes: "Current Classes"
# archived_classes: "Archived Classes"
# archived_classes_blurb: "Classes can be archived for future reference. Unarchive a class to view it in the Current Classes list again."

View file

@ -14,7 +14,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
for_developers: "Pour développeurs" # Not currently shown on home page.
or_ipad: "Ou télécharger pour iPad"
new_home:
new_home:
slogan: "Le jeu le plus engageant pour apprendre la programmation."
classroom_edition: "Édition Classe:"
learn_to_code: "Apprend à programmer:"
@ -79,7 +79,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
teachers: "Enseignants!"
teachers_and_educators: "Enseignants et éducateurs"
class_in_box: "Apprenez comment notre plateforme classe-tout-inclus s'adapte à votre curriculum."
# get_started: "Get Started"
get_started: "Commencer"
students: "Étudiants:"
join_class: "Joindre une classe"
role: "Votre rôle:"
@ -515,9 +515,9 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
tip_nemo: "Quand la vie vous rabaisse, vous savez ce que vous devenez faire ? Juste continuer de nager, juste continuer de nager. - Dory, Finding Nemo"
tip_internet_weather: "Just move to the internet, it's great here. We get to live inside where the weather is always awesome. - John Green"
# tip_nerds: "Nerds are allowed to love stuff, like jump-up-and-down-in-the-chair-can't-control-yourself love it. - John Green"
# tip_self_taught: "I taught myself 90% of what I've learned. And that's normal! - Hank Green"
# tip_luna_lovegood: "Don't worry, you're just as sane as I am. - Luna Lovegood"
# tip_good_idea: "The best way to have a good idea is to have a lot of ideas. - Linus Pauling"
tip_self_taught: "Je me suis enseigné 90% de ce que j'ai appris. Et c'est normal! - Hank Green"
tip_luna_lovegood: "Ne t'en fais pas, tu es aussi sain que moi. - Luna Lovegood"
tip_good_idea: "La meilleure façon d'avoir une bonne idée est d'avoir beaucoup d'idées. - Linus Pauling"
# tip_programming_not_about_computers: "Computer Science is no more about computers than astronomy is about telescopes. - Edsger Dijkstra"
# tip_mulan: "Believe you can, then you will. - Mulan"
@ -585,7 +585,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
feature5: "Tutoriels vidéo"
feature6: "Assitance par e-mail dédiée"
feature7: "<strong>Clans</strong> privés"
# feature8: "<strong>No ads!</strong>"
feature8: "<strong>Sans pubs!</strong>"
free: "Gratuit"
month: "mois"
must_be_logged: "Vous devez être identifié. Veuillez créer un compte ou vous identifier depuis le menu ci-dessus."
@ -725,16 +725,16 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
editor_config_behaviors_description: "Ferme automatiquement les accolades, parenthèses, et chaînes de caractères."
about:
# main_title: "If you want to learn to program, you need to write (a lot of) code."
# main_description: "At CodeCombat, our job is to make sure you're doing that with a smile on your face."
# mission_link: "Mission"
# team_link: "Team"
# story_link: "Story"
# press_link: "Press"
# mission_title: "Our mission: make programming accessible to every student on Earth."
# mission_description_1: "<strong>Programming is magic</strong>. It's the ability to create things from pure imagination. We started CodeCombat to give learners the feeling of wizardly power at their fingertips by using <strong>typed code</strong>."
main_title: "Si tu veux apprendre la programmation, tu dois écrire beaucoup de code."
main_description: "Chez CodeCombat, notre but est d'assurer que tu le fasses avec un sourire."
mission_link: "Mission"
team_link: "Equipe"
story_link: "Histoire"
press_link: "Presse"
mission_title: "Notre mission: faire en sorte que la programmation soit accessible à chaque élève sur la Terre."
# mission_description_1: "<strong>Programming is magic.</strong>. It's the ability to create things from pure imagination. We started CodeCombat to give learners the feeling of wizardly power at their fingertips by using <strong>typed code</strong>."
# mission_description_2: "As it turns out, that enables them to learn faster too. WAY faster. It's like having a conversation instead of reading a manual. We want to bring that conversation to every school and to <strong>every student</strong>, because everyone should have the chance to learn the magic of programming."
# team_title: "Meet the CodeCombat team"
team_title: "Rencontrez l'équipe CodeCombat."
# team_values: "We value open and respectful dialog, where the best idea wins. Our decisions are grounded in customer research and our process is focused on delivering tangible results for them. Everyone is hands-on, from our CEO to our Github contributors, because we value growth and learning in our team."
nick_title: "Programmeur" # {change}
nick_blurb: "Gourou de Motivation"
@ -975,7 +975,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
social_facebook: "Aimer CodeCombat sur Facebook"
social_twitter: "Suivre CodeCombat sur Twitter"
social_gplus: "Rejoindre CodeCombat sur Google+"
# social_slack: "Chat with us in the public CodeCombat Slack channel"
social_slack: "Bavardez avec nous sur la chaîne publique Slack de CodeCombat."
contribute_to_the_project: "Contribuer au projet"
clans:
@ -1027,15 +1027,15 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
# track_concepts1: "Track concepts"
track_concepts2a: "appris par chaque élèves"
track_concepts2b: "appris par chaque membres"
track_concepts3a: "Suivre les niveaux complétés par chaque élèves"
track_concepts3b: "Suivre les niveaux complétés par chaque membres"
# track_concepts4a: "See your students'"
# track_concepts4b: "See your members'"
track_concepts5: "solutions"
track_concepts3a: "Suivre les niveaux complétés par chaque élève"
track_concepts3b: "Suivre les niveaux complétés par chaque membre"
track_concepts4a: "Voir vos élèves"
track_concepts4b: "Voir vos membres'"
track_concepts5: "Solutions"
track_concepts6a: "Classer les élèves par nom ou avancement"
track_concepts6b: "Classer les membres par nom ou avancement"
track_concepts7: "Nécessite une invitation"
# track_concepts8: "to join"
track_concepts8: "Joindre"
private_require_sub: "Les clans privés nécessitent un abonnement pour être créés ou rejoins."
courses:
@ -1089,7 +1089,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
purchasing_for: "Vous achetez une licence pour"
creating_for: "Vous créez une classe pour"
for: "pour" # Like in 'for 30 students'
# receive_code: "Afterwards you will receive an unlock code to distribute to your students, which they can use to enroll in your class."
receive_code: "Après, vous recevrez un code à distribuer à vos élèves, pour qu'ils puissent s'inscrir dans votre cours."
free_trial: "Essai gratuit pour les professeurs !"
get_access: "pour obtenir un accès individuel à tous les cours pour évaluation."
questions: "Questions?"
@ -1101,8 +1101,8 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
no_experience: "Aucune expérience en développement requise"
easy_monitor: "Suivez facilement la progression des élèves"
purchase_for_class: "Achetez un cours pour votre classe. C'est très simple de rajouter vos élèves !"
# see_the: "See the"
more_info: "pour plus d'informations."
see_the: "Voir"
more_info: "Pour plus d'informations."
choose_course: "Choisissez votre cours:"
enter_code: "Entrez un code de déverouillage pour rejoindre une classe existante"
enter_code1: "Entrez le code de déverouillage"
@ -1125,7 +1125,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
change_language: "Changez la langue du cours"
keep_using: "Continuer à utiliser"
switch_to: "Changer"
# greetings: "Greetings!"
greetings: "Salutations!"
back_classrooms: "Retour à mes classes"
back_courses: "Retour à mes cours"
edit_details: "Modifier les informations de la classe"
@ -1162,7 +1162,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
play_now_learn_3: "des chaines de caractères & des variables pour personnaliser des actions"
play_now_learn_4: "comment battre un ogre (compétence importante dans la vie !)"
welcome_to_page: "Bienvenu sur la page des Cours !"
# completed_hoc: "Amazing! You've completed the Hour of Code course!"
completed_hoc: "Génial! Tu as fini le cours Heur de Codage!"
ready_for_more_header: "Motivé pour plus ? Jouez au mode campagne !"
ready_for_more_1: "Utilisez des gemmes pour débloquer de nouveaux acessoires !"
ready_for_more_2: "Jouez à travers de nouveaux mondes et challenges"
@ -1182,7 +1182,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
play_arena: "Jouer à l'Arene"
start: "Démarrer"
last_level: "Dernier Niveau"
# welcome_to_hoc: "Adventurers, welcome to our Hour of Code!"
welcome_to_hoc: "Aventuriers, bienvenu à note heur de codage!"
logged_in_as: "Connecté en tant que :"
not_you: "Pas vous ?"
welcome_back: "Salut aventurier, content de te revoir !"
@ -1259,17 +1259,17 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
add_students1: "ajouter des élèves"
view_edit: "voir/modifier"
students_enrolled: "élèves inscrits"
# students_assigned: "students assigned"
students_assigned: "élèves attribués"
length: "Durée:"
title: "Titre" # Flat style redesign
# subtitle: "Review course guidelines, solutions, and levels"
# changelog: "View latest changes to course levels."
# select_language: "Select language"
# select_level: "Select level"
select_language: "Selectionner langue"
select_level: "Selectionner niveau"
# play_level: "Play Level"
concepts_covered: "Conceptes Couverts"
# print_guide: "Print Guide (PDF)"
# view_guide_online: "View Guide Online (PDF)"
print_guide: "Imprimer Guide (PDF)"
view_guide_online: "Voir Guide En Ligne (PDF)"
# last_updated: "Last updated:"
# grants_lifetime_access: "Grants access to all Courses."
# enrollment_credits_available: "Enrollment Credits Available:"
@ -1294,7 +1294,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
# class_name: "Class Name"
# teacher_account_restricted: "Your account is a teacher account, and so cannot access student content."
# teacher:
teacher:
# teacher_dashboard: "Teacher Dashboard" # Navbar
# my_classes: "My Classes"
# courses: "Courses"
@ -1311,15 +1311,15 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
# what_is_a_teacher_account: "What's a Teacher Account?"
# teacher_account_explanation: "A CodeCombat Teacher account allows you to set up classrooms, monitor students progress as they work through courses, manage enrollments and access resources to aid in your curriculum-building."
# current_classes: "Current Classes"
# archived_classes: "Archived Classes"
archived_classes: "Cours Archivés"
# archived_classes_blurb: "Classes can be archived for future reference. Unarchive a class to view it in the Current Classes list again."
# view_class: "view class"
# archive_class: "archive class"
# unarchive_class: "unarchive class"
# unarchive_this_class: "Unarchive this class"
# no_students_yet: "This class has no students yet."
# add_students: "Add Students"
# create_new_class: "Create a New Class"
view_class: "Voir Cours"
archive_class: "Archiver Cours"
unarchive_class: "Désarchiver cour"
unarchive_this_class: "Désarchiver ce cours"
no_students_yet: "Ce cours n'a pas encore d'élèves."
add_students: "Ajourter Elèves"
create_new_class: "Créer une Nouveau Cour"
# class_overview: "Class Overview" # View Class page
# avg_playtime: "Average level playtime"
# total_playtime: "Total play time"
@ -1398,7 +1398,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
ambassador_title: "Ambassadeur"
ambassador_title_description: "(Aide)"
ambassador_summary: "Domptez les membres du forum, et guidez ceux qui ont besoin d'aide. Nos ambassadeurs représentent CodeCombat face au monde."
# teacher_title: "Teacher"
teacher_title: "Professeur"
editor:
main_title: "Éditeurs CodeCombat"

View file

@ -1084,7 +1084,7 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian
number_total_students: "Укупан број ученика у школи/округу"
enter_number_students: "Унеси број ученика који ти треба за овај разред."
name_class: "Именуј свој разред"
# displayed_course_page: "This will be displayed on the course page for you and your students. It can be changed later."
displayed_course_page: "Ово ће бити приказано на страници курса за тебе и твоје ученике. Може бити измењено касније."
buy: "Купи"
purchasing_for: "Купујеш лиценцу за"
creating_for: "Правиш разред за"
@ -1095,100 +1095,100 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian
questions: "Питања?"
teachers_click: "Учитељи кликните овде"
students_click: "Ученици кликните овде"
# courses_on_coco: "Courses on CodeCombat"
# designed_to: "Courses are designed to introduce computer science concepts using CodeCombat's fun and engaging environment. CodeCombat levels are organized around key topics to encourage progressive learning, over the course of 5 hours."
# more_in_less: "Learn more in less time"
# no_experience: "No coding experience necesssary"
# easy_monitor: "Easily monitor student progress"
# purchase_for_class: "Purchase a course for your entire class. It's easy to sign up your students!"
# see_the: "See the"
# more_info: "for more information."
# choose_course: "Choose Your Course:"
# enter_code: "Enter an unlock code to join an existing class"
# enter_code1: "Enter unlock code"
# enroll: "Enroll"
# pick_from_classes: "Pick from your current classes"
# enter: "Enter"
# or: "Or"
# topics: "Topics"
# hours_content: "Hours of content:"
# get_free: "Get FREE course"
# enroll_paid: "Enroll Students in Paid Courses"
# you_have1: "You have"
# you_have2: "unused paid enrollments"
# use_one: "Use 1 paid enrollment for"
# use_multiple: "Use paid enrollments for the following students:"
# already_enrolled: "already enrolled"
# licenses_remaining: "licenses remaining:"
# insufficient_enrollments: "insufficient paid enrollments"
# get_enrollments: "Get More Enrollments"
# change_language: "Change Course Language"
# keep_using: "Keep Using"
# switch_to: "Switch To"
# greetings: "Greetings!"
# back_classrooms: "Back to my classrooms"
# back_courses: "Back to my courses"
# edit_details: "Edit class details"
# enrolled_courses: "enrolled in paid courses:"
# purchase_enrollments: "Purchase Enrollments"
# remove_student: "remove student"
# assign: "Assign"
# to_assign: "to assign paid courses."
# teacher: "Teacher"
# complete: "Complete"
# none: "None"
# save: "Save"
# play_campaign_title: "Play the Campaign"
# play_campaign_description: "Youre ready to take the next step! Explore hundreds of challenging levels, learn advanced programming skills, and compete in multiplayer arenas!"
# create_account_title: "Create an Account"
courses_on_coco: "Курсеви на CodeCombat-у"
designed_to: "Курсеви су дизајнирани да представе концепте компјутерских наука користећи забавно и ангажујуће окружење CodeCombat-а. CodeCombat нивои су организовани око кључних тема како би подстакли прогресивно учење током периода од 5 сати."
more_in_less: "Научи више за мање времена"
no_experience: "Искуство у кодирању није неопходно"
easy_monitor: "Једноставно надгледај напредак ученика"
purchase_for_class: "Купи курс за свој целокупан разред. Уписивање ученика је једноставно!"
see_the: "Погледај"
more_info: "за више информација."
choose_course: "Изабери свој курс:"
enter_code: "Унеси код за откључавање да се придружиш постојећем разреду"
enter_code1: "Унеси код за откључавање"
enroll: "Упиши се"
pick_from_classes: "Изабери из својих тренутних разреда"
enter: "Унеси"
or: "Или"
topics: "Теме"
hours_content: "Сати садржаја:"
get_free: "Добиј БЕСПЛАТАН курс"
enroll_paid: "Упиши студенте у плаћене курсеве"
you_have1: "Имаш"
you_have2: "неискоришћених плаћених уписа"
use_one: "Искористи 1 плаћени упис за"
use_multiple: "Искористи плаћене уписе за следеће ученике:"
already_enrolled: "већ уписан"
licenses_remaining: "преостале лиценце:"
insufficient_enrollments: "недовољно плаћених уписа"
get_enrollments: "Добиј још уписа"
change_language: "Промени језик курса"
keep_using: "Настави да користиш"
switch_to: "Пребаци на"
greetings: "Поздрав!"
back_classrooms: "Назад на моје учионице"
back_courses: "Назад на моје курсеве"
edit_details: "Измени детаље разреда"
enrolled_courses: "уписани у плаћеним курсевима:"
purchase_enrollments: "Купи уписе"
remove_student: "уклони ученика"
assign: "Додели"
to_assign: "да доделиш плаћене курсеве."
teacher: "Учитељ"
complete: "Заврши"
none: "Нема"
save: "Сачувај"
play_campaign_title: "Играј кампању"
# play_campaign_description: "Youre ready to take the next step! Explore hundreds of challenging levels, learn advanced programming skills, and compete in multiplayer arenas!"
create_account_title: "Направи налог"
# create_account_description: "Sign up for a FREE CodeCombat account and gain access to more levels, more programming skills, and more fun!"
# preview_campaign_title: "Preview Campaign"
preview_campaign_title: "Приказ кампање"
# preview_campaign_description: "Take a sneak peek at all that CodeCombat has to offer before signing up for your FREE account."
# arena: "Arena"
# arena_soon_title: "Arena Coming Soon"
arena: "Арена"
arena_soon_title: "Арена стиже ускоро"
# arena_soon_description: "We are working on a multiplayer arena for classrooms at the end of"
# not_enrolled1: "Not enrolled"
# not_enrolled2: "Ask your teacher to enroll you in the next course."
# next_course: "Next Course"
# coming_soon1: "Coming soon"
not_enrolled1: "Ниси уписан"
not_enrolled2: "Питај свог учитеља да те упише на следећи курс."
next_course: "Следећи курс"
coming_soon1: "Ускоро"
# coming_soon2: "We are hard at work making more courses for you!"
# available_levels: "Available Levels"
# welcome_to_courses: "Adventurers, welcome to Courses!"
# ready_to_play: "Ready to play?"
# start_new_game: "Start New Game"
# play_now_learn_header: "Play now to learn"
# play_now_learn_1: "basic syntax to control your character"
# play_now_learn_2: "while loops to solve pesky puzzles"
# play_now_learn_3: "strings & variables to customize actions"
# play_now_learn_4: "how to defeat an ogre (important life skills!)"
# welcome_to_page: "Welcome to your Courses page!"
# completed_hoc: "Amazing! You've completed the Hour of Code course!"
# ready_for_more_header: "Ready for more? Play the campaign mode!"
# ready_for_more_1: "Use gems to unlock new items!"
# ready_for_more_2: "Play through brand new worlds and challenges"
# ready_for_more_3: "Learn even more programming!"
# saved_games: "Saved Games"
# hoc: "Hour of Code"
# my_classes: "My Classes"
# class_added: "Class successfully added!"
# view_class: "view class"
# view_levels: "view levels"
# join_class: "Join A Class"
# ask_teacher_for_code: "Ask your teacher if you have a CodeCombat class code! If so, enter it below:"
# enter_c_code: "<Enter Class Code>"
# join: "Join"
# joining: "Joining class"
# course_complete: "Course Complete"
# play_arena: "Play Arena"
# start: "Start"
# last_level: "Last Level"
# welcome_to_hoc: "Adventurers, welcome to our Hour of Code!"
# logged_in_as: "Logged in as:"
# not_you: "Not you?"
# welcome_back: "Hi adventurer, welcome back!"
# continue_playing: "Continue Playing"
# more_options: "More options:"
# option1_header: "Option 1: Invite students via email"
available_levels: "Доступни нивои"
welcome_to_courses: "Авантуристи, добродошли у курсеве!"
ready_to_play: "Спреман да играш?"
start_new_game: "Почни нову игру"
play_now_learn_header: "Играј сада да научиш"
play_now_learn_1: "основну синтаксу да контролишеш свог лика"
play_now_learn_2: "while петље да решиш заморне слагалице"
play_now_learn_3: "стрингове & променљиве да подесиш акције"
play_now_learn_4: "како да победиш огра (важне животне вештине!)"
welcome_to_page: "Добродошао на твоју Курсеви страницу!"
completed_hoc: "Невероватно! Завршио си курс Сат Кодирања!"
ready_for_more_header: "Спреман за још? Играј кампања верзију!"
ready_for_more_1: "Користи драгуље да откључаш нове предмете!"
ready_for_more_2: "Играј кроз потпуно нове светове и изазове"
ready_for_more_3: "Научи још више програмирања!"
saved_games: "Сачуване игре"
hoc: "Сат Кодирања"
my_classes: "Моји разреди"
class_added: "Разред успешно додат!"
view_class: "види разред"
view_levels: "види нивое"
join_class: "Придружи се разреду"
ask_teacher_for_code: "Питај свог учитеља да ли имаш CodeCombat код за разред! Ако да, унеси га испод:"
enter_c_code: "<Упиши код за разред>"
join: "Придружи се"
joining: "Придруживање разреду"
course_complete: "Курс завршен"
play_arena: "Играј Арену"
start: "Почни"
last_level: "Последњи ниво"
welcome_to_hoc: "Авантуристи, добродошли на наш Сат Кодирања!"
logged_in_as: "Уписан као:"
not_you: "Није ти?"
welcome_back: "Здраво авантуристо, добродошао назад!"
continue_playing: "Настави да играш"
more_options: "Још опција:"
option1_header: "Опција 1: Позови ученике преко мејла"
# option1_body: "Students will automatically be sent an invitation to join this class, and will need to create an account with a username and password."
# option2_header: "Option 2: Send URL to your students"
# option2_body: "Students will be asked to enter an email address, username and password to create an account."
@ -1202,96 +1202,96 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian
# total_all_classes: "Total Across All Classes"
# how_many_enrollments: "How many additional paid enrollments do you need?"
# each_student_access: "Each student in a class will get access to Courses 2-4 once they are enrolled in paid courses. You may assign each course to each student individually."
# purchase_now: "Purchase Now"
purchase_now: "Купи сад"
# enrollments: "enrollments"
# remove_student1: "Remove Student"
remove_student1: "Уклони ученика"
# are_you_sure: "Are you sure you want to remove this student from this class?"
# remove_description1: "Student will lose access to this classroom and assigned classes. Progress and gameplay is NOT lost, and the student can be added back to the classroom at any time."
# remove_description2: "The activated paid license will not be returned."
# keep_student: "Keep Student"
# removing_user: "Removing user"
keep_student: "Задржи ученика"
removing_user: "Уклањање корисника"
# to_join_ask: "To join a class, ask your teacher for an unlock code."
# join_this_class: "Join Class"
join_this_class: "Придружи се разреду"
# enter_here: "<enter unlock code here>"
# successfully_joined: "Successfully joined"
# click_to_start: "Click here to start taking"
# my_courses: "My Courses"
# classroom: "Classroom"
my_courses: "Моји курсеви"
classroom: "Учионица"
# use_school_email: "use your school email if you have one"
# unique_name: "a unique name no one has chosen"
# pick_something: "pick something you can remember"
# class_code: "Class Code"
class_code: "Код разреда"
# optional_ask: "optional - ask your teacher to give you one!"
# optional_school: "optional - what school do you go to?"
# start_playing: "Start Playing"
# skip_this: "Skip this, I'll create an account later!"
# welcome: "Welcome"
start_playing: "Почни да играш"
skip_this: "Прескочи ово, направићу налог касније!"
welcome: "Добродошао"
# getting_started: "Getting Started with Courses"
# download_getting_started: "Download Getting Started Guide [PDF]"
# getting_started_1: "Create a new class by clicking the green 'Create New Class' button below."
# getting_started_2: "Once you've created a class, click the blue 'Add Students' button."
# getting_started_3: "You'll see student's progress below as they sign up and join your class."
# additional_resources: "Additional Resources"
# additional_resources_1_pref: "Download/print our"
# additional_resources_1_mid: "Course 1"
# additional_resources_1_mid2: "and"
# additional_resources_1_mid3: "Course 2"
additional_resources: "Додатни ресурси"
additional_resources_1_pref: "Преузми/одштампај наш"
additional_resources_1_mid: "Курс 1"
additional_resources_1_mid2: "и"
additional_resources_1_mid3: "Курс 2"
# additional_resources_1_suff: "teacher's guides with solutions for each level."
# additional_resources_2_pref: "Complete our"
# additional_resources_2_suff: "to get two free enrollments for the rest of our paid courses."
# additional_resources_3_pref: "Visit our"
# additional_resources_3_mid: "Teacher Forums"
additional_resources_3_pref: "Посети наше"
additional_resources_3_mid: "форуме за учитеље"
# additional_resources_3_suff: "to connect to fellow educators who are using CodeCombat."
# additional_resources_4_pref: "Check out our"
# additional_resources_4_mid: "Schools Page"
# additional_resources_4_suff: "to learn more about CodeCombat's classroom offerings."
# educator_wiki_pref: "Or check out our new"
# educator_wiki_mid: "educator wiki"
educator_wiki_mid: "едукатор wiki"
# educator_wiki_suff: "to browse the guide online."
# your_classes: "Your Classes"
# no_classes: "No classes yet!"
# create_new_class1: "create new class"
# available_courses: "Available Courses"
# unused_enrollments: "Unused enrollments available:"
your_classes: "Твоји разреди"
no_classes: "Још увек нема разреда!"
create_new_class1: "направи нови разред"
available_courses: "Доступни курсеви"
unused_enrollments: "Доступни неискоришћени уписи:"
# students_access: "All students get access to Introduction to Computer Science for free. One enrollment per student is required to assign them to paid CodeCombat courses. A single student does not need multiple enrollments to access all paid courses."
# active_courses: "active courses"
# no_students: "No students yet!"
# add_students1: "add students"
# view_edit: "view/edit"
# students_enrolled: "students enrolled"
# students_assigned: "students assigned"
# length: "Length:"
# title: "Courses" # Flat style redesign
no_students: "Још увек нема ученика!"
add_students1: "додај ученике"
view_edit: "види/измени"
students_enrolled: "ученика уписано"
students_assigned: "ученика додељено"
length: "Дужина:"
title: "Курсеви" # Flat style redesign
# subtitle: "Review course guidelines, solutions, and levels"
# changelog: "View latest changes to course levels."
# select_language: "Select language"
# select_level: "Select level"
# play_level: "Play Level"
# concepts_covered: "Concepts covered"
# print_guide: "Print Guide (PDF)"
# view_guide_online: "View Guide Online (PDF)"
select_language: "Изабери језик"
select_level: "Изабери ниво"
play_level: "Играј ниво"
# concepts_covered: "Покривени концепти"
print_guide: "Одштампај водич (PDF)"
view_guide_online: "Види водич онлајн (PDF)"
# last_updated: "Last updated:"
# grants_lifetime_access: "Grants access to all Courses."
# enrollment_credits_available: "Enrollment Credits Available:"
# description: "Description" # ClassroomSettingsModal
# language_select: "Select a language"
language_select: "Изабери језик"
# language_cannot_change: "Language cannot be changed once students join a class."
# learn_p: "Learn Python"
# learn_j: "Learn JavaScript"
learn_p: "Научи Python"
learn_j: "Научи JavaScript"
# avg_student_exp_label: "Average Student Programming Experience"
# avg_student_exp_desc: "This will help us understand how to pace courses better."
# avg_student_exp_select: "Select the best option"
avg_student_exp_select: "Изабери најбољу опцију"
# avg_student_exp_none: "No Experience - little to no experience"
# avg_student_exp_beginner: "Beginner - some exposure or block-based"
# avg_student_exp_intermediate: "Intermediate - some experience with typed code"
# avg_student_exp_advanced: "Advanced - extensive experience with typed code"
# avg_student_exp_varied: "Varied Levels of Experience"
# student_age_range_label: "Student Age Range"
# student_age_range_younger: "Younger than 6"
# student_age_range_older: "Older than 18"
# student_age_range_to: "to"
# create_class: "Create Class"
# class_name: "Class Name"
student_age_range_label: "Опсег старости ученика"
student_age_range_younger: "Млађи од 6"
student_age_range_older: "Старији од 18"
student_age_range_to: "до"
create_class: "Направи разред"
class_name: "Име разреда"
# teacher_account_restricted: "Your account is a teacher account, and so cannot access student content."
# teacher:

View file

@ -124,10 +124,11 @@ class CocoModel extends Backbone.Model
validate: ->
errors = @getValidationErrors()
if errors?.length
console.debug "Validation failed for #{@constructor.className}: '#{@get('name') or @}'."
for error in errors
console.debug "\t", error.dataPath, ':', error.message
console.trace?()
unless application.testing
console.debug "Validation failed for #{@constructor.className}: '#{@get('name') or @}'."
for error in errors
console.debug "\t", error.dataPath, ':', error.message
console.trace?()
return errors
save: (attrs, options) ->
@ -188,7 +189,6 @@ class CocoModel extends Backbone.Model
keys.push key
return unless keys.length
console.debug 'Patching', @get('name') or @, keys
@save(attrs, options)
fetch: (options) ->

View file

@ -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', 'game-dev']
if o.thangs and @get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'hero-practice']
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
@ -147,7 +147,7 @@ module.exports = class Level extends CocoModel
# Load the user's chosen hero AFTER getting stats from default char
if /Hero Placeholder/.test(levelThang.id) and @get('type', true) in ['course']
heroThangType = me.get('heroConfig')?.thangType
heroThangType = me.get('heroConfig')?.thangType or ThangType.heroes.captain
levelThang.thangType = heroThangType if heroThangType
sortSystems: (levelSystems, systemModels) ->

View file

@ -41,7 +41,7 @@ module.exports = class LevelSession extends CocoModel
@get('submittedCodeLanguage')? and @get('team')?
completed: ->
@get('state')?.complete || false
@get('state')?.complete || @get('submitted') || false
shouldAvoidCorruptData: (attrs) ->
return false unless me.team is 'humans'

View file

@ -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', 'game-dev']}
type: {'enum': ['ladder', 'ladder-tutorial', 'hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'hero-practice']}
slug: { type: 'string', format: 'hidden' }
original: { type: 'string', format: 'hidden' }
adventurer: { type: 'boolean' }

View file

@ -313,7 +313,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', 'game-dev'])
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', 'hero-practice'])
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'}
@ -324,6 +324,7 @@ _.extend LevelSchema.properties,
url: c.url {title: 'URL', description: 'Link to the video on Vimeo.'}
replayable: {type: 'boolean', title: 'Replayable', description: 'Whether this (hero) level infinitely scales up its difficulty and can be beaten over and over for greater rewards.'}
buildTime: {type: 'number', description: 'How long it has taken to build this level.'}
practiceThresholdMinutes: {type: 'number', description: 'Players with larger playtimes may be directed to a practice level.'}
# Admin flags
adventurer: { type: 'boolean' }

View file

@ -19,3 +19,6 @@
.test-failed
color: red
.lineUnder
border-bottom: 1px solid #ccc

View file

@ -10,7 +10,6 @@ block content
a(href="/account", data-i18n="nav.account")
li.active(data-i18n="account.payments")
- console.log('render', view.payments.size())
if view.payments.size()
table.table.table-striped
tr

View file

@ -44,6 +44,8 @@ block content
ul
li
a(href="/admin/analytics") Dashboard
li
a(href="/admin/school-counts") School Counts
li
a(href="/admin/analytics/subscriptions") Subscriptions
li

View file

@ -0,0 +1,49 @@
extends /templates/base
//- DO NOT TRANSLATE
block content
if !me.isAdmin()
div You must be logged in as an admin to view this page.
else
p CodeCombat is now in #{view.totalSchools} schools with #{view.totalStudents} students [and #{view.totalTeachers} teachers] [in #{view.totalStates} states]
p Students not attached to NCES data: #{view.untriagedStudents}
.small Teacher: owns a classroom or has a teacher role
.small Student: member of a classroom or has schoolName set
.small States, Districts, Schools are from NCES
h2 State Counts
if view.stateCounts
table.table.table-striped.table-condensed
tr
th State
th Districts
th Schools
th Teachers
th Students
each stateCount in view.stateCounts
tr
td= stateCount.state
td= stateCount.districts
td= stateCount.schools
td= stateCount.teachers
td= stateCount.students
h2 District Counts by State
if view.districtCounts
table.table.table-striped.table-condensed
tr
th State
th District
th Schools
th Teachers
th Students
each districtCount in view.districtCounts
tr
td= districtCount.state
td= districtCount.district
td= districtCount.schools
td= districtCount.teachers
td= districtCount.students

View file

@ -101,9 +101,6 @@ mixin addCredits
button#request-sent-btn.btn-lg.btn.btn-forest(disabled=true, data-i18n="teacher.request_sent")
else
p(data-i18n="teacher.num_enrollments_needed")
div.m-t-2
input#students-input.enrollment-count.text-center(value=view.state.get('numberOfStudents') type='number')
strong(data-i18n="teacher.credits")
p.m-y-2(data-i18n="teacher.get_enrollments_blurb")
button#contact-us-btn.btn-lg.btn.btn-forest(data-i18n="contribute.contact_us_url")

View file

@ -300,7 +300,7 @@ mixin courseProgressTab
mixin courseOverview
- var course = state.get('selectedCourse')
- var levels = view.classroom.getLevels({courseID: course.id, withoutLadderLevels: true}).models
- var levels = view.classroom.getLevels({courseID: course.id}).models
.course-overview-row
.course-title.student-name
span= course.get('name')
@ -318,7 +318,7 @@ mixin studentLevelsRow(student)
div.student-email.small-details= student.get('email')
div.student-levels-progress
- var course = state.get('selectedCourse')
- var levels = view.classroom.getLevels({courseID: course.id, withoutLadderLevels: true}).models
- var levels = view.classroom.getLevels({courseID: course.id}).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, session)

View file

@ -16,9 +16,18 @@ block content
p.alert.alert-info
| To Run: #{view.testCount - view.passed - view.problem - view.failed}
.form.form-inline
.row.lineUnder
.form-group.campaign-mix
input(id="careAboutFrames", type="checkbox", checked=!!view.careAboutFrames, disabled=!!view.tests)
label(for="careAboutFrames") Check frame counts
.form-group.campaign-mix
label(for="cores") Threads:
input(id="cores", type="number", min="1", max="16", value=view.cores, disabled=!!view.tests)
if view.levelsByCampaign
.form.form-inline
.row
.row.lineUnder
each campaignInfo, campaign in view.levelsByCampaign
.form-group.campaign-mix
- var campaignID = "campaign-" + campaign + "-checkbox";

View file

@ -160,11 +160,11 @@ block content
p
span CodeCombat Inc.
br
span 360 3rd St Suite 700 (Livefyre)
span 301 Howard St Suite 830
br
span San Francisco, CA 94107
span San Francisco, CA 94105
br
a(href='mailto:team@codecombat.com') team@codecombat.com
p
em Last Edited on 2016-02-01
em Last Edited on 2016-06-21

View file

@ -1,5 +1,7 @@
extends /templates/core/modal-base-flat
//- TODO: i18n
block modal-header-content
.text-center
h3 Contact Our Classroom Team
@ -11,26 +13,36 @@ block modal-body-content
- var sent = view.state.get('sendingState') === 'sent';
- var values = view.state.get('formValues');
- var errors = view.state.get('formErrors');
.form-group(class=errors.name ? 'has-error' : '')
label.control-label(for="name" data-i18n="general.name")
+formErrors(errors.name)
input.form-control(name="name", type="text", value=values.name || '', tabindex=1, disabled=sending || sent)
.form-group(class=errors.email ? 'has-error' : '')
label.control-label(for="email" data-i18n="general.email")
+formErrors(errors.email)
input.form-control(name="email", type="email", value=values.email || '', tabindex=1, disabled=sending || sent)
.form-group(class=errors.licensesNeeded ? 'has-error' : '')
label.control-label(for="licensesNeeded" data-i18n="teachers.licenses_needed")
+formErrors(errors.licensesNeeded)
input.form-control(name="licensesNeeded", type="text", value=values.licensesNeeded || '', tabindex=1, disabled=sending || sent)
.form-group(class=errors.message ? 'has-error' : '')
label.control-label(for="message" data-i18n="general.message")
+formErrors(errors.message)
textarea.form-control(name="message", tabindex=1 disabled=sending || sent)= values.message
if view.state.get('sendingState') === 'error'
.alert.alert-danger Could not send message.
if sent
.alert.alert-success Message sent!
.text-right
button#submit-btn.btn.btn-navy.btn-lg(type='submit' disabled=sending || sent) Submit
block modal-footer
mixin formErrors(errors)

View file

@ -122,9 +122,9 @@ module.exports = class NewHomeView extends RootView
onChangeSchoolLevelDropdown: (e) ->
levels =
elementary: {'introduction-to-computer-science': '2-4', 'computer-science-5': '15-20', default: '10-15', total: '50-70 hours (about one year)'}
middle: {'introduction-to-computer-science': '1-3', 'computer-science-5': '7-10', default: '5-8', total: '25-35 hours (about one semester)'}
high: {'introduction-to-computer-science': '1', 'computer-science-5': '6-9', default: '5-6', total: '22-28 hours (about one semester)'}
elementary: {'introduction-to-computer-science': '2-4', 'computer-science-6': '24-30', 'computer-science-7': '30-40', 'computer-science-8': '30-40', default: '16-25', total: '150-215 hours (about two and a half years)'}
middle: {'introduction-to-computer-science': '1-3', 'computer-science-6': '12-14', 'computer-science-7': '14-16', 'computer-science-8': '14-16', default: '8-12', total: '75-100 hours (about one and a half years)'}
high: {'introduction-to-computer-science': '1', 'computer-science-6': '10-12', 'computer-science-7': '12-16', 'computer-science-8': '12-16', default: '8-10', total: '65-85 hours (about one year)'}
level = if e then $(e.target).val() else 'middle'
@$el.find('#courses-row .course-details').each ->
slug = $(@).data('course-slug')

View file

@ -0,0 +1,144 @@
RootView = require 'views/core/RootView'
CocoCollection = require 'collections/CocoCollection'
Classroom = require 'models/Classroom'
TrialRequest = require 'models/TrialRequest'
User = require 'models/User'
# TODO: trim orphaned students: course instances != Single Player, hourOfCode != true
# TODO: match anonymous trial requests with real users via email
module.exports = class SchoolCountsView extends RootView
id: 'admin-school-counts-view'
template: require 'templates/admin/school-counts'
initialize: ->
return super() unless me.isAdmin()
@classrooms = new CocoCollection([], { url: "/db/classroom/-/users", model: Classroom })
@supermodel.loadCollection(@classrooms, 'classrooms', {cache: false})
@students = new CocoCollection([], { url: "/db/user/-/students", model: User })
@supermodel.loadCollection(@students, 'students', {cache: false})
@teachers = new CocoCollection([], { url: "/db/user/-/teachers", model: User })
@supermodel.loadCollection(@teachers, 'teachers', {cache: false})
@trialRequests = new CocoCollection([], { url: "/db/trial.request/-/users", model: TrialRequest })
@supermodel.loadCollection(@trialRequests, 'trial-requests', {cache: false})
super()
onLoaded: ->
return super() unless me.isAdmin()
console.log(new Date().toISOString(), 'onLoaded')
teacherMap = {} # Used to make sure teachers and students only counted once
studentMap = {} # Used to make sure teachers and students only counted once
teacherStudentMap = {} # Used to link students to their teacher locations
orphanedSchoolStudentMap = {} # Used to link student schoolName to teacher Nces data
countryStateDistrictSchoolCountsMap = {} # Data graph
console.log(new Date().toISOString(), 'Processing classrooms...')
for classroom in @classrooms.models
teacherID = classroom.get('ownerID')
teacherMap[teacherID] ?= {}
teacherMap[teacherID] = true
teacherStudentMap[teacherID] ?= {}
for studentID in classroom.get('members')
studentMap[studentID] = true
teacherStudentMap[teacherID][studentID] = true
console.log(new Date().toISOString(), 'Processing teachers...')
for teacher in @teachers.models
teacherMap[teacher.id] ?= {}
delete studentMap[teacher.id]
console.log(new Date().toISOString(), 'Processing students...')
for student in @students.models when not teacherMap[student.id]
schoolName = student.get('schoolName')
studentMap[student.id] = true
orphanedSchoolStudentMap[schoolName] ?= {}
orphanedSchoolStudentMap[schoolName][student.id] = true
console.log(new Date().toISOString(), 'Processing trial requests...')
# TODO: this step is crazy slow
orphanSchoolsMatched = 0
orphanStudentsMatched = 0
for trialRequest in @trialRequests.models
teacherID = trialRequest.get('applicant')
unless teacherMap[teacherID]
# console.log("Skipping non-teacher #{teacherID} trial request #{trialRequest.id}")
continue
props = trialRequest.get('properties')
if props.nces_id and props.country and props.state
country = props.country
state = props.state
district = props.nces_district
school = props.nces_name
countryStateDistrictSchoolCountsMap[country] ?= {}
countryStateDistrictSchoolCountsMap[country][state] ?= {}
countryStateDistrictSchoolCountsMap[country][state][district] ?= {}
countryStateDistrictSchoolCountsMap[country][state][district][school] ?= {students: {}, teachers: {}}
countryStateDistrictSchoolCountsMap[country][state][district][school].teachers[teacherID] = true
for studentID, val of teacherStudentMap[teacherID]
countryStateDistrictSchoolCountsMap[country][state][district][school].students[studentID] = true
for orphanSchool, students of orphanedSchoolStudentMap
if school is orphanSchool or school.replace(/unified|elementary|high|district|#\d+|isd|unified district|school district/ig, '').trim() is orphanSchool.trim()
orphanSchoolsMatched++
for studentID, val of students
orphanStudentsMatched++
countryStateDistrictSchoolCountsMap[country][state][district][school].students[studentID] = true
delete orphanedSchoolStudentMap[school]
console.log(new Date().toISOString(), "#{orphanSchoolsMatched} orphanSchoolsMatched #{orphanStudentsMatched} orphanStudentsMatched")
console.log(new Date().toISOString(), 'Building graph...')
@totalSchools = 0
@totalStudents = 0
@totalTeachers = 0
@totalStates = 0
@stateCounts = []
stateCountsMap = {}
@districtCounts = []
for country, stateDistrictSchoolCountsMap of countryStateDistrictSchoolCountsMap
continue unless /usa/ig.test(country)
for state, districtSchoolCountsMap of stateDistrictSchoolCountsMap
@totalStates++
stateData = {state: state, districts: 0, schools: 0, students: 0, teachers: 0}
for district, schoolCountsMap of districtSchoolCountsMap
stateData.districts++
districtData = {state: state, district: district, schools: 0, students: 0, teachers: 0}
for school, counts of schoolCountsMap
studentCount = Object.keys(counts.students).length
teacherCount = Object.keys(counts.teachers).length
@totalSchools++
@totalStudents += studentCount
@totalTeachers += teacherCount
stateData.schools++
stateData.students += studentCount
stateData.teachers += teacherCount
districtData.schools++
districtData.students += studentCount
districtData.teachers += teacherCount
@districtCounts.push(districtData)
@stateCounts.push(stateData)
stateCountsMap[state] = stateData
@untriagedStudents = Object.keys(studentMap).length - @totalStudents
@stateCounts.sort (a, b) ->
return -1 if a.students > b.students
return 1 if a.students < b.students
return -1 if a.teachers > b.teachers
return 1 if a.teachers < b.teachers
return -1 if a.districts > b.districts
return 1 if a.districts < b.districts
b.state.localeCompare(a.state)
@districtCounts.sort (a, b) ->
if a.state isnt b.state
return -1 if stateCountsMap[a.state].students > stateCountsMap[b.state].students
return 1 if stateCountsMap[a.state].students < stateCountsMap[b.state].students
return -1 if stateCountsMap[a.state].teachers > stateCountsMap[b.state].teachers
return 1 if stateCountsMap[a.state].teachers < stateCountsMap[b.state].teachers
a.state.localeCompare(b.state)
else
return -1 if a.students > b.students
return 1 if a.students < b.students
return -1 if a.teachers > b.teachers
return 1 if a.teachers < b.teachers
a.district.localeCompare(b.district)
super()

View file

@ -14,7 +14,6 @@ module.exports = class EnrollmentsView extends RootView
template: template
events:
'input #students-input': 'onInputStudentsInput'
'click #enroll-students-btn': 'onClickEnrollStudentsButton'
'click #how-to-enroll-link': 'onClickHowToEnrollLink'
'click #contact-us-btn': 'onClickContactUsButton'
@ -96,17 +95,8 @@ module.exports = class EnrollmentsView extends RootView
@openModalView(new HowToEnrollModal())
onClickContactUsButton: ->
window.tracker?.trackEvent 'Classes Licenses Contact Us', category: 'Teachers', enrollmentsNeeded: @state.get('numberOfStudents'), ['Mixpanel']
@openModalView(new TeachersContactModal({ enrollmentsNeeded: @state.get('numberOfStudents') }))
onInputStudentsInput: ->
input = @$('#students-input').val()
if input isnt "" and (parseFloat(input) isnt parseInt(input) or _.isNaN parseInt(input))
@$('#students-input').val(@state.get('numberOfStudents'))
else
@state.set({'numberOfStudents': Math.max(parseInt(@$('#students-input').val()) or 0, 0)}, {silent: true}) # do not re-render
numberOfStudentsIsValid: -> 0 < @get('numberOfStudents') < 100000
window.tracker?.trackEvent 'Classes Licenses Contact Us', category: 'Teachers', ['Mixpanel']
@openModalView(new TeachersContactModal())
onClickEnrollStudentsButton: ->
window.tracker?.trackEvent 'Classes Licenses Enroll Students', category: 'Teachers', ['Mixpanel']

View file

@ -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', 'course', 'course-ladder', 'game-dev']
if @level?.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'hero-practice']
schema.required = []
treemaOptions =
supermodel: @supermodel

View file

@ -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', 'game-dev'] then options.thangType = thangType
if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'hero-practice'] then options.thangType = thangType
@thangComponentEditView = new ThangComponentsEditView options
@listenTo @thangComponentEditView, 'components-changed', @onComponentsChanged

View file

@ -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', 'game-dev']) or @getThangByID(thangID)
return if not (@level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'hero-practice']) 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', 'game-dev']
else if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'hero-practice']
components = [] # Load them all from default ThangType Components
else
components = _.cloneDeep thangType.get('components') ? []

View file

@ -7,7 +7,7 @@ LevelLoader = require 'lib/LevelLoader'
utils = require 'core/utils'
module.exports = class VerifierTest extends CocoClass
constructor: (@levelID, @updateCallback, @supermodel, @language) ->
constructor: (@levelID, @updateCallback, @supermodel, @language, @options) ->
super()
# TODO: turn this into a Subview
# TODO: listen to the progress report from Angel to show a simulation progress bar (maybe even out of the number of frames we actually know it'll take)
@ -91,7 +91,7 @@ module.exports = class VerifierTest extends CocoClass
isSuccessful: () ->
return false unless @solution?
return false unless @frames == @solution.frameCount
return false unless @frames == @solution.frameCount or @options.dontCareAboutFrames
if @goals and @solution.goals
for k of @goals
continue if not @solution.goals[k]

View file

@ -23,6 +23,10 @@ module.exports = class VerifierView extends RootView
@problem = 0
@testCount = 0
defaultCores = 2
@cores = Math.max(window.navigator.hardwareConcurrency, defaultCores)
@careAboutFrames = true
if @levelID
@levelIDs = [@levelID]
@testLanguages = ['python', 'javascript', 'java', 'lua', 'coffeescript']
@ -56,6 +60,8 @@ module.exports = class VerifierView extends RootView
onClickGoButton: (e) ->
@filterCampaigns()
@levelIDs = []
@careAboutFrames = @$("#careAboutFrames").is(':checked')
@cores = @$("#cores").val()|0
for campaign, campaignInfo of @levelsByCampaign
if @$("#campaign-#{campaign}-checkbox").is(':checked')
for level in campaignInfo.levels
@ -87,8 +93,6 @@ module.exports = class VerifierView extends RootView
@render()
onTestLevelsLoaded: ->
defaultCores = 2
cores = Math.max(window.navigator.hardwareConcurrency, defaultCores)
@linksQueryString = window.location.search
#supermodel = if @levelID then @supermodel else undefined
@ -102,7 +106,8 @@ module.exports = class VerifierView extends RootView
@tasksList.push level: levelID, language: codeLanguage
@testCount = @tasksList.length
chunks = _.groupBy @tasksList, (v,i) -> i%cores
console.log("Starting in", @cores, "cores...")
chunks = _.groupBy @tasksList, (v,i) => i%@cores
supermodels = [@supermodel]
_.forEach chunks, (chunk, i) =>
@ -128,7 +133,7 @@ module.exports = class VerifierView extends RootView
++@problem
next()
, chunkSupermodel, task.language
, chunkSupermodel, task.language, {dontCareAboutFrames: not @careAboutFrames}
@tests.unshift test
@render()
, => @render()

View file

@ -205,7 +205,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', 'game-dev']
@$el.addClass 'hero' if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'hero-practice']
@$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
@ -467,7 +467,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 in ['course', 'game-dev']
if levelType in ['course', 'game-dev', 'hero-practice']
return false
else if levelType is 'hero' and gamesSimulated
return false if stillBuggy
@ -540,7 +540,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', 'game-dev']
$('#level-done-button').show() unless @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'hero-practice']
@showVictory() if e.showModal
return if @victorySeen
@victorySeen = true
@ -558,7 +558,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', 'game-dev'] then HeroVictoryModal else VictoryModal
ModalClass = if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'hero-practice'] then HeroVictoryModal else VictoryModal
ModalClass = CourseVictoryModal if @isCourseMode() or me.isSessionless()
ModalClass = PicoCTFVictoryModal if window.serverConfig.picoCTF
victoryModal = new ModalClass(options)

View file

@ -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', 'game-dev']
if @level.get('type', true) in ['hero', 'hero-ladder', 'course', 'course-ladder', 'game-dev', 'hero-practice']
achievements = new CocoCollection([], {
url: "/db/achievement?related=#{@session.get('level').original}"
model: Achievement
@ -155,7 +155,7 @@ module.exports = class HeroVictoryModal extends ModalView
c = super()
c.levelName = utils.i18n @level.attributes, 'name'
# TODO: support 'game-dev'
if @level.get('type', true) not in ['hero', 'game-dev']
if @level.get('type', true) not in ['hero', 'game-dev', 'hero-practice']
c.victoryText = utils.i18n @level.get('victory') ? {}, 'body'
earnedAchievementMap = _.indexBy(@newEarnedAchievements or [], (ea) -> ea.get('achievement'))
for achievement in (@achievements?.models or [])
@ -223,7 +223,7 @@ module.exports = class HeroVictoryModal extends ModalView
afterRender: ->
super()
@$el.toggleClass 'with-achievements', @level.get('type', true) in ['hero', 'hero-ladder', 'game-dev'] # TODO: support game-dev
@$el.toggleClass 'with-achievements', @level.get('type', true) in ['hero', 'hero-ladder', 'game-dev', 'hero-practice'] # TODO: support game-dev
return unless @supermodel.finished()
@playSelectionSound hero, true for original, hero of @thangTypes # Preload them
@updateSavingProgressStatus()
@ -233,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', 'game-dev'] # TODO: support game-dev
return @endSequentialAnimations() unless @level.get('type', true) in ['hero', 'hero-ladder', 'game-dev', 'hero-practice'] # TODO: support game-dev
@updateXPBars 0
#playVictorySound = => @playSound 'victory-title-appear' # TODO: actually add this
@$el.find('#victory-header').delay(250).queue(->
@ -264,7 +264,7 @@ module.exports = class HeroVictoryModal extends ModalView
beginSequentialAnimations: ->
return if @destroyed
return unless @level.get('type', true) in ['hero', 'hero-ladder', 'game-dev'] # TODO: support game-dev
return unless @level.get('type', true) in ['hero', 'hero-ladder', 'game-dev', 'hero-practice'] # TODO: support game-dev
@sequentialAnimatedPanels = _.map(@animatedPanels.find('.reward-panel'), (panel) -> {
number: $(panel).data('number')
previousNumber: $(panel).data('previous-number')

View file

@ -171,7 +171,7 @@ module.exports = class Spell
writable = @permissions.readwrite.length > 0 and not @isAISource
skipProtectAPI = @skipProtectAPI or not writable or @levelType in ['game-dev']
problemContext = @createProblemContext thang
includeFlow = (@levelType in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']) and not skipProtectAPI
includeFlow = (@levelType in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'hero-practice']) and not skipProtectAPI
aetherOptions = createAetherOptions
functionName: @name
codeLanguage: @language

View file

@ -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', 'game-dev']
if true or @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'hero-practice']
# 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')

View file

@ -157,7 +157,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', 'game-dev']) or not @options.programmable
if not (@options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'hero-practice']) or not @options.programmable
@organizePalette propStorage, allDocs, excludedDocs
else
@organizePaletteHero propStorage, allDocs, excludedDocs
@ -199,7 +199,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', 'game-dev'] 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', 'hero-practice'] then 'play_level.tome_your_skills' else 'play_level.tome_available_spells'
defaultGroup = $.i18n.t i18nKey
@entryGroups = {}
@entryGroups[defaultGroup] = @entries

View file

@ -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', 'game-dev'] # 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', 'hero-practice'] # 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()

View file

@ -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', 'game-dev']
unless @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'hero-practice']
@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)
@ -194,7 +194,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', 'game-dev'] # 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', 'hero-practice'] # Never deselect the hero in the Tome.
thang = e.thang
spellName = e.spellName
@spellList?.$el.hide()

View file

@ -7,20 +7,22 @@ contact = require 'core/contact'
module.exports = class TeachersContactModal extends ModalView
id: 'teachers-contact-modal'
template: require 'templates/teachers/teachers-contact-modal'
defaultLicenses: 15
events:
'submit form': 'onSubmitForm'
initialize: (options={}) ->
@state = new State({
formValues: {
name: ''
email: ''
licensesNeeded: @defaultLicenses
message: ''
}
formErrors: {}
sendingState: 'standby' # 'sending', 'sent', 'error'
})
@enrollmentsNeeded = options.enrollmentsNeeded or '-'
@trialRequests = new TrialRequests()
@supermodel.trackRequest @trialRequests.fetchOwn()
@state.on 'change', @render, @
@ -28,41 +30,46 @@ module.exports = class TeachersContactModal extends ModalView
onLoaded: ->
trialRequest = @trialRequests.first()
props = trialRequest?.get('properties') or {}
message = """
Name of School/District: #{props.organization or ''}
Your Name: #{props.name || ''}
Enrollments Needed: #{@enrollmentsNeeded}
Message: Hi CodeCombat! I want to learn more about the Classroom experience and get licenses so that my students can access Computer Science 2 and on.
"""
name = if props.firstName and props.lastName then "#{props.firstName} #{props.lastName}" else me.get('name') ? ''
email = props.email or me.get('email') or ''
@state.set('formValues', { email, message })
message = """
Hi CodeCombat! I want to learn more about the Classroom experience and get licenses so that my students can access Computer Science 2 and on.
Name of School/District: #{props.organization or ''}
Role: #{props.role or ''}
Phone Number: #{props.phoneNumber or ''}
"""
@state.set('formValues', { name, email, licensesNeeded: @defaultLicenses, message })
super()
onSubmitForm: (e) ->
e.preventDefault()
return if @state.get('sendingState') is 'sending'
formValues = forms.formToObject @$el
@state.set('formValues', formValues)
formErrors = {}
if not forms.validateEmail(formValues.email)
unless formValues.name
formErrors.name = 'Name required.'
unless forms.validateEmail(formValues.email)
formErrors.email = 'Invalid email.'
if not formValues.message
unless parseInt(formValues.licensesNeeded) > 0
formErrors.licensesNeeded = 'Licenses needed is required.'
unless formValues.message
formErrors.message = 'Message required.'
@state.set({ formErrors, formValues, sendingState: 'standby' })
return unless _.isEmpty(formErrors)
@state.set('sendingState', 'sending')
data = _.extend({ country: me.get('country'), recipientID: 'schools@codecombat.com', enrollmentsNeeded: @enrollmentsNeeded }, formValues)
data = _.extend({ country: me.get('country'), recipientID: 'schools@codecombat.com' }, formValues)
contact.send({
data
context: @
success: ->
@state.set({ sendingState: 'sent' })
me.set('enrollmentRequestSent', true)
setTimeout(=>
setTimeout(=>
@hide?()
, 3000)
error: -> @state.set({ sendingState: 'error' })

View file

@ -0,0 +1,194 @@
// Copy ZenProspect contacts with email replies into Close.io leads
'use strict';
if (process.argv.length !== 4) {
console.log("Usage: node <script> <Close.io general API key> <ZenProspect auth token>");
process.exit();
}
const closeIoApiKey = process.argv[2];
const zpAuthToken = process.argv[3];
const scriptStartTime = new Date();
const async = require('async');
const request = require('request');
const zpPageSize = 100;
getZPRepliedContacts((err, emailContactMap) => {
if (err) {
console.log(err);
return;
}
const tasks = [];
for (const email in emailContactMap) {
const contact = emailContactMap[email];
// if (contact.organization !== 'Cabarrus County Schools') continue;
tasks.push(createUpsertCloseLeadFn(contact));
}
async.parallel(tasks, (err, results) => {
if (err) console.log(err);
log("Script runtime: " + (new Date() - scriptStartTime));
});
});
function createCloseLead(zpContact, done) {
const postData = {
name: zpContact.organization,
status: 'Contacted',
contacts: [
{
name: zpContact.name,
title: zpContact.title,
emails: [{email: zpContact.email}]
}
],
custom: {
lastUpdated: new Date(),
'Lead Origin': 'outbound campaign'
}
};
if (zpContact.phone) {
postData.contacts[0].phones = [{phone: zpContact.phone}];
}
const options = {
uri: `https://${closeIoApiKey}:X@app.close.io/api/v1/lead/`,
body: JSON.stringify(postData)
};
request.post(options, (error, response, body) => {
if (error) return done(error);
const newLead = JSON.parse(body);
if (newLead.errors || newLead['field-errors']) {
console.error(`New lead POST error for ${zpContact.name} ${zpContact.organization}`);
return done(newLead.errors || newLead['field-errors']);
}
return done();
});
}
function updateCloseLead(zpContact, existingLead, done) {
const putData = {
status: 'Contacted',
'custom.lastUpdated': new Date(),
'custom.Lead Origin': 'outbound campaign'
};
const options = {
uri: `https://${closeIoApiKey}:X@app.close.io/api/v1/lead/${existingLead.id}/`,
body: JSON.stringify(putData)
};
request.put(options, (error, response, body) => {
if (error) return done(error);
const result = JSON.parse(body);
if (result.errors || result['field-errors']) {
return done(`Update existing lead PUT error for ${existingLead.id} ${zpContact.email} ${result.errors || result['field-errors']}`);
}
const postData = {
lead_id: existingLead.id,
name: zpContact.name,
title: zpContact.title,
emails: [{email: zpContact.email}]
};
const options = {
uri: `https://${closeIoApiKey}:X@app.close.io/api/v1/contact/`,
body: JSON.stringify(postData)
};
request.post(options, (error, response, body) => {
if (error) return done(error);
const result = JSON.parse(body);
if (result.errors || result['field-errors']) {
return done(`New Contact POST error for ${existingLead.id} ${zpContact.email} ${result.errors || result['field-errors']}`);
}
return done();
});
});
}
function createUpsertCloseLeadFn(zpContact) {
return (done) => {
// console.log(`DEBUG: createUpsertCloseLeadFn ${zpContact.organization} ${zpContact.email}`);
const query = `email:${zpContact.email}`;
const url = `https://${closeIoApiKey}:X@app.close.io/api/v1/lead/?query=${encodeURIComponent(query)}`;
request.get(url, (error, response, body) => {
if (error) return done(error);
const data = JSON.parse(body);
if (data.total_results != 0) return done();
const query = `name:${zpContact.organization}`;
const url = `https://${closeIoApiKey}:X@app.close.io/api/v1/lead/?query=${encodeURIComponent(query)}`;
request.get(url, (error, response, body) => {
if (error) return done(error);
const data = JSON.parse(body);
if (data.total_results === 0) {
console.log(`DEBUG: Creating lead for ${zpContact.organization} ${zpContact.email}`);
return createCloseLead(zpContact, done);
}
else {
const existingLead = data.data[0];
console.log(`DEBUG: Adding ${zpContact.organization} ${zpContact.email} to ${existingLead.id}`);
return updateCloseLead(zpContact, existingLead, done);
}
});
});
};
}
function getZPRepliedContactsPage(contacts, page, done) {
// console.log(`DEBUG: Fetching page ${page} ${zpPageSize}...`);
const options = {
url: `https://www.zenprospect.com/api/v1/contacts/search?codecombat_special_auth_token=${zpAuthToken}&page=${page}&per_page=${zpPageSize}`,
headers: {
'Accept': 'application/json'
}
};
request.get(options, (err, response, body) => {
if (err) return done(err);
const data = JSON.parse(body);
for (let contact of data.contacts) {
if (contact.email_replied) {
contacts.push({
organization: contact.organization_name,
name: contact.name,
title: contact.title,
email: contact.email,
phone: contact.phone,
data: contact
});
}
}
return done(null, data.pipeline_total);
});
}
function getZPRepliedContacts(done) {
// Get first page to get total contact count for parallized page fetches
const contacts = [];
getZPRepliedContactsPage(contacts, 0, (err, total) => {
if (err) return done(err);
const createGetZPLeadsPage = (leads, page) => {
return (done) => {
getZPRepliedContactsPage(leads, page, done);
};
}
const tasks = [];
for (let i = 1; (i - 1) * zpPageSize < total; i++) {
tasks.push(createGetZPLeadsPage(contacts, i));
}
async.parallel(tasks, (err, results) => {
if (err) return done(err);
const emailContactMap = {};
for (const contact of contacts) {
if (!contact.organization || !contact.name || !contact.title || !contact.email) {
console.log(JSON.stringify(contact, null, 2));
return done(`DEBUG: missing data for zp contact:`);
}
if (!emailContactMap[contact.email]) emailContactMap[contact.email] = contact;
}
log(`${total} total ZP contacts, ${Object.keys(emailContactMap).length} with replies`);
return done(null, emailContactMap);
});
});
}
function log(str) {
console.log(new Date().toISOString() + " " + str);
}

View file

@ -27,7 +27,7 @@ const customFieldsToRemove = [
];
// Skip these problematic leads
const leadsToSkip = ['6 sınıflar', 'fdsafd', 'ashtasht', 'matt+20160404teacher3 school', 'sdfdsf', 'ddddd', 'dsfadsaf', "Nolan's School of Wonders"];
const leadsToSkip = ['6 sınıflar', 'fdsafd', 'ashtasht', 'matt+20160404teacher3 school', 'sdfdsf', 'ddddd', 'dsfadsaf', "Nolan's School of Wonders", 'asdfsadf'];
const createTeacherEmailTemplatesAuto1 = ['tmpl_i5bQ2dOlMdZTvZil21bhTx44JYoojPbFkciJ0F560mn', 'tmpl_CEZ9PuE1y4PRvlYiKB5kRbZAQcTIucxDvSeqvtQW57G'];
const demoRequestEmailTemplatesAuto1 = ['tmpl_s7BZiydyCHOMMeXAcqRZzqn0fOtk0yOFlXSZ412MSGm', 'tmpl_cGb6m4ssDvqjvYd8UaG6cacvtSXkZY3vj9b9lSmdQrf'];
@ -624,7 +624,7 @@ class CocoLead {
// ** Upsert Close.io methods
function updateExistingLead(lead, existingLead, done) {
function updateExistingLead(lead, existingLead, userApiKeyMap, done) {
// console.log('DEBUG: updateExistingLead', existingLead.id);
const putData = lead.getLeadPutData(existingLead);
const options = {
@ -646,7 +646,7 @@ function updateExistingLead(lead, existingLead, done) {
const tasks = []
for (const newContact of newContacts) {
newContact.lead_id = existingLead.id;
tasks.push(createAddContactFn(newContact, lead, existingLead));
tasks.push(createAddContactFn(newContact, lead, existingLead, userApiKeyMap));
}
async.parallel(tasks, (err, results) => {
if (err) return done(err);
@ -737,7 +737,7 @@ function createFindExistingLeadFn(email, name, existingLeads) {
};
}
function createUpdateLeadFn(lead, existingLeads) {
function createUpdateLeadFn(lead, existingLeads, userApiKeyMap) {
return (done) => {
// console.log('DEBUG: updateLead', lead.name);
const query = `name:"${lead.name}"`;
@ -750,7 +750,7 @@ function createUpdateLeadFn(lead, existingLeads) {
if (existingLeads[lead.name.toLowerCase()]) {
if (existingLeads[lead.name.toLowerCase()].length === 1) {
// console.log(`DEBUG: Using lead from email lookup: ${lead.name}`);
return updateExistingLead(lead, existingLeads[lead.name.toLowerCase()][0], done);
return updateExistingLead(lead, existingLeads[lead.name.toLowerCase()][0], userApiKeyMap, done);
}
console.error(`ERROR: ${existingLeads[lead.name.toLowerCase()].length} email leads found for ${lead.name}`);
return done();
@ -761,7 +761,7 @@ function createUpdateLeadFn(lead, existingLeads) {
console.error(`ERROR: ${data.total_results} leads found for ${lead.name}`);
return done();
}
return updateExistingLead(lead, data.data[0], done);
return updateExistingLead(lead, data.data[0], userApiKeyMap, done);
} catch (error) {
// console.log(url);
console.log(`ERROR: updateLead ${error}`);
@ -772,9 +772,11 @@ function createUpdateLeadFn(lead, existingLeads) {
};
}
function createAddContactFn(postData, internalLead, externalLead) {
function createAddContactFn(postData, internalLead, closeIoLead, userApiKeyMap) {
return (done) => {
// console.log('DEBUG: addContact', postData.lead_id);
// Create new contact
const options = {
uri: `https://${closeIoApiKey}:X@app.close.io/api/v1/contact/`,
body: JSON.stringify(postData)
@ -788,11 +790,20 @@ function createAddContactFn(postData, internalLead, externalLead) {
return done();
}
// Send emails to new contact
const email = postData.emails[0].email;
const countryCode = getCountryCode(internalLead.contacts[email].trial.properties.country, [email]);
const emailTemplate = getEmailTemplate(internalLead.contacts[email].trial.properties.siteOrigin, externalLead.status_label);
sendMail(email, externalLead.id, newContact.id, emailTemplate, getEmailApiKey(externalLead.status_label), emailDelayMinutes, done);
// Find previous internal user for new contact correspondence
const url = `https://${closeIoApiKey}:X@app.close.io/api/v1/activity/email/?lead_id=${closeIoLead.id}`;
request.get(url, (error, response, body) => {
if (error) return done(error);
const data = JSON.parse(body);
let emailApiKey = data.data && data.data.length > 0 ? userApiKeyMap[data.data[0].user_id] : getEmailApiKey(closeIoLead.status_label);
if (!emailApiKey) emailApiKey = getEmailApiKey(closeIoLead.status_label);
// Send email to new contact
const email = postData.emails[0].email;
const countryCode = getCountryCode(internalLead.contacts[email].trial.properties.country, [email]);
const emailTemplate = getEmailTemplate(internalLead.contacts[email].trial.properties.siteOrigin, closeIoLead.status_label);
sendMail(email, closeIoLead.id, newContact.id, emailTemplate, emailApiKey, emailDelayMinutes, done);
});
});
};
}
@ -883,25 +894,44 @@ function sendMail(toEmail, leadId, contactId, template, emailApiKey, delayMinute
}
function updateLeads(leads, done) {
// Lookup existing leads via email to protect against direct lead name querying later
// Querying via lead name is unreliable
const existingLeads = {};
const tasks = [];
for (const name in leads) {
if (leadsToSkip.indexOf(name) >= 0) continue;
for (const email in leads[name].contacts) {
tasks.push(createFindExistingLeadFn(email.toLowerCase(), name.toLowerCase(), existingLeads));
}
const userApiKeyMap = {};
let createGetUserFn = (apiKey) => {
return (done) => {
const url = `https://${apiKey}:X@app.close.io/api/v1/me/`;
request.get(url, (error, response, body) => {
if (error) return done();
const results = JSON.parse(body);
userApiKeyMap[results.id] = apiKey;
return done();
});
};
}
async.series(tasks, (err, results) => {
if (err) return done(err);
const tasks = [];
for (const closeIoMailApiKey of closeIoMailApiKeys) {
tasks.push(createGetUserFn(closeIoMailApiKey.apiKey));
}
async.parallel(tasks, (err, results) => {
if (err) console.log(err);
// Lookup existing leads via email to protect against direct lead name querying later
// Querying via lead name is unreliable
const existingLeads = {};
const tasks = [];
for (const name in leads) {
if (leadsToSkip.indexOf(name) >= 0) continue;
tasks.push(createUpdateLeadFn(leads[name], existingLeads));
for (const email in leads[name].contacts) {
tasks.push(createFindExistingLeadFn(email.toLowerCase(), name.toLowerCase(), existingLeads));
}
}
async.series(tasks, (err, results) => {
return done(err);
if (err) return done(err);
const tasks = [];
for (const name in leads) {
if (leadsToSkip.indexOf(name) >= 0) continue;
tasks.push(createUpdateLeadFn(leads[name], existingLeads, userApiKeyMap));
}
async.series(tasks, (err, results) => {
return done(err);
});
});
});
}

View file

@ -2,7 +2,8 @@ winston = require 'winston'
module.exports.setup = ->
winston.remove(winston.transports.Console)
winston.add(winston.transports.Console,
colorize: true,
timestamp: true
)
if not global.testing
winston.add(winston.transports.Console,
colorize: true,
timestamp: true
)

View file

@ -17,6 +17,8 @@ module.exports.addDelightedUser = addDelightedUser = (user, trialRequest) ->
testGroupNumber: user.get('testGroupNumber')
gender: user.get('gender')
lastLevel: user.get('lastLevel')
state: if props.nces_id and props.country is 'USA' then props.state else 'other'
@postPeople(form)
module.exports.postPeople = (form) ->

View file

@ -158,7 +158,7 @@ class EarnedAchievementHandler extends Handler
onFinished = ->
t1 = new Date().getTime()
runningTime = ((t1-t0)/1000/60/60).toFixed(2)
console.log "we finished in #{runningTime} hours"
log.info "we finished in #{runningTime} hours"
callback arguments...
filter = {}
@ -278,7 +278,7 @@ class EarnedAchievementHandler extends Handler
#log.debug "Incrementing score for these achievements with #{newTotalPoints - previousPoints}"
pointDelta = newTotalPoints - previousPoints
pctDone = (100 * usersFinished / total).toFixed(2)
console.log "Updated points to #{newTotalPoints} (#{if pointDelta < 0 then '' else '+'}#{pointDelta}) for #{user.get('name') or '???'} (#{user.get('_id')}) (#{pctDone}%)"
log.info "Updated points to #{newTotalPoints} (#{if pointDelta < 0 then '' else '+'}#{pointDelta}) for #{user.get('name') or '???'} (#{user.get('_id')}) (#{pctDone}%)"
if recalculatingAll
update = {$set: {points: newTotalPoints, 'earned.gems': 0, 'earned.heroes': [], 'earned.items': [], 'earned.levels': []}}
else

View file

@ -34,7 +34,7 @@ PaymentHandler = class PaymentHandler extends Handler
super arguments...
logPaymentError: (req, msg) ->
console.warn "Payment Error: #{req.user.get('slug')} (#{req.user._id}): '#{msg}'"
log.warn "Payment Error: #{req.user.get('slug')} (#{req.user._id}): '#{msg}'"
makeNewInstance: (req) ->
payment = super(req)
@ -377,6 +377,9 @@ PaymentHandler = class PaymentHandler extends Handler
#- Incrementing/recalculating gems
incrementGemsFor: (user, gems, done) ->
if not gems
return done()
purchased = _.clone(user.get('purchased'))
if not purchased?.gems
purchased ?= {}

View file

@ -21,7 +21,7 @@ recipientCouponID = 'free'
class SubscriptionHandler extends Handler
logSubscriptionError: (user, msg) ->
console.warn "Subscription Error: #{user.get('slug')} (#{user._id}): '#{msg}'"
log.warn "Subscription Error: #{user.get('slug')} (#{user._id}): '#{msg}'"
getByRelationship: (req, res, args...) ->
return @getStripeEvents(req, res) if args[1] is 'stripe_events'
@ -176,7 +176,7 @@ class SubscriptionHandler extends Handler
purchased = _.clone(req.user.get('purchased'))
purchased ?= {}
purchased.gems ?= 0
purchased.gems += parseInt(charge.metadata.gems)
purchased.gems += parseInt(charge.metadata.gems) if charge.metadata.gems
req.user.set('purchased', purchased)
req.user.save (err, user) =>
@ -257,7 +257,7 @@ class SubscriptionHandler extends Handler
purchased = _.clone(req.user.get('purchased'))
purchased ?= {}
purchased.gems ?= 0
purchased.gems += product.get('gems') * months
purchased.gems += product.get('gems') * months if product.get('gems')
req.user.set('purchased', purchased)
req.user.save (err, user) =>
@ -440,7 +440,7 @@ class SubscriptionHandler extends Handler
purchased = _.clone(user.get('purchased'))
purchased ?= {}
purchased.gems ?= 0
purchased.gems += product.get('gems')
purchased.gems += product.get('gems') if product.get('gems')
user.set('purchased', purchased)
user.save (err) =>
@ -550,7 +550,7 @@ class SubscriptionHandler extends Handler
purchased = _.clone(recipient.get('purchased'))
purchased ?= {}
purchased.gems ?= 0
purchased.gems += product.get('gems')
purchased.gems += product.get('gems') if product.get('gems')
recipient.set('purchased', purchased)
recipient.save (err) =>
if err

View file

@ -119,7 +119,7 @@ UserHandler = class UserHandler extends Handler
log.error "Database error setting user name: #{err}" if err
return callback(res: 'Database error.', code: 500) if err
r = {message: 'is already used by another account', property: 'name'}
console.log 'Another user exists' if otherUser
log.info 'Another user exists' if otherUser
return callback({res: r, code: 409}) if otherUser
user.set('name', req.body.name)
callback(null, req, user)
@ -775,7 +775,7 @@ UserHandler = class UserHandler extends Handler
else
update = $unset: {}
update.$unset[statKey] = ''
console.log "... updating #{userStringID} patches #{statKey} to #{count}, #{usersTotal} players found so far." if count
log.info "... updating #{userStringID} patches #{statKey} to #{count}, #{usersTotal} players found so far." if count
User.findByIdAndUpdate user.get('_id'), update, (err) ->
log.error err if err?
doneWithUser()
@ -801,7 +801,7 @@ UserHandler = class UserHandler extends Handler
update = {}
update[method] = {}
update[method][statName] = count or ''
console.log "... updating #{user.get('_id')} patches #{JSON.stringify(query)} #{statName} to #{count}, #{usersTotal} players found so far." if count
log.info "... updating #{user.get('_id')} patches #{JSON.stringify(query)} #{statName} to #{count}, #{usersTotal} players found so far." if count
User.findByIdAndUpdate user.get('_id'), update, doneUpdatingUser
userStream = User.find({anonymous: false}).sort('_id').stream()
@ -865,7 +865,7 @@ UserHandler = class UserHandler extends Handler
update = {}
update[method] = {}
update[method][statName] = count or ''
console.log "... updating #{userStringID} patches #{query} to #{count}, #{usersTotal} players found so far." if count
log.info "... updating #{userStringID} patches #{query} to #{count}, #{usersTotal} players found so far." if count
User.findByIdAndUpdate user.get('_id'), update, doneWithUser
statRecalculators:
@ -883,7 +883,7 @@ UserHandler = class UserHandler extends Handler
--numberRunning
userStream.resume()
if streamFinished and usersFinished is usersTotal
console.log "----------- Finished recalculating statistics for gamesCompleted for #{usersFinished} players. -----------"
log.info "----------- Finished recalculating statistics for gamesCompleted for #{usersFinished} players. -----------"
done?()
userStream.on 'error', (err) -> log.error err
userStream.on 'close', -> streamFinished = true
@ -895,7 +895,7 @@ UserHandler = class UserHandler extends Handler
LevelSession.count {creator: userID, 'state.complete': true}, (err, count) ->
update = if count then {$set: 'stats.gamesCompleted': count} else {$unset: 'stats.gamesCompleted': ''}
console.log "... updating #{userID} gamesCompleted to #{count}, #{usersTotal} players found so far." if Math.random() < 0.001
log.info "... updating #{userID} gamesCompleted to #{count}, #{usersTotal} players found so far." if Math.random() < 0.001
User.findByIdAndUpdate user.get('_id'), update, doneWithUser
articleEdits: (done) ->

View file

@ -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().replace(/ /g, '') }).select('name ownerID aceConfig')
classroom = yield Classroom.findOne({ code: code.toLowerCase().replace(RegExp(' ', '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.')
@ -104,7 +104,7 @@ module.exports =
members = classroom.get('members') or []
members = members.slice(memberSkip, memberSkip + memberLimit)
dbqs = []
select = 'state.complete level creator playtime changed dateFirstCompleted'
select = 'state.complete level creator playtime changed dateFirstCompleted submitted'
for member in members
dbqs.push(LevelSession.find({creator: member.toHexString()}).select(select).exec())
results = yield dbqs
@ -141,7 +141,7 @@ module.exports =
classroom.set 'members', []
database.assignBody(req, classroom)
# copy over data from how courses are right now
# Copy over data from how courses are right now
courses = yield Course.find()
campaigns = yield Campaign.find({_id: {$in: (course.get('campaignID') for course in courses)}})
campaignMap = {}
@ -151,6 +151,8 @@ module.exports =
courseData = { _id: course._id, levels: [] }
campaign = campaignMap[course.get('campaignID').toString()]
levels = _.values(campaign.get('levels'))
# TODO: remove hero-practice filter after classroom Ux supports practice levels
levels = _.reject(levels, {'type': 'hero-practice'})
levels = _.sortBy(levels, 'campaignIndex')
for level in levels
levelData = { original: mongoose.Types.ObjectId(level.original) }
@ -170,7 +172,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().replace(/ /g, '')
code = req.body.code.toLowerCase().replace(RegExp(' ', 'g'), '')
classroom = yield Classroom.findOne({code: code})
if not classroom
log.debug("classrooms.join: Classroom not found with code #{code}")
@ -246,3 +248,8 @@ module.exports =
sendwithus.api.send context, _.noop
res.status(200).send({})
getUsers: wrap (req, res, next) ->
throw new errors.Unauthorized('You must be an administrator.') unless req.user?.isAdmin()
classrooms = yield Classroom.find().select('ownerID members').lean()
res.status(200).send(classrooms)

View file

@ -39,7 +39,7 @@ module.exports =
throw new errors.NotFound('Level original ObjectId not found in that course')
if not nextLevelOriginal
res.status(200).send({})
return res.status(200).send({})
dbq = Level.findOne({original: mongoose.Types.ObjectId(nextLevelOriginal)})

View file

@ -49,3 +49,8 @@ module.exports =
trialRequests = yield TrialRequest.find({applicant: mongoose.Types.ObjectId(applicantID)})
trialRequests = (tr.toObject({req: req}) for tr in trialRequests)
res.status(200).send(trialRequests)
getUsers: wrap (req, res, next) ->
throw new errors.Unauthorized('You must be an administrator.') unless req.user?.isAdmin()
trialRequests = yield TrialRequest.find(status: {$ne: 'denied'}).select('applicant properties').lean()
res.status(200).send(trialRequests)

View file

@ -94,3 +94,14 @@ module.exports =
verify_link: "http://codecombat.com/user/#{user._id}/verify/#{user.verificationCode(timestamp)}"
sendwithus.api.send context, (err, result) ->
res.status(200).send({})
getStudents: wrap (req, res, next) ->
throw new errors.Unauthorized('You must be an administrator.') unless req.user?.isAdmin()
students = yield User.find({$and: [{schoolName: {$exists: true}}, {schoolName: {$ne: ''}}, {anonymous: false}]}).select('schoolName').lean()
res.status(200).send(students)
getTeachers: wrap (req, res, next) ->
throw new errors.Unauthorized('You must be an administrator.') unless req.user?.isAdmin()
teacherRoles = ['teacher', 'technology coordinator', 'advisor', 'principal', 'superintendent', 'parent']
teachers = yield User.find(anonymous: false, role: {$in: teacherRoles}).select('').lean()
res.status(200).send(teachers)

View file

@ -28,6 +28,9 @@ AnalyticsLogEventSchema.statics.logEvent = (user, event, properties={}) ->
doc.save()
analyticsMongoose = mongoose.createConnection "mongodb://#{config.mongo.analytics_host}:#{config.mongo.analytics_port}/#{config.mongo.analytics_db}"
module.exports = AnalyticsLogEvent = analyticsMongoose.model('analytics.log.event', AnalyticsLogEventSchema, config.mongo.analytics_collection)
unless config.proxy
analyticsMongoose = mongoose.createConnection()
analyticsMongoose.open "mongodb://#{config.mongo.analytics_host}:#{config.mongo.analytics_port}/#{config.mongo.analytics_db}", (error) ->
console.log "Couldnt connect to analytics", error
module.exports = AnalyticsLogEvent = analyticsMongoose.model('analytics.log.event', AnalyticsLogEventSchema, config.mongo.analytics_collection)

View file

@ -59,7 +59,7 @@ EarnedAchievementSchema.statics.createForAchievement = (achievement, doc, origin
earned.achievedAmount = newAmount
#console.log 'earnedPoints is', (expFunction(newAmount) - expFunction(originalAmount)) * pointWorth, 'was', earned.earnedPoints, earned.previouslyAchievedAmount, 'got exp function for new amount', newAmount, expFunction(newAmount), 'for original amount', originalAmount, expFunction(originalAmount), 'with point worth', pointWorth
earnedPoints = earned.earnedPoints = (expFunction(newAmount) - expFunction(originalAmount)) * pointWorth
earnedGems = earned.earnedGems = (expFunction(newAmount) - expFunction(originalAmount)) * gemWorth
earnedGems = earned.earnedGems = (expFunction(newAmount) - expFunction(originalAmount)) * gemWorth ? 0
earned.previouslyAchievedAmount = originalAmount
EarnedAchievement.update {achievement: earned.achievement, user: earned.user}, earned, {upsert: true}, (err) ->
return log.error err if err?

View file

@ -9,6 +9,7 @@ AnalyticsUsersActive = require './AnalyticsUsersActive'
Classroom = require '../models/Classroom'
languages = require '../routes/languages'
_ = require 'lodash'
errors = require '../commons/errors'
config = require '../../server_config'
stripe = require('stripe')(config.stripe.secretKey)
@ -347,10 +348,11 @@ UserSchema.methods.saveActiveUser = (event, done=null) ->
done?()
UserSchema.pre('save', (next) ->
if _.isNaN(@get('purchased')?.gems)
return next(new errors.InternalServerError('Attempting to save NaN to user'))
Classroom = require './Classroom'
if @isTeacher() and not @wasTeacher
Classroom.update({members: @_id}, {$pull: {members: @_id}}, {multi: true}).exec (err, res) ->
console.log 'removed self from all classrooms as a member', err, res
if email = @get('email')
@set('emailLower', email.toLowerCase())
if name = @get('name')

View file

@ -47,6 +47,7 @@ updateUserProperty = (userID, userProperty, answer) ->
return log.error err if err
updateUserGems = (userID, gemDelta) ->
return unless gemDelta
update = $inc: {'earned.gems': gemDelta}
User.update {_id: mongoose.Types.ObjectId(userID)}, update, (err, result) ->
return log.error err if err

View file

@ -25,11 +25,11 @@ module.exports.setup = (app) ->
createMailContent = (req, fromAddress, done) ->
country = req.body.country
enrollmentsNeeded = req.body.enrollmentsNeeded
licensesNeeded = req.body.licensesNeeded
message = req.body.message
user = req.user
subject = switch
when enrollmentsNeeded then "#{enrollmentsNeeded} Licenses needed for #{fromAddress}"
when licensesNeeded then "#{licensesNeeded} Licenses needed for #{fromAddress}"
when req.body.subject then req.body.subject
else "Contact Us Form: #{fromAddress}"
level = if user?.get('points') > 0 then Math.floor(5 * Math.log((1 / 100) * (user.get('points') + 100))) + 1 else 0

View file

@ -14,7 +14,10 @@ module.exports.setup = (app) ->
app.get('/auth/unsubscribe', mw.auth.unsubscribe)
app.get('/auth/whoami', mw.auth.whoAmI)
app.all('/db/*', mw.auth.checkHasUser())
app.delete('/db/*', mw.auth.checkHasUser())
app.patch('/db/*', mw.auth.checkHasUser())
app.post('/db/*', mw.auth.checkHasUser())
app.put('/db/*', mw.auth.checkHasUser())
Achievement = require '../models/Achievement'
app.get('/db/achievement', mw.achievements.fetchByRelated, mw.rest.get(Achievement))
@ -66,6 +69,7 @@ module.exports.setup = (app) ->
app.post('/db/classroom/:classroomID/members/:memberID/reset-password', mw.classrooms.setStudentPassword)
app.post('/db/classroom/:anything/members', mw.auth.checkLoggedIn(), mw.classrooms.join)
app.get('/db/classroom/:handle', mw.auth.checkLoggedIn()) # TODO: Finish migrating route, adding now so 401 is returned
app.get('/db/classroom/-/users', mw.auth.checkHasPermission(['admin']), mw.classrooms.getUsers)
CodeLog = require ('../models/CodeLog')
app.post('/db/codelogs', mw.codelogs.post)
@ -88,8 +92,9 @@ module.exports.setup = (app) ->
app.put('/db/user/-/remain-teacher', mw.users.remainTeacher)
app.post('/db/user/:userID/request-verify-email', mw.users.sendVerificationEmail)
app.post('/db/user/:userID/verify/:verificationCode', mw.users.verifyEmailAddress) # TODO: Finalize URL scheme
app.get('/db/level/:handle/session', mw.levels.upsertSession)
app.get('/db/level/:handle/session', mw.auth.checkHasUser(), mw.levels.upsertSession)
app.get('/db/user/-/students', mw.auth.checkHasPermission(['admin']), mw.users.getStudents)
app.get('/db/user/-/teachers', mw.auth.checkHasPermission(['admin']), mw.users.getTeachers)
app.get('/db/prepaid', mw.auth.checkLoggedIn(), mw.prepaids.fetchByCreator)
app.post('/db/prepaid', mw.auth.checkHasPermission(['admin']), mw.prepaids.post)
@ -102,5 +107,6 @@ module.exports.setup = (app) ->
app.post('/db/trial.request', mw.trialRequests.post)
app.get('/db/trial.request/:handle', mw.auth.checkHasPermission(['admin']), mw.rest.getByHandle(TrialRequest))
app.put('/db/trial.request/:handle', mw.auth.checkHasPermission(['admin']), mw.trialRequests.put)
app.get('/db/trial.request/-/users', mw.auth.checkHasPermission(['admin']), mw.trialRequests.getUsers)
app.get('/healthcheck', mw.healthcheck)

View file

@ -101,7 +101,7 @@ module.exports.setup = (app) ->
# Update purchased gems
# TODO: is this correct for a resub?
Payment.find({recipient: recipient._id, gems: {$exists: true}}).select('gems').exec (err, payments) ->
gems = _.reduce payments, ((sum, p) -> sum + p.get('gems')), 0
gems = _.reduce payments, ((sum, p) -> sum + (p.get('gems') or 0)), 0
purchased = _.clone(recipient.get('purchased'))
purchased ?= {}
purchased.gems = gems

View file

@ -84,7 +84,7 @@ setupExpressMiddleware = (app) ->
app.use express.compress filter: (req, res) ->
return false if req.headers.host is 'codecombat.com' # CloudFlare will gzip it for us on codecombat.com
compressible res.getHeader('Content-Type')
else
else if not global.testing
express.logger.format('dev', developmentLogging)
app.use(express.logger('dev'))
app.use(express.static(path.join(__dirname, 'public'), maxAge: 0)) # CloudFlare overrides maxAge, and we don't want local development caching.

View file

@ -36,14 +36,25 @@ if (database.generateMongoConnectionString() !== dbString) {
throw Error('Stopping server tests because db connection string was not as expected.');
}
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 10; // for long Stripe tests
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 15; // for long Stripe tests
require('../server/common'); // Make sure global testing functions are set up
// Ignore Stripe/Nocking erroring
console.error = function() {
try {
if(arguments[1].stack.indexOf('An error occurred with our connection to Stripe') > -1)
return;
}
catch (e) { }
console.log.apply(console, arguments);
};
var initialized = false;
beforeEach(function(done) {
if (initialized) {
return done();
}
console.log('/spec/helpers/helper.js - Initializing spec environment...');
var async = require('async');
async.series([
@ -103,6 +114,7 @@ beforeEach(function(done) {
process.exit(1);
}
initialized = true;
console.log('/spec/helpers/helper.js - Done');
done();
});
});

View file

@ -1,7 +1,7 @@
# import this at the top of every file so we're not juggling connections
# and common libraries are available
console.log 'IT BEGINS'
console.log '/spec/server/common.coffee - Setting up spec globals...'
if process.env.COCO_MONGO_HOST
throw Error('Tests may not run with production environment')
@ -60,7 +60,7 @@ unittest.getUser = (name, email, password, done, force) ->
return done(unittest.users[email]) if unittest.users[email] and not force
request.post getURL('/auth/logout'), ->
request.get getURL('/auth/whoami'), ->
req = request.post(getURL('/db/user'), (err, response, body) ->
req = request.post({url: getURL('/db/user'), json: {email, password}}, (err, response, body) ->
throw err if err
User.findOne({email: email}).exec((err, user) ->
throw err if err
@ -70,9 +70,6 @@ unittest.getUser = (name, email, password, done, force) ->
wrapUpGetUser(email, user, done)
)
)
form = req.form()
form.append('email', email)
form.append('password', password)
wrapUpGetUser = (email, user, done) ->
unittest.users[email] = user
@ -139,58 +136,48 @@ GLOBAL.loginNewUser = (done) ->
email = "#{name}@me.com"
request.post getURL('/auth/logout'), ->
unittest.getUser name, email, password, (user) ->
req = request.post(getURL('/auth/login'), (error, response) ->
json = {username: email, password}
req = request.post({url: getURL('/auth/login'), json}, (error, response) ->
expect(response.statusCode).toBe(200)
done(user)
)
form = req.form()
form.append('username', email)
form.append('password', password)
, true
GLOBAL.loginJoe = (done) ->
request.post getURL('/auth/logout'), ->
unittest.getNormalJoe (user) ->
req = request.post(getURL('/auth/login'), (error, response) ->
json = {username: 'normal@jo.com', password: 'food'}
req = request.post({url: getURL('/auth/login'), json}, (error, response) ->
expect(response.statusCode).toBe(200)
done(user)
)
form = req.form()
form.append('username', 'normal@jo.com')
form.append('password', 'food')
GLOBAL.loginSam = (done) ->
request.post getURL('/auth/logout'), ->
unittest.getOtherSam (user) ->
req = request.post(getURL('/auth/login'), (error, response) ->
json = { username: 'other@sam.com', password: 'beer'}
req = request.post({url: getURL('/auth/login'), json}, (error, response) ->
expect(response.statusCode).toBe(200)
done(user)
)
form = req.form()
form.append('username', 'other@sam.com')
form.append('password', 'beer')
GLOBAL.loginAdmin = (done) ->
request.post getURL('/auth/logout'), ->
unittest.getAdmin (user) ->
req = request.post(getURL('/auth/login'), (error, response) ->
json = { username: 'admin@afc.com', password: '80yqxpb38j' }
req = request.post({url: getURL('/auth/login'), json}, (error, response) ->
expect(response.statusCode).toBe(200)
done(user)
)
form = req.form()
form.append('username', 'admin@afc.com')
form.append('password', '80yqxpb38j')
# find some other way to make the admin object an admin... maybe directly?
GLOBAL.loginUser = (user, done) ->
request.post getURL('/auth/logout'), ->
req = request.post(getURL('/auth/login'), (error, response) ->
json = { username: user.get('email'), password: user.get('name') }
req = request.post({ url: getURL('/auth/login'), json}, (error, response) ->
expect(response.statusCode).toBe(200)
done(user)
)
form = req.form()
form.append('username', user.get('email'))
form.append('password', user.get('name'))
GLOBAL.logoutUser = (done) ->
request.post getURL('/auth/logout'), ->
@ -213,3 +200,4 @@ _drop = (done) ->
GLOBAL.resetUserIDCounter = (number=0) ->
User.idCounter = number
console.log '/spec/server/common.coffee - Done'

View file

@ -403,10 +403,10 @@ describe 'Clans', ->
loginNewUser (user2) ->
user2.set 'stripe.free', true
user2.save (err) ->
request.put {uri: "#{clanURL}/#{clan.id}/join" }, (err, res, body) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(200)
done()
request.put {uri: "#{clanURL}/#{clan.id}/join" }, (err, res, body) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(200)
done()
it 'Join clan when not premium 403', (done) ->
loginNewUser (user1) ->

View file

@ -86,7 +86,14 @@ describe 'POST /db/classroom', ->
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSONB})
expect(res.statusCode).toBe(200)
@levelB = yield Level.findById(res.body._id)
levelJSONC = { name: 'Level C', permissions: [{access: 'owner', target: admin.id}], type: 'hero-practice' }
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSONC})
expect(res.statusCode).toBe(200)
@levelC = yield Level.findById(res.body._id)
campaignJSON = { name: 'Campaign', levels: {} }
paredLevelC = _.pick(@levelC.toObject(), 'name', 'original', 'type', 'slug')
paredLevelC.campaignIndex = 2
campaignJSON.levels[@levelC.get('original').toString()] = paredLevelC
paredLevelB = _.pick(@levelB.toObject(), 'name', 'original', 'type', 'slug')
paredLevelB.campaignIndex = 1
campaignJSON.levels[@levelB.get('original').toString()] = paredLevelB
@ -124,7 +131,7 @@ describe 'POST /db/classroom', ->
[res, body] = yield request.postAsync {uri: classroomsURL, json: data }
expect(res.statusCode).toBe(403)
done()
it 'makes a copy of the list of all levels in all courses', utils.wrap (done) ->
teacher = yield utils.initUser({role: 'teacher'})
yield utils.loginUser(teacher)
@ -136,7 +143,17 @@ describe 'POST /db/classroom', ->
expect(classroom.get('courses')[0].levels[0].slug).toBe('level-a')
expect(classroom.get('courses')[0].levels[0].name).toBe('Level A')
done()
it 'makes a copy of the list of all non-practice levels in all courses', utils.wrap (done) ->
teacher = yield utils.initUser({role: 'teacher'})
yield utils.loginUser(teacher)
data = { name: 'tmp Classroom 2' }
[res, body] = yield request.postAsync {uri: classroomsURL, json: data }
classroom = yield Classroom.findById(res.body._id)
# console.log(JSON.stringify(classroom.get('courses')[0], null, 2));
expect(classroom.get('courses')[0].levels.length).toEqual(2)
done()
describe 'GET /db/classroom/:handle/levels', ->
beforeEach utils.wrap (done) ->

View file

@ -4,23 +4,23 @@ request = require '../request'
User = require '../../../server/models/User'
# TODO: need to update this test since /contact calls external Close.io API now
xdescribe 'POST /contact', ->
beforeEach utils.wrap (done) ->
spyOn(sendwithus.api, 'send')
@teacher = yield utils.initUser({role: 'teacher'})
yield utils.loginUser(@teacher)
done()
describe 'when recipientID is "schools@codecombat.com"', ->
it 'sends to that email', utils.wrap (done) ->
[res, body] = yield request.postAsync({url: getURL('/contact'), json: {
sender: 'some@email.com'
message: 'A message'
recipientID: 'schools@codecombat.com'
}})
expect(sendwithus.api.send).toHaveBeenCalled()
user = yield User.findById(@teacher.id)
yield new Promise((resolve) -> setTimeout(resolve, 10))
expect(user.get('enrollmentRequestSent')).toBe(true)
done()
#xdescribe 'POST /contact', ->
#
# beforeEach utils.wrap (done) ->
# spyOn(sendwithus.api, 'send')
# @teacher = yield utils.initUser({role: 'teacher'})
# yield utils.loginUser(@teacher)
# done()
#
# describe 'when recipientID is "schools@codecombat.com"', ->
# it 'sends to that email', utils.wrap (done) ->
# [res, body] = yield request.postAsync({url: getURL('/contact'), json: {
# sender: 'some@email.com'
# message: 'A message'
# recipientID: 'schools@codecombat.com'
# }})
# expect(sendwithus.api.send).toHaveBeenCalled()
# user = yield User.findById(@teacher.id)
# yield new Promise((resolve) -> setTimeout(resolve, 10))
# expect(user.get('enrollmentRequestSent')).toBe(true)
# done()

View file

@ -1,178 +1,178 @@
require '../common'
# Doesn't work on Travis. Need to figure out why, probably by having the
# url not depend on some external resource.
mongoose = require 'mongoose'
request = require '../request'
xdescribe '/file', ->
url = getURL('/file')
files = []
options = {
uri: url
json: {
# url: 'http://scotterickson.info/images/where-are-you.jpg'
url: 'http://fc07.deviantart.net/fs37/f/2008/283/5/1/Chu_Chu_Pikachu_by_angelishi.gif'
filename: 'where-are-you.jpg'
mimetype: 'image/jpeg'
description: 'None!'
}
}
filepath = 'tmp/file' # TODO Warning hard coded path !!!
jsonOptions= {
path: 'my_path'
postName: 'my_buffer'
filename: 'ittybitty.data'
mimetype: 'application/octet-stream'
description: 'rando-info'
# my_buffer_url: 'http://scotterickson.info/images/where-are-you.jpg'
my_buffer_url: 'http://fc07.deviantart.net/fs37/f/2008/283/5/1/Chu_Chu_Pikachu_by_angelishi.gif'
}
allowHeader = 'GET, POST'
it 'preparing test : deletes all the files first', (done) ->
dropGridFS ->
done()
it 'can\'t be created if invalid (property path is required)', (done) ->
func = (err, res, body) ->
expect(res.statusCode).toBe(422)
done()
loginAdmin ->
request.post(options, func)
it 'can be created by an admin', (done) ->
func = (err, res, body) ->
expect(res.statusCode).toBe(200)
expect(body._id).toBeDefined()
expect(body.filename).toBe(options.json.filename)
expect(body.contentType).toBe(options.json.mimetype)
expect(body.length).toBeDefined()
expect(body.uploadDate).toBeDefined()
expect(body.metadata).toBeDefined()
expect(body.metadata.name).toBeDefined()
expect(body.metadata.path).toBe(options.json.path)
expect(body.metadata.creator).toBeDefined()
expect(body.metadata.description).toBe(options.json.description)
expect(body.md5).toBeDefined()
files.push(body)
done()
options.json.path = filepath
request.post(options, func)
it 'can be read by an admin.', (done) ->
request.get {uri: url+'/'+files[0]._id}, (err, res) ->
expect(res.statusCode).toBe(200)
expect(res.headers['content-type']).toBe(files[0].contentType)
done()
it 'returns 404 for missing files', (done) ->
id = '000000000000000000000000'
request.get {uri: url+'/'+id}, (err, res) ->
expect(res.statusCode).toBe(404)
done()
it 'returns 404 for invalid ids', (done) ->
request.get {uri: url+'/thiswillnotwork'}, (err, res) ->
expect(res.statusCode).toBe(404)
done()
it 'can be created directly with form parameters', (done) ->
options2 = {
uri: url
}
func = (err, res, body) ->
expect(res.statusCode).toBe(200)
body = JSON.parse(body)
expect(body._id).toBeDefined()
expect(body.filename).toBe(jsonOptions.filename)
expect(body.contentType).toBe(jsonOptions.mimetype)
expect(body.length).toBeDefined()
expect(body.uploadDate).toBeDefined()
expect(body.metadata).toBeDefined()
expect(body.metadata.name).toBeDefined()
expect(body.metadata.path).toBe(jsonOptions.path)
expect(body.metadata.creator).toBeDefined()
expect(body.metadata.description).toBe(jsonOptions.description)
expect(body.md5).toBeDefined()
files.push(body)
done()
# the only way I could figure out how to get request to do what I wanted...
r = request.post(options2, func)
form = r.form()
form.append('path', jsonOptions.path)
form.append('postName', jsonOptions.postName)
form.append('filename', jsonOptions.filename)
form.append('mimetype', jsonOptions.mimetype)
form.append('description', jsonOptions.description)
form.append('my_buffer', request(jsonOptions.my_buffer_url))
it 'created directly, can be read', (done) ->
request.get {uri: url+'/'+files[1]._id}, (err, res) ->
expect(res.statusCode).toBe(200)
expect(res.headers['content-type']).toBe(files[1].contentType)
done()
it 'does not overwrite existing files', (done) ->
options.json.description = 'Face'
func = (err, res, body) ->
expect(res.statusCode).toBe(409)
collection = mongoose.connection.db.collection('media.files')
collection.find({}).toArray (err, results) ->
# ittybitty.data, and just one Where are you.jpg
expect(results.length).toBe(2)
for f in results
expect(f.metadata.description).not.toBe('Face')
done()
request.post(options, func)
it 'does overwrite existing files if force is true', (done) ->
options.json.force = 'true' # TODO ask why it's a string and not a boolean ?
func = (err, res, body) ->
expect(res.statusCode).toBe(200)
collection = mongoose.connection.db.collection('media.files')
collection.find({}).toArray (err, results) ->
# ittybitty.data, and just one Where are you.jpg
expect(results.length).toBe(2)
hit = false
for f in results
hit = true if f.metadata.description is 'Face'
expect(hit).toBe(true)
done()
request.post(options, func)
it ' can\'t be requested with HTTP PATCH method', (done) ->
request {method: 'patch', uri: url}, (err, res) ->
expect(res.statusCode).toBe(405)
expect(res.headers.allow).toBe(allowHeader)
done()
it ' can\'t be requested with HTTP PUT method', (done) ->
request.put {uri: url}, (err, res) ->
expect(res.statusCode).toBe(405)
expect(res.headers.allow).toBe(allowHeader)
done()
it ' can\'t be requested with HTTP HEAD method', (done) ->
request.head {uri: url}, (err, res) ->
expect(res.statusCode).toBe(405)
expect(res.headers.allow).toBe(allowHeader)
done()
it ' can\'t be requested with HTTP DEL method', (done) ->
request.del {uri: url}, (err, res) ->
expect(res.statusCode).toBe(405)
expect(res.headers.allow).toBe(allowHeader)
done()
# TODO: test server errors, see what they do
#require '../common'
#
## Doesn't work on Travis. Need to figure out why, probably by having the
## url not depend on some external resource.
#mongoose = require 'mongoose'
#request = require '../request'
#
#xdescribe '/file', ->
# url = getURL('/file')
# files = []
# options = {
# uri: url
# json: {
# # url: 'http://scotterickson.info/images/where-are-you.jpg'
# url: 'http://fc07.deviantart.net/fs37/f/2008/283/5/1/Chu_Chu_Pikachu_by_angelishi.gif'
# filename: 'where-are-you.jpg'
# mimetype: 'image/jpeg'
# description: 'None!'
# }
# }
# filepath = 'tmp/file' # TODO Warning hard coded path !!!
#
# jsonOptions= {
# path: 'my_path'
# postName: 'my_buffer'
# filename: 'ittybitty.data'
# mimetype: 'application/octet-stream'
# description: 'rando-info'
# # my_buffer_url: 'http://scotterickson.info/images/where-are-you.jpg'
# my_buffer_url: 'http://fc07.deviantart.net/fs37/f/2008/283/5/1/Chu_Chu_Pikachu_by_angelishi.gif'
# }
#
# allowHeader = 'GET, POST'
#
# it 'preparing test : deletes all the files first', (done) ->
# dropGridFS ->
# done()
#
# it 'can\'t be created if invalid (property path is required)', (done) ->
# func = (err, res, body) ->
# expect(res.statusCode).toBe(422)
# done()
#
# loginAdmin ->
# request.post(options, func)
#
# it 'can be created by an admin', (done) ->
# func = (err, res, body) ->
# expect(res.statusCode).toBe(200)
# expect(body._id).toBeDefined()
# expect(body.filename).toBe(options.json.filename)
# expect(body.contentType).toBe(options.json.mimetype)
# expect(body.length).toBeDefined()
# expect(body.uploadDate).toBeDefined()
# expect(body.metadata).toBeDefined()
# expect(body.metadata.name).toBeDefined()
# expect(body.metadata.path).toBe(options.json.path)
# expect(body.metadata.creator).toBeDefined()
# expect(body.metadata.description).toBe(options.json.description)
# expect(body.md5).toBeDefined()
# files.push(body)
# done()
#
# options.json.path = filepath
# request.post(options, func)
#
# it 'can be read by an admin.', (done) ->
# request.get {uri: url+'/'+files[0]._id}, (err, res) ->
# expect(res.statusCode).toBe(200)
# expect(res.headers['content-type']).toBe(files[0].contentType)
# done()
#
# it 'returns 404 for missing files', (done) ->
# id = '000000000000000000000000'
# request.get {uri: url+'/'+id}, (err, res) ->
# expect(res.statusCode).toBe(404)
# done()
#
# it 'returns 404 for invalid ids', (done) ->
# request.get {uri: url+'/thiswillnotwork'}, (err, res) ->
# expect(res.statusCode).toBe(404)
# done()
#
# it 'can be created directly with form parameters', (done) ->
# options2 = {
# uri: url
# }
#
# func = (err, res, body) ->
# expect(res.statusCode).toBe(200)
# body = JSON.parse(body)
# expect(body._id).toBeDefined()
# expect(body.filename).toBe(jsonOptions.filename)
# expect(body.contentType).toBe(jsonOptions.mimetype)
# expect(body.length).toBeDefined()
# expect(body.uploadDate).toBeDefined()
# expect(body.metadata).toBeDefined()
# expect(body.metadata.name).toBeDefined()
# expect(body.metadata.path).toBe(jsonOptions.path)
# expect(body.metadata.creator).toBeDefined()
# expect(body.metadata.description).toBe(jsonOptions.description)
# expect(body.md5).toBeDefined()
# files.push(body)
# done()
#
# # the only way I could figure out how to get request to do what I wanted...
# r = request.post(options2, func)
# form = r.form()
# form.append('path', jsonOptions.path)
# form.append('postName', jsonOptions.postName)
# form.append('filename', jsonOptions.filename)
# form.append('mimetype', jsonOptions.mimetype)
# form.append('description', jsonOptions.description)
# form.append('my_buffer', request(jsonOptions.my_buffer_url))
#
# it 'created directly, can be read', (done) ->
# request.get {uri: url+'/'+files[1]._id}, (err, res) ->
# expect(res.statusCode).toBe(200)
# expect(res.headers['content-type']).toBe(files[1].contentType)
# done()
#
# it 'does not overwrite existing files', (done) ->
# options.json.description = 'Face'
#
# func = (err, res, body) ->
# expect(res.statusCode).toBe(409)
# collection = mongoose.connection.db.collection('media.files')
# collection.find({}).toArray (err, results) ->
# # ittybitty.data, and just one Where are you.jpg
# expect(results.length).toBe(2)
# for f in results
# expect(f.metadata.description).not.toBe('Face')
# done()
#
# request.post(options, func)
#
# it 'does overwrite existing files if force is true', (done) ->
# options.json.force = 'true' # TODO ask why it's a string and not a boolean ?
#
# func = (err, res, body) ->
# expect(res.statusCode).toBe(200)
# collection = mongoose.connection.db.collection('media.files')
# collection.find({}).toArray (err, results) ->
# # ittybitty.data, and just one Where are you.jpg
# expect(results.length).toBe(2)
# hit = false
# for f in results
# hit = true if f.metadata.description is 'Face'
# expect(hit).toBe(true)
# done()
#
# request.post(options, func)
#
# it ' can\'t be requested with HTTP PATCH method', (done) ->
# request {method: 'patch', uri: url}, (err, res) ->
# expect(res.statusCode).toBe(405)
# expect(res.headers.allow).toBe(allowHeader)
# done()
#
# it ' can\'t be requested with HTTP PUT method', (done) ->
# request.put {uri: url}, (err, res) ->
# expect(res.statusCode).toBe(405)
# expect(res.headers.allow).toBe(allowHeader)
# done()
#
# it ' can\'t be requested with HTTP HEAD method', (done) ->
# request.head {uri: url}, (err, res) ->
# expect(res.statusCode).toBe(405)
# expect(res.headers.allow).toBe(allowHeader)
# done()
#
# it ' can\'t be requested with HTTP DEL method', (done) ->
# request.del {uri: url}, (err, res) ->
# expect(res.statusCode).toBe(405)
# expect(res.headers.allow).toBe(allowHeader)
# done()
#
## TODO: test server errors, see what they do

View file

@ -23,14 +23,14 @@ describe '/db/level.session', ->
# TODO Tried to mimic what happens on the site. Why is this even so hard to do.
# Right now it's even possible to create ownerless sessions through POST
xit 'allows users to create level sessions through PATCH', (done) ->
loginJoe (joe) ->
request {method: 'patch', uri: url + mongoose.Types.ObjectId(), json: session}, (err, res, body) ->
expect(err).toBeNull()
expect(res.statusCode).toBe 200
console.log body
expect(body.creator).toEqual joe.get('_id').toHexString()
done()
# xit 'allows users to create level sessions through PATCH', (done) ->
# loginJoe (joe) ->
# request {method: 'patch', uri: url + mongoose.Types.ObjectId(), json: session}, (err, res, body) ->
# expect(err).toBeNull()
# expect(res.statusCode).toBe 200
# console.log body
# expect(body.creator).toEqual joe.get('_id').toHexString()
# done()
# Should remove this as soon as the PATCH test case above works
it 'create a level session', (done) ->

View file

@ -1,29 +0,0 @@
require '../common'
config = require '../../../server_config'
nockUtils = require('../nock-utils')
request = require '../request'
xdescribe 'nock-utils', ->
afterEach nockUtils.teardownNock
describe 'a test using setupNock', ->
it 'records and plays back third-party requests, but not localhost requests', (done) ->
nockUtils.setupNock 'nock-test.json', (err, nockDone) ->
request.get { uri: getURL('/db/level') }, (err) ->
expect(err).toBeNull()
t0 = new Date().getTime()
request.get { uri: 'http://zombo.com/' }, (err) ->
console.log 'cached speed', new Date().getTime() - t0
expect(err).toBeNull()
nockDone()
done()
describe 'another, sibling test that does not use setupNock', ->
it 'is proceeds normally', (done) ->
request.get { uri: getURL('/db/level') }, (err) ->
expect(err).toBeNull()
t0 = new Date().getTime()
request.get { uri: 'http://zombo.com/' }, (err) ->
console.log 'uncached speed', new Date().getTime() - t0
expect(err).toBeNull()
done()

View file

@ -544,7 +544,7 @@ describe '/db/prepaid', ->
logoutUser () ->
fetchPrepaid joeCode, (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(401)
expect(res.statusCode).toEqual(403)
done()
it 'User can fetch a prepaid code', (done) ->

View file

@ -1,24 +1,24 @@
require '../common'
request = require '../request'
describe 'queue', ->
someURL = getURL('/queue/')
allowHeader = 'GET, POST, PUT'
xit 'can\'t be requested with HTTP PATCH method', (done) ->
request {method: 'patch', uri: someURL}, (err, res, body) ->
expect(res.statusCode).toBe(405)
expect(res.headers.allow).toBe(allowHeader)
done()
xit 'can\'t be requested with HTTP HEAD method', (done) ->
request.head {uri: someURL}, (err, res, body) ->
expect(res.statusCode).toBe(405)
expect(res.headers.allow).toBe(allowHeader)
done()
xit 'can\'t be requested with HTTP DELETE method', (done) ->
request.del {uri: someURL}, (err, res, body) ->
expect(res.statusCode).toBe(405)
expect(res.headers.allow).toBe(allowHeader)
done()
#require '../common'
#request = require '../request'
#
#describe 'queue', ->
# someURL = getURL('/queue/')
# allowHeader = 'GET, POST, PUT'
#
# xit 'can\'t be requested with HTTP PATCH method', (done) ->
# request {method: 'patch', uri: someURL}, (err, res, body) ->
# expect(res.statusCode).toBe(405)
# expect(res.headers.allow).toBe(allowHeader)
# done()
#
# xit 'can\'t be requested with HTTP HEAD method', (done) ->
# request.head {uri: someURL}, (err, res, body) ->
# expect(res.statusCode).toBe(405)
# expect(res.headers.allow).toBe(allowHeader)
# done()
#
# xit 'can\'t be requested with HTTP DELETE method', (done) ->
# request.del {uri: someURL}, (err, res, body) ->
# expect(res.statusCode).toBe(405)
# expect(res.headers.allow).toBe(allowHeader)
# done()

View file

@ -1441,62 +1441,62 @@ describe 'Subscriptions', ->
nockDone()
done()
xit 'Unsubscribed user1 subscribes 13 users, unsubcribes 2', (done) ->
nockUtils.setupNock 'sub-test-34.json', (err, nockDone) ->
# TODO: Hits the Stripe error 'Request rate limit exceeded'.
# TODO: Need a better test for 12+ bulk discounts. Or, we could update the bulk disount logic.
# TODO: verify interim invoices?
recipientCount = 13
recipientsToVerify = [0, 1, 10, 11, 12]
recipients = new SubbedRecipients recipientCount, recipientsToVerify
# Create recipients
recipients.createRecipients ->
expect(recipients.length()).toEqual(recipientCount)
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
# Create sponsor user
loginNewUser (user1) ->
# Subscribe recipients
recipients.subRecipients user1, token, ->
User.findById user1.id, (err, user1) ->
# Unsubscribe first recipient
unsubscribeRecipient user1, recipients.get(0), ->
User.findById user1.id, (err, user1) ->
stripeInfo = user1.get('stripe')
expect(stripeInfo.recipients.length).toEqual(recipientCount - 1)
verifyNotSponsoring user1.id, recipients.get(0).id, ->
verifyNotRecipient recipients.get(0).id, ->
stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) ->
expect(err).toBeNull()
expect(subscription).not.toBeNull()
expect(subscription.quantity).toEqual(getUnsubscribedQuantity(recipientCount - 1))
# Unsubscribe last recipient
unsubscribeRecipient user1, recipients.get(recipientCount - 1), ->
User.findById user1.id, (err, user1) ->
stripeInfo = user1.get('stripe')
expect(stripeInfo.recipients.length).toEqual(recipientCount - 2)
verifyNotSponsoring user1.id, recipients.get(recipientCount - 1).id, ->
verifyNotRecipient recipients.get(recipientCount - 1).id, ->
stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) ->
expect(err).toBeNull()
expect(subscription).not.toBeNull()
numSponsored = recipientCount - 2
if numSponsored <= 1
expect(subscription.quantity).toEqual(subPrice)
else if numSponsored <= 11
expect(subscription.quantity).toEqual(subPrice + (numSponsored - 1) * subPrice * 0.8)
else
expect(subscription.quantity).toEqual(subPrice + 10 * subPrice * 0.8 + (numSponsored - 11) * subPrice * 0.6)
nockDone()
done()
# xit 'Unsubscribed user1 subscribes 13 users, unsubcribes 2', (done) ->
# nockUtils.setupNock 'sub-test-34.json', (err, nockDone) ->
# # TODO: Hits the Stripe error 'Request rate limit exceeded'.
# # TODO: Need a better test for 12+ bulk discounts. Or, we could update the bulk disount logic.
# # TODO: verify interim invoices?
# recipientCount = 13
# recipientsToVerify = [0, 1, 10, 11, 12]
# recipients = new SubbedRecipients recipientCount, recipientsToVerify
#
# # Create recipients
# recipients.createRecipients ->
# expect(recipients.length()).toEqual(recipientCount)
#
# stripe.tokens.create {
# card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
# }, (err, token) ->
#
# # Create sponsor user
# loginNewUser (user1) ->
#
# # Subscribe recipients
# recipients.subRecipients user1, token, ->
# User.findById user1.id, (err, user1) ->
#
# # Unsubscribe first recipient
# unsubscribeRecipient user1, recipients.get(0), ->
# User.findById user1.id, (err, user1) ->
#
# stripeInfo = user1.get('stripe')
# expect(stripeInfo.recipients.length).toEqual(recipientCount - 1)
# verifyNotSponsoring user1.id, recipients.get(0).id, ->
# verifyNotRecipient recipients.get(0).id, ->
# stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) ->
# expect(err).toBeNull()
# expect(subscription).not.toBeNull()
# expect(subscription.quantity).toEqual(getUnsubscribedQuantity(recipientCount - 1))
#
# # Unsubscribe last recipient
# unsubscribeRecipient user1, recipients.get(recipientCount - 1), ->
# User.findById user1.id, (err, user1) ->
# stripeInfo = user1.get('stripe')
# expect(stripeInfo.recipients.length).toEqual(recipientCount - 2)
# verifyNotSponsoring user1.id, recipients.get(recipientCount - 1).id, ->
# verifyNotRecipient recipients.get(recipientCount - 1).id, ->
# stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) ->
# expect(err).toBeNull()
# expect(subscription).not.toBeNull()
# numSponsored = recipientCount - 2
# if numSponsored <= 1
# expect(subscription.quantity).toEqual(subPrice)
# else if numSponsored <= 11
# expect(subscription.quantity).toEqual(subPrice + (numSponsored - 1) * subPrice * 0.8)
# else
# expect(subscription.quantity).toEqual(subPrice + 10 * subPrice * 0.8 + (numSponsored - 11) * subPrice * 0.6)
# nockDone()
# done()
describe 'APIs', ->
subscriptionURL = getURL('/db/subscription')
@ -1694,7 +1694,6 @@ describe 'Subscriptions', ->
token: token.id
timestamp: new Date()
request.put {uri: "#{subscriptionURL}/-/year_sale", json: requestBody, headers: headers }, (err, res) ->
console.log err
expect(err).toBeNull()
nockDone()
done()

View file

@ -11,16 +11,13 @@ describe 'POST /db/user', ->
createAnonNameUser = (name, done)->
request.post getURL('/auth/logout'), ->
request.get getURL('/auth/whoami'), ->
req = request.post(getURL('/db/user'), (err, response) ->
req = request.post({ url: getURL('/db/user'), json: {name}}, (err, response) ->
expect(response.statusCode).toBe(200)
request.get getURL('/auth/whoami'), (request, response, body) ->
res = JSON.parse(response.body)
expect(res.anonymous).toBeTruthy()
expect(res.name).toEqual(name)
request.get { url: getURL('/auth/whoami'), json: true }, (request, response, body) ->
expect(body.anonymous).toBeTruthy()
expect(body.name).toEqual(name)
done()
)
form = req.form()
form.append('name', name)
it 'preparing test : clears the db first', (done) ->
clearModels [User], (err) ->
@ -77,16 +74,13 @@ describe 'POST /db/user', ->
createAnonNameUser('Jim', done)
it 'should allow setting existing user name to anonymous user', (done) ->
req = request.post(getURL('/db/user'), (err, response, body) ->
req = request.post({url: getURL('/db/user'), json: {email: 'new@user.com', password: 'new'}}, (err, response, body) ->
expect(response.statusCode).toBe(200)
request.get getURL('/auth/whoami'), (request, response, body) ->
res = JSON.parse(response.body)
expect(res.anonymous).toBeFalsy()
createAnonNameUser 'Jim', done
)
form = req.form()
form.append('email', 'new@user.com')
form.append('password', 'new')
describe 'PUT /db/user', ->
@ -103,23 +97,22 @@ describe 'PUT /db/user', ->
it 'denies requests to edit someone who is not joe', (done) ->
unittest.getAdmin (admin) ->
req = request.put getURL(urlUser),
(err, res) ->
request.put {url: getURL(urlUser), json: {_id: admin.id}}, (err, res) ->
expect(res.statusCode).toBe(403)
done()
req.form().append('_id', admin.id)
it 'denies invalid data', (done) ->
unittest.getNormalJoe (joe) ->
req = request.put getURL(urlUser),
(err, res) ->
json = {
_id: joe.id
email: 'farghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlar
ghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghl'
}
request.put { url: getURL(urlUser), json }, (err, res) ->
expect(res.statusCode).toBe(422)
expect(res.body.indexOf('too long')).toBeGreaterThan(-1)
expect(res.body[0].message.indexOf('too long')).toBeGreaterThan(-1)
done()
form = req.form()
form.append('_id', joe.id)
form.append('email', 'farghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlar
ghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghl')
it 'does not allow normals to edit their permissions', utils.wrap (done) ->
user = yield utils.initUser()
@ -132,47 +125,45 @@ ghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghl
loginAdmin -> done()
it 'denies non-existent ids', (done) ->
req = request.put getURL(urlUser),
(err, res) ->
json = {
_id: '513108d4cb8b610000000004',
email: 'perfectly@good.com'
}
request.put {url: getURL(urlUser), json}, (err, res) ->
expect(res.statusCode).toBe(404)
done()
form = req.form()
form.append('_id', '513108d4cb8b610000000004')
form.append('email', 'perfectly@good.com')
it 'denies if the email being changed is already taken', (done) ->
unittest.getNormalJoe (joe) ->
unittest.getAdmin (admin) ->
req = request.put getURL(urlUser), (err, res) ->
json = { _id: admin.id, email: joe.get('email').toUpperCase() }
request.put { url: getURL(urlUser), json }, (err, res) ->
expect(res.statusCode).toBe(409)
expect(res.body.indexOf('already used')).toBeGreaterThan(-1)
expect(res.body.message.indexOf('already used')).toBeGreaterThan(-1)
done()
form = req.form()
form.append('_id', String(admin._id))
form.append('email', joe.get('email').toUpperCase())
it 'does not care if you include your existing name', (done) ->
unittest.getNormalJoe (joe) ->
req = request.put getURL(urlUser+'/'+joe._id), (err, res) ->
json = { _id: joe._id, name: 'Joe' }
request.put { url: getURL(urlUser+'/'+joe._id), json }, (err, res) ->
expect(res.statusCode).toBe(200)
done()
form = req.form()
form.append('_id', String(joe._id))
form.append('name', 'Joe')
it 'accepts name and email changes', (done) ->
unittest.getNormalJoe (joe) ->
req = request.put getURL(urlUser), (err, res) ->
json = {
_id: joe.id
email: 'New@email.com'
name: 'Wilhelm'
}
request.put { url: getURL(urlUser), json }, (err, res) ->
expect(res.statusCode).toBe(200)
unittest.getUser('Wilhelm', 'New@email.com', 'null', (joe) ->
expect(joe.get('name')).toBe('Wilhelm')
expect(joe.get('emailLower')).toBe('new@email.com')
expect(joe.get('email')).toBe('New@email.com')
done())
form = req.form()
form.append('_id', String(joe._id))
form.append('email', 'New@email.com')
form.append('name', 'Wilhelm')
it 'should not allow two users with the same name slug', (done) ->
loginSam (sam) ->
@ -189,7 +180,8 @@ ghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghl
it 'should silently rename an anonymous user if their name conflicts upon signup', (done) ->
request.post getURL('/auth/logout'), ->
request.get getURL('/auth/whoami'), ->
req = request.post getURL('/db/user'), (err, response) ->
json = { name: 'admin' }
request.post { url: getURL('/db/user'), json }, (err, response) ->
expect(response.statusCode).toBe(200)
request.get getURL('/auth/whoami'), (err, response) ->
expect(err).toBeNull()
@ -205,8 +197,6 @@ ghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghl
expect(finalGuy.name).not.toEqual guy.name
expect(finalGuy.name.length).toBe guy.name.length + 1
done()
form = req.form()
form.append('name', 'admin')
it 'should be able to unset a slug by setting an empty name', (done) ->
loginSam (sam) ->
@ -467,13 +457,13 @@ describe 'PUT /db/user/-/remain-teacher', ->
describe 'GET /db/user', ->
it 'logs in as admin', (done) ->
req = request.post(getURL('/auth/login'), (error, response) ->
json = {
username: 'admin@afc.com'
password: '80yqxpb38j'
}
request.post { url: getURL('/auth/login'), json }, (error, response) ->
expect(response.statusCode).toBe(200)
done()
)
form = req.form()
form.append('username', 'admin@afc.com')
form.append('password', '80yqxpb38j')
it 'get schema', (done) ->
request.get {uri: getURL(urlUser+'/schema')}, (err, res, body) ->
@ -523,7 +513,7 @@ describe 'GET /db/user', ->
# TODO Ruben should be able to fetch other users but probably with restricted data access
# Add to the test case above an extra data check
xit 'can fetch another user with restricted fields'
# xit 'can fetch another user with restricted fields'
describe 'GET /db/user/:handle', ->

View file

@ -77,4 +77,4 @@ module.exports.teardownNock = ->
before = (scope) ->
scope.body = (body) -> true
Promise.promisifyAll(module.exports)
Promise.promisifyAll(module.exports)

View file

@ -1,77 +1,77 @@
GLOBAL._ = require 'lodash'
require '../common'
AnalyticsUsersActive = require '../../../server/models/AnalyticsUsersActive'
LevelSession = require '../../../server/models/LevelSession'
User = require '../../../server/models/User'
mongoose = require 'mongoose'
# TODO: these tests have some rerun/cleanup issues
# TODO: add tests for purchase, payment, subscribe, unsubscribe, and earned achievements
# TODO: AnalyticsUsersActive collection isn't currently used.
# TODO: Will remove these tests if we end up ripping out the disabled saveActiveUser calls.
describe 'Analytics', ->
xit 'registered user', (done) ->
clearModels [AnalyticsUsersActive], (err) ->
expect(err).toBeNull()
user = new User
permissions: []
name: "Fred" + Math.floor(Math.random() * 10000)
user.save (err) ->
expect(err).toBeNull()
userID = mongoose.Types.ObjectId(user.get('_id'))
AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) ->
expect(activeUsers.length).toEqual(0)
user.register ->
AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) ->
expect(err).toBeNull()
expect(activeUsers.length).toEqual(1)
expect(activeUsers[0]?.get('event')).toEqual('register')
done()
xit 'level completed', (done) ->
clearModels [AnalyticsUsersActive], (err) ->
expect(err).toBeNull()
unittest.getNormalJoe (joe) ->
userID = mongoose.Types.ObjectId(joe.get('_id'))
session = new LevelSession
name: 'Beat Gandalf'
levelID: 'lotr'
permissions: simplePermissions
state: complete: false
creator: userID
session.save (err) ->
expect(err).toBeNull()
AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) ->
expect(activeUsers.length).toEqual(0)
session.set 'state', complete: true
session.save (err) ->
expect(err).toBeNull()
AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) ->
expect(err).toBeNull()
expect(activeUsers.length).toEqual(1)
expect(activeUsers[0]?.get('event')).toEqual('level-completed/lotr')
done()
xit 'level playtime', (done) ->
clearModels [AnalyticsUsersActive], (err) ->
expect(err).toBeNull()
unittest.getNormalJoe (joe) ->
userID = mongoose.Types.ObjectId(joe.get('_id'))
session = new LevelSession
name: 'Beat Gandalf'
levelID: 'lotr'
permissions: simplePermissions
playtime: 60
creator: userID
session.save (err) ->
expect(err).toBeNull()
AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) ->
expect(err).toBeNull()
expect(activeUsers.length).toEqual(1)
expect(activeUsers[0]?.get('event')).toEqual('level-playtime/lotr')
done()
#GLOBAL._ = require 'lodash'
#
#require '../common'
#AnalyticsUsersActive = require '../../../server/models/AnalyticsUsersActive'
#LevelSession = require '../../../server/models/LevelSession'
#User = require '../../../server/models/User'
#mongoose = require 'mongoose'
#
## TODO: these tests have some rerun/cleanup issues
## TODO: add tests for purchase, payment, subscribe, unsubscribe, and earned achievements
#
## TODO: AnalyticsUsersActive collection isn't currently used.
## TODO: Will remove these tests if we end up ripping out the disabled saveActiveUser calls.
#
#describe 'Analytics', ->
#
# xit 'registered user', (done) ->
# clearModels [AnalyticsUsersActive], (err) ->
# expect(err).toBeNull()
# user = new User
# permissions: []
# name: "Fred" + Math.floor(Math.random() * 10000)
# user.save (err) ->
# expect(err).toBeNull()
# userID = mongoose.Types.ObjectId(user.get('_id'))
# AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) ->
# expect(activeUsers.length).toEqual(0)
# user.register ->
# AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) ->
# expect(err).toBeNull()
# expect(activeUsers.length).toEqual(1)
# expect(activeUsers[0]?.get('event')).toEqual('register')
# done()
#
# xit 'level completed', (done) ->
# clearModels [AnalyticsUsersActive], (err) ->
# expect(err).toBeNull()
# unittest.getNormalJoe (joe) ->
# userID = mongoose.Types.ObjectId(joe.get('_id'))
# session = new LevelSession
# name: 'Beat Gandalf'
# levelID: 'lotr'
# permissions: simplePermissions
# state: complete: false
# creator: userID
# session.save (err) ->
# expect(err).toBeNull()
# AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) ->
# expect(activeUsers.length).toEqual(0)
# session.set 'state', complete: true
# session.save (err) ->
# expect(err).toBeNull()
# AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) ->
# expect(err).toBeNull()
# expect(activeUsers.length).toEqual(1)
# expect(activeUsers[0]?.get('event')).toEqual('level-completed/lotr')
# done()
#
# xit 'level playtime', (done) ->
# clearModels [AnalyticsUsersActive], (err) ->
# expect(err).toBeNull()
# unittest.getNormalJoe (joe) ->
# userID = mongoose.Types.ObjectId(joe.get('_id'))
# session = new LevelSession
# name: 'Beat Gandalf'
# levelID: 'lotr'
# permissions: simplePermissions
# playtime: 60
# creator: userID
# session.save (err) ->
# expect(err).toBeNull()
# AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) ->
# expect(err).toBeNull()
# expect(activeUsers.length).toEqual(1)
# expect(activeUsers[0]?.get('event')).toEqual('level-playtime/lotr')
# done()
#

View file

@ -36,7 +36,6 @@ describe 'CoursesHelper', ->
describe 'progressData.get({classroom, course})', ->
it 'returns object with .completed=true and .started=true', ->
progressData = helper.calculateAllProgress(@classrooms, @courses, @courseInstances, @members)
console.log 'progress data?', progressData
progress = progressData.get {@classroom, @course}
expect(progress.completed).toBe true
expect(progress.started).toBe true

View file

@ -66,16 +66,6 @@ describe 'EnrollmentsView', ->
fail('There should be an #action-col, other tests depend on it.')
describe '"Get Licenses" area', ->
describe '"Contact Us" button', ->
it 'opens a TeachersContactModal, passing in the number of licenses', ->
spyOn(@view, 'openModalView')
@view.state.set('numberOfStudents', 20)
@view.$('#contact-us-btn').click()
expect(view.openModalView).toHaveBeenCalled()
args = view.openModalView.calls.argsFor(0)
expect(args[0] instanceof TeachersContactModal).toBe(true)
expect(args[0].enrollmentsNeeded).toBe(20)
describe 'when the teacher has made contact', ->
beforeEach ->

View file

@ -4,18 +4,28 @@ factories = require 'test/app/factories'
describe 'TeachersContactModal', ->
beforeEach (done) ->
@modal = new TeachersContactModal({ enrollmentsNeeded: 10 })
@modal = new TeachersContactModal()
@modal.render()
trialRequests = new TrialRequests([factories.makeTrialRequest()])
@modal.trialRequests.fakeRequests[0].respondWith({ status: 200, responseText: trialRequests.stringify() })
@modal.supermodel.once('loaded-all', done)
jasmine.demoModal(@modal)
it 'shows an error when the name is empty and the form is submitted', ->
@modal.$('input[name="name"]').val('')
@modal.$('form').submit()
expect(@modal.$('input[name="name"]').closest('.form-group').hasClass('has-error')).toBe(true)
it 'shows an error when the email is invalid and the form is submitted', ->
@modal.$('input[name="email"]').val('not an email')
@modal.$('form').submit()
expect(@modal.$('input[name="email"]').closest('.form-group').hasClass('has-error')).toBe(true)
it 'shows an error when licensesNeeded is not > 0 and the form is submitted', ->
@modal.$('input[name="licensesNeeded"]').val('')
@modal.$('form').submit()
expect(@modal.$('input[name="licensesNeeded"]').closest('.form-group').hasClass('has-error')).toBe(true)
it 'shows an error when the message is empty and the form is submitted', ->
@modal.$('textarea[name="message"]').val('')
@modal.$('form').submit()