Merge remote-tracking branch 'codecombat/master'
This commit is contained in:
commit
168cdc800d
78 changed files with 1446 additions and 966 deletions
.travis.ymlREADME.mdserver_setup.coffee
app
assets/images/pages/home
core
lib
locale
models
schemas/models
styles/editor/verifier
templates
views
NewHomeView.coffee
admin
courses
editor
component
level/thangs
verifier
play/level
teachers
scripts
server
commons
delighted.coffeehandlers
earned_achievement_handler.coffeepayment_handler.coffeesubscription_handler.coffeeuser_handler.coffee
middleware
models
routes
spec
helpers
server
test/app
|
@ -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
160
README.md
|
@ -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>
|
||||
[](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).
|
||||
|
||||
[](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)
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
|
BIN
app/assets/images/pages/home/course_languages.png
Executable file → Normal file
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 ![]() ![]() |
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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: "You’re 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: "You’re 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:
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -19,3 +19,6 @@
|
|||
|
||||
.test-failed
|
||||
color: red
|
||||
|
||||
.lineUnder
|
||||
border-bottom: 1px solid #ccc
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
49
app/templates/admin/school-counts.jade
Normal file
49
app/templates/admin/school-counts.jade
Normal 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
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
144
app/views/admin/SchoolCountsView.coffee
Normal file
144
app/views/admin/SchoolCountsView.coffee
Normal 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()
|
|
@ -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']
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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') ? []
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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' })
|
||||
|
|
194
scripts/addZenProspectLeadsToClose.js
Normal file
194
scripts/addZenProspectLeadsToClose.js
Normal 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);
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ?= {}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)})
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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()
|
|
@ -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) ->
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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', ->
|
||||
|
|
|
@ -77,4 +77,4 @@ module.exports.teardownNock = ->
|
|||
before = (scope) ->
|
||||
scope.body = (body) -> true
|
||||
|
||||
Promise.promisifyAll(module.exports)
|
||||
Promise.promisifyAll(module.exports)
|
||||
|
|
|
@ -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()
|
||||
#
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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()
|
||||
|
|
Reference in a new issue