Merge branch 'master' into production

This commit is contained in:
Matt Lott 2015-01-26 16:34:18 -08:00
commit 814ea967be
17 changed files with 501 additions and 159 deletions

View file

@ -1,7 +1,7 @@
### Please sign our Contributor License Agreement
**[http://codecombat.com/cla](http://codecombat.com/cla)**
**[https://codecombat.com/cla](https://codecombat.com/cla)**
It just grants us a non-exclusive license to use your contribution and certifies you have the right to contribute the code you submit. For both our sakes, we need this before we can accept a pull request. Don't worry, it's super easy.
For more info, see [http://codecombat.com/legal](http://codecombat.com/legal).
For more info, see [https://codecombat.com/legal](https://codecombat.com/legal).

View file

@ -1,20 +1,20 @@
CodeCombat
==========
![](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/readme_00.png)
<div style="text-align:center"><a href="http://codecombat.com/"><img src ="https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/readme_00.png" /></div></a>
[![Build Status](https://travis-ci.org/codecombat/codecombat.png?branch=master)](https://travis-ci.org/codecombat/codecombat)
CodeCombat is a multiplayer programming game for learning how to code. **See the [Archmage (coder) developer wiki](https://github.com/codecombat/codecombat/wiki/Archmage-Home) for a dev setup guide, extensive documentation, and much more. Every new person that wants to start contributing the project coding should start there**
CodeCombat is a multiplayer programming game for learning how to code. **See the [Archmage (coder) developer wiki](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.**
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.
### [Getting Started](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, Linux, or Windows. 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](https://github.com/codecombat/codecombat/wiki/Dev-Setup:-Mac-and-Vagrant), [Linux](https://github.com/codecombat/codecombat/wiki/Dev-Setup:-Linux), or [Windows](https://github.com/codecombat/codecombat/wiki/Dev-Setup:-Windows). See [the docs for details](https://github.com/codecombat/codecombat/wiki/Developer-environment).
### [Getting In Touch](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, our issue tracker, or our developer chat room, 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](discourse.codecombat.com), our [issue tracker](https://github.com/codecombat/codecombat/issues), or our [developer chat room](https://www.hipchat.com/g3plnOKqa), or see the docs for [more on how to contribute](https://github.com/codecombat/codecombat/wiki/Developer-organization).
### [License](https://github.com/codecombat/codecombat/blob/master/LICENSE)

View file

@ -325,6 +325,7 @@ module.exports.thangNames = thangNames =
'Cairn'
'Cecily'
'Clare'
'Erica'
'Gemma'
'Ivy'
'Jensen'
@ -366,6 +367,7 @@ module.exports.thangNames = thangNames =
'Robin'
'Roman'
'Simon'
'Sharp Shooter'
'Slyvos'
'Vican'
]
@ -435,6 +437,7 @@ module.exports.thangNames = thangNames =
'Polifemo'
'Saltporker'
'Skrungt'
'Stinker'
'Tarlok'
'Trogdor'
'Trung'
@ -570,6 +573,7 @@ module.exports.thangNames = thangNames =
'Knight': [
'Almeric'
'Alphonse'
'Altair'
'Arthur'
'Drake'
'Duran'
@ -587,6 +591,7 @@ module.exports.thangNames = thangNames =
'Anya'
'Brigette'
'Dimia'
'Div'
'Hardcastle'
'Helena'
'Isa'
@ -596,6 +601,7 @@ module.exports.thangNames = thangNames =
'Leona'
'Lia'
'Lily'
'Nicks'
'Philips'
'Sarre'
]
@ -628,5 +634,6 @@ module.exports.thangNames = thangNames =
]
'Ogre Scout F': [
'Freesa'
'Ganju'
'Ralthora'
]

View file

@ -81,7 +81,7 @@ module.exports = nativeDescription: "Català", englishDescription: "Catalan", tr
awaiting_levels_adventurer_prefix: "Fem cinc nivells per setmana"
awaiting_levels_adventurer: "Inicia sessió com aventurer"
awaiting_levels_adventurer_suffix: "sigues el primer en jugar els nous nivells"
# adjust_volume: "Adjust volume"
adjust_volume: "Ajustar volum"
choose_your_level: "Escull el teu nivell" # The rest of this section is the old play view at /play-old and isn't very important.
adventurer_prefix: "Pots saltar a qualsevols dels nivells de més abaix, o discutir els nivells de més amunt."
adventurer_forum: "El fòrum de l'aventurer"
@ -105,7 +105,7 @@ module.exports = nativeDescription: "Català", englishDescription: "Catalan", tr
forgot_password: "Contrasenya oblidada?"
authenticate_gplus: "Inicia amb G+"
load_profile: "Carrega un perfil de G+"
# load_email: "Load G+ Email"
load_email: "Carregar email de G+"
finishing: "Acabant"
sign_in_with_facebook: "Inicia amb Facebook"
sign_in_with_gplus: "Inicia amb G+"
@ -167,7 +167,7 @@ module.exports = nativeDescription: "Català", englishDescription: "Catalan", tr
# submitter: "Submitter"
# submitted: "Submitted"
# commit_msg: "Commit Message"
# review: "Review"
review: "Revisió"
version_history: "Historial de versions"
# version_history_for: "Version History for: "
# select_changes: "Select two changes below to see the difference."
@ -175,19 +175,19 @@ module.exports = nativeDescription: "Català", englishDescription: "Catalan", tr
undo_shortcut: "(Ctrl+Z)"
# redo_prefix: "Redo"
redo_shortcut: "(Ctrl+Shift+Z)"
# play_preview: "Play preview of current level"
play_preview: "Reproduir avanç del nivell actual"
result: "Resultat"
results: "Resultats"
description: "Descripció"
# or: "or"
or: "o"
# subject: "Subject"
email: "Email"
password: "Contrasenya"
message: "Missatge"
# code: "Code"
code: "Codi"
# ladder: "Ladder"
# when: "When"
# opponent: "Opponent"
opponent: "Oponent"
# rank: "Rank"
score: "Puntuació"
win: "Guanyats"
@ -200,7 +200,7 @@ module.exports = nativeDescription: "Català", englishDescription: "Catalan", tr
player_level: "Nivell" # Like player level 5, not like level: Dungeons of Kithgard
warrior: "Guerrer"
# ranger: "Ranger"
# wizard: "Wizard"
wizard: "Mag"
units:
second: "segon"
@ -234,27 +234,27 @@ module.exports = nativeDescription: "Català", englishDescription: "Catalan", tr
timed_out: "S'ha acabat el temps"
failing: "Fallant"
action_timeline: "Cronologia d'accions"
# click_to_select: "Click on a unit to select it."
click_to_select: "Fes clic a una unitat per seleccionar-la"
control_bar_multiplayer: "Multijugador"
# control_bar_join_game: "Join Game"
# reload: "Reload"
# reload_title: "Reload All Code?"
# reload_really: "Are you sure you want to reload this level back to the beginning?"
# reload_confirm: "Reload All"
control_bar_join_game: "Entrar al joc"
reload: "Recarregar"
reload_title: "Recarregar tot el codi?"
reload_really: "Estàs segur que vos recarregar aquest nivell al principi?"
reload_confirm: "Recarregar tot"
victory: "Victòria"
victory_title_prefix: ""
victory_title_suffix: " Complet"
victory_sign_up: "Inicia sessió per a desar el progressos"
# victory_sign_up_poke: "Want to save your code? Create a free account!"
victory_sign_up_poke: "Vols guardar el teu codi? Crea un compte gratuit!"
victory_rate_the_level: "Valora el nivell: " # Only in old-style levels.
# victory_return_to_ladder: "Return to Ladder"
victory_play_continue: "Continuar"
victory_saving_progress: "Desa progrés"
victory_go_home: "Tornar a l'inici" # Only in old-style levels.
victory_review: "Diguens més!" # Only in old-style levels.
# victory_hour_of_code_done: "Are You Done?"
victory_hour_of_code_done: "Has acabat?"
# victory_hour_of_code_done_yes: "Yes, I'm finished with my Hour of Code™!"
# victory_experience_gained: "XP Gained"
victory_experience_gained: "XP Guanyada"
victory_gems_gained: "Gemmes guanyades"
guide_title: "Guia"
# tome_minion_spells: "Your Minions' Spells" # Only in old-style levels.
@ -286,24 +286,24 @@ module.exports = nativeDescription: "Català", englishDescription: "Catalan", tr
infinite_loop_try_again: "Tornar a intentar"
infinite_loop_reset_level: "Reiniciar nivell"
# infinite_loop_comment_out: "Comment Out My Code"
# tip_toggle_play: "Toggle play/paused with Ctrl+P."
# tip_scrub_shortcut: "Ctrl+[ and Ctrl+] rewind and fast-forward."
tip_toggle_play: "Canvia entre reproduir/pausa amb Ctrl+P"
tip_scrub_shortcut: "Ctrl+[ i Ctrl+] per rebobinar i avançar"
# tip_guide_exists: "Click the guide, inside game menu (at the top of the page), for useful info."
# tip_open_source: "CodeCombat is 100% open source!"
# tip_beta_launch: "CodeCombat launched its beta in October, 2013."
tip_open_source: "CodeCombat és 100% codi lliure!"
tip_beta_launch: "CodeCombat va llançar la seva beta l'octubre de 2013."
tip_think_solution: "Pensa en la solució,no en el problema."
# tip_theory_practice: "In theory, there is no difference between theory and practice. But in practice, there is. - Yogi Berra"
# tip_error_free: "There are two ways to write error-free programs; only the third one works. - Alan Perlis"
# tip_debugging_program: "If debugging is the process of removing bugs, then programming must be the process of putting them in. - Edsger W. Dijkstra"
tip_theory_practice: "En teoria no hi ha diferència entre la teoria i la pràctica. Però a la pràctica si que n'hi ha. - Yogi Berra"
tip_error_free: "Només hi ha dues maneres d'escriure programes sense errors; la tercera és la única que funciona. - Alan Perlis"
tip_debugging_program: "Si debuguejar és el procés d'eliminar errors, llavors programar és el procés de posar-los. - Edsger W. Dijkstra"
# tip_forums: "Head over to the forums and tell us what you think!"
# tip_baby_coders: "In the future, even babies will be Archmages."
tip_baby_coders: "En el futur fins i tot els bebés podran ser Artximags."
# tip_morale_improves: "Loading will continue until morale improves."
# tip_all_species: "We believe in equal opportunities to learn programming for all species."
# tip_reticulating: "Reticulating spines."
# tip_harry: "Yer a Wizard, "
# tip_great_responsibility: "With great coding skill comes great debug responsibility."
# tip_munchkin: "If you don't eat your vegetables, a munchkin will come after you while you're asleep."
# tip_binary: "There are only 10 types of people in the world: those who understand binary, and those who don't."
tip_binary: "Hi ha 10 tipus de persones al mon, les que saben programar en binari i les que no"
# tip_commitment_yoda: "A programmer must have the deepest commitment, the most serious mind. ~ Yoda"
# tip_no_try: "Do. Or do not. There is no try. - Yoda"
# tip_patience: "Patience you must have, young Padawan. - Yoda"
@ -344,7 +344,7 @@ module.exports = nativeDescription: "Català", englishDescription: "Catalan", tr
inventory:
choose_inventory: "Equipar objectes"
equipped_item: "Equipat"
# required_purchase_title: "Required"
required_purchase_title: "Necessari"
available_item: "Disponible"
# restricted_title: "Restricted"
# should_equip: "(double-click to equip)"

View file

@ -1,7 +1,7 @@
module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription: "German (Germany)", translation:
home:
slogan: "Lerne spielend Programmieren"
no_ie: "CodeCombat läuft nicht im IE8 oder älteren Browsern. Tut uns Leid!" # Warning that only shows up in IE8 and older
no_ie: "CodeCombat läuft nicht im IE 8 oder älteren Browsern. Tut uns Leid!" # Warning that only shows up in IE8 and older
no_mobile: "CodeCombat ist nicht für Mobilgeräte optimiert und funktioniert möglicherweise nicht." # Warning that shows up on mobile devices
play: "Spielen" # The big play button that opens up the campaign view.
old_browser: "Oh! Dein Browser ist zu alt für CodeCombat. Sorry!" # Warning that shows up on really old Firefox/Chrome/Safari
@ -966,12 +966,12 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
email_settings_url: "deiner Emaileinstellungen"
email_description_suffix: "oder durch von uns gesendete Links kannst du jederzeit deine Einstellungen ändern und Abonnements kündigen."
cost_title: "Kosten"
cost_description: "CodeCombat ist zur Zeit 100% kostenlos! Eines unserer Hauptziele ist, es dabei zu belassen, so dass es so viele Leute wie möglich spielen können, unabhängig davon in welcher Lebenssituation sie sich befinden. Falls dunkle Wolken aufziehen, könnten wir manche Inhalte im Rahmen eines Abonnements anbieten, aber lieber nicht. Mit etwas Glück können wir die Firma erhalten durch:"
cost_description: "Alle normalen Level von CodeCombat sind kostenlos spielbar, mit einem Abonement von $9.99 USD/Monat kann man extra Level Verzweigungen und 3500 Bonus Juwelen pro Monat. Du kannst das Abonement mit nur einem Click wiederrufen und wir versprechen eine 100% Geld-zurück Garantie."
copyrights_title: "Copyrights und Lizenzen"
contributor_title: "Contributor License Agreement"
contributor_description_prefix: "Alle Beiträge, sowohl auf unserer Webseite als auch in unserem GitHub Repository, unterliegen unserer"
cla_url: "CLA"
contributor_description_suffix: "zu welcher du dich einverstanden erklären musst bevor du beitragen kannst."
contributor_description_suffix: "zu welcher du dich einverstanden erklären musst bevor du zu der Entwicklung beitragen kannst."
code_title: "Code - MIT"
code_description_prefix: "Der gesamte Code der CodeCombat gehört oder auf codecombat.com gehostet wird, sowohl im GitHub Repository als auch auch in der codecombat.com Datenbank, ist lizensiert durch die"
mit_license_url: "MIT Lizenz"

View file

@ -11,3 +11,31 @@
.button.close
font-size: 63px
.line-graph-label
font-size: 10pt
font-weight: normal
.line-graph-container
height: 500px
width: 100%
position: relative
.x.axis
font-size: 9pt
path
display: none
.y.axis
font-size: 9pt
path
display: none
.key-line
font-size: 9pt
.key-text
font-size: 9pt
.graph-point-info-container
display: none
position: absolute
padding: 10px
border: 1px solid black
z-index: 3
background-color: blanchedalmond
font-size: 10pt

View file

@ -219,9 +219,6 @@ $gameControlMargin: 30px
color: black
text-shadow: 0 1px 0 white
.campaign-label
text-shadow: 0 1px 0 white
.start-level
display: block
margin: 10px auto 0 auto

View file

@ -5,7 +5,7 @@ block content
h1(data-i18n="admin.growth_title") Growth
if me.isAdmin()
if crunchingData
h4 Cruncing Data..
h4 Crunching Data..
else
h2 Registered Users
h3 Per-Day

View file

@ -10,49 +10,20 @@
input.form-control#input-startday(type='text', style='width:100px;', value=analytics.startDay)
input.form-control#input-endday(type='text', style='width:100px;', value=analytics.endDay)
button.btn.btn-default.btn-sm#reload-button(style='margin-left:10px;') Reload
h4 Completion Rates
if analytics.levelCompletions.loading
div Loading...
else
table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt')
thead
tr
td Date
td Started
td Finished
td Completion %
if analytics.levelHelps.levels.length === analytics.levelCompletions.levels.length
td Helps Clicked
td Helps / Started
td Help Videos
td Videos / Started
tbody
- for (var i = 0; i < analytics.levelCompletions.levels.length; i++)
tr
td= analytics.levelCompletions.levels[i].created
td= analytics.levelCompletions.levels[i].started
td= analytics.levelCompletions.levels[i].finished
td= analytics.levelCompletions.levels[i].rate
if analytics.levelHelps.levels.length === analytics.levelCompletions.levels.length && analytics.levelCompletions.levels[i].created == analytics.levelHelps.levels[i].day
td= analytics.levelHelps.levels[i].alertHelps + analytics.levelHelps.levels[i].paletteHelps
td= ((analytics.levelHelps.levels[i].alertHelps + analytics.levelHelps.levels[i].paletteHelps) / analytics.levelCompletions.levels[i].started).toFixed(2)
td= analytics.levelHelps.levels[i].videoStarts
td= (analytics.levelHelps.levels[i].videoStarts / analytics.levelCompletions.levels[i].started).toFixed(2)
h4 Average Playtimes
if analytics.levelPlaytimes.loading
div Loading...
else
table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt')
thead
tr
td Date
td Average (s)
tbody
- for (var i = 0; i < analytics.levelPlaytimes.levels.length; i++)
tr
td= analytics.levelPlaytimes.levels[i].created
td= analytics.levelPlaytimes.levels[i].average.toFixed(2)
each graph in analytics.graphs
each line in graph.lines
label.line-graph-label
input.line-graph-checkbox(data-lineid="#{line.lineID}", type='checkbox', checked=line.enabled)
span #{line.description}
span &nbsp;&nbsp;
.line-graph-container
each line in graph.lines
each point in line.points
.graph-point-info-container(data-pointid="#{point.pointID}")
div(style='font-weight:bold;') #{point.day}
each value in point.values
div #{value}
h4 Common Problems
if analytics.commonProblems.loading
@ -66,18 +37,18 @@
td Error Hint
td Count
tbody
- for (var i = 0; i < analytics.commonProblems.levels.length && i < 20; i++)
- for (var i = 0; i < analytics.commonProblems.data.length && i < 20; i++)
tr
td= analytics.commonProblems.levels[i].language
td= analytics.commonProblems.levels[i].message
td= analytics.commonProblems.levels[i].hint
td= analytics.commonProblems.levels[i].count
td= analytics.commonProblems.data[i].language
td= analytics.commonProblems.data[i].message
td= analytics.commonProblems.data[i].hint
td= analytics.commonProblems.data[i].count
h4 Recent Sessions
if analytics.recentSessions.loading
div Loading...
else
div(style='font-size:10pt') Latest #{analytics.recentSessions.levels.length} sessions for this level
div(style='font-size:10pt') Latest #{analytics.recentSessions.data.length} sessions for this level
div(style='font-size:10pt') Double-click row to open player and session
table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt')
thead
@ -89,17 +60,61 @@
td Complete
td Changed
tbody
- for (var i = 0; i < analytics.recentSessions.levels.length; i++)
tr.recent-session(data-player-id=analytics.recentSessions.levels[i].creator, data-session-id=analytics.recentSessions.levels[i]._id)
td= analytics.recentSessions.levels[i]._id
td= analytics.recentSessions.levels[i].creatorName || analytics.recentSessions.levels[i].creator
td= analytics.recentSessions.levels[i].codeLanguage
td= analytics.recentSessions.levels[i].playtime
if analytics.recentSessions.levels[i].state && analytics.recentSessions.levels[i].state.complete
td= analytics.recentSessions.levels[i].state.complete
- for (var i = 0; i < analytics.recentSessions.data.length; i++)
tr.recent-session(data-player-id=analytics.recentSessions.data[i].creator, data-session-id=analytics.recentSessions.data[i]._id)
td= analytics.recentSessions.data[i]._id
td= analytics.recentSessions.data[i].creatorName || analytics.recentSessions.data[i].creator
td= analytics.recentSessions.data[i].codeLanguage
td= analytics.recentSessions.data[i].playtime
if analytics.recentSessions.data[i].state && analytics.recentSessions.data[i].state.complete
td= analytics.recentSessions.data[i].state.complete
else
td false
td= analytics.recentSessions.levels[i].changed
td= analytics.recentSessions.data[i].changed
h4 Completion Rates
if analytics.levelCompletions.loading
div Loading...
else
table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt')
thead
tr
td Date
td Started
td Finished
td Completion %
if analytics.levelHelps.data.length === analytics.levelCompletions.data.length
td Helps Clicked
td Helps / Started
td Help Videos
td Videos / Started
tbody
- for (var i = 0; i < analytics.levelCompletions.data.length; i++)
tr
td= analytics.levelCompletions.data[i].created
td= analytics.levelCompletions.data[i].started
td= analytics.levelCompletions.data[i].finished
td= analytics.levelCompletions.data[i].rate
if analytics.levelHelps.data.length === analytics.levelCompletions.data.length && analytics.levelCompletions.data[i].created == analytics.levelHelps.data[i].day
td= analytics.levelHelps.data[i].alertHelps + analytics.levelHelps.data[i].paletteHelps
td= ((analytics.levelHelps.data[i].alertHelps + analytics.levelHelps.data[i].paletteHelps) / analytics.levelCompletions.data[i].started).toFixed(2)
td= analytics.levelHelps.data[i].videoStarts
td= (analytics.levelHelps.data[i].videoStarts / analytics.levelCompletions.data[i].started).toFixed(2)
h4 Average Playtimes
if analytics.levelPlaytimes.loading
div Loading...
else
table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt')
thead
tr
td Date
td Average (s)
tbody
- for (var i = 0; i < analytics.levelPlaytimes.data.length; i++)
tr
td= analytics.levelPlaytimes.data[i].created
td= analytics.levelPlaytimes.data[i].average.toFixed(2)
if level.get('tasks')
.tasks

View file

@ -37,7 +37,7 @@
span(data-i18n="play.players") players
span.spr , #{Math.round(playCount.playtime / 3600)}
span(data-i18n="play.hours_played") hours played
.campaign-label= i18n(campaign.attributes, 'name')
if isIPadApp && !level.disabled && !level.locked
button.btn.btn-success.btn-lg.start-level(data-i18n="common.play") Play

View file

@ -11,7 +11,7 @@ block content
h3 Preparation
p CodeCombat is free to play for the core level progression and does not require students to sign up. We encourage teachers to
a(href="http://codecombat.com/play") play through the campaign
a(href="/play") play through the campaign
| to try it out, but the only thing you absolutely need to do to be ready is ensure students have access to a computer.
p It is not necessary for teachers to be comfortable with computer science concepts for students to have fun learning with CodeCombat.

View file

@ -10,16 +10,18 @@ module.exports = class CampaignLevelView extends CocoView
template: require 'templates/editor/campaign/campaign-level-view'
events:
'change .line-graph-checkbox': 'updateGraphCheckbox'
'click .close': 'onClickClose'
'click #reload-button': 'onClickReloadButton'
'dblclick .recent-session': 'onDblClickRecentSession'
'mouseenter .graph-point': 'onMouseEnterPoint'
'mouseleave .graph-point': 'onMouseLeavePoint'
constructor: (options, @level) ->
super(options)
@fullLevel = new Level _id: @level.id
@fullLevel.fetch()
@listenToOnce @fullLevel, 'sync', => @render?()
@levelSlug = @level.get('slug')
@getAnalytics()
@ -33,6 +35,17 @@ module.exports = class CampaignLevelView extends CocoView
super()
$("#input-startday").datepicker dateFormat: "yy-mm-dd"
$("#input-endday").datepicker dateFormat: "yy-mm-dd"
# TODO: Why does this have to be called from afterRender() instead of getRenderData()?
@updateAnalyticsGraphs()
updateGraphCheckbox: (e) ->
lineID = $(e.target).data('lineid')
checked = $(e.target).prop('checked')
for graph in @analytics.graphs
for line in graph.lines
if line.lineID is lineID
line.enabled = checked
return @render()
onClickClose: ->
@$el.addClass('hidden')
@ -51,7 +64,295 @@ module.exports = class CampaignLevelView extends CocoView
session = new LevelSession _id: row.data 'session-id'
@openModalView new ModelModal models: [session, player]
onMouseEnterPoint: (e) ->
pointID = $(e.target).data('pointid')
container = @$el.find(".graph-point-info-container[data-pointid=#{pointID}]").show()
margin = 20
width = container.outerWidth()
height = container.outerHeight()
container.css('left', e.offsetX - width / 2)
container.css('top', e.offsetY - height - margin)
onMouseLeavePoint: (e) ->
pointID = $(e.target).data('pointid')
@$el.find(".graph-point-info-container[data-pointid=#{pointID}]").hide()
updateAnalyticsGraphData: ->
# console.log 'updateAnalyticsGraphData'
# Build graphs based on available @analytics data
# Currently only one graph
@analytics.graphs = [graphID: 'level-completions', lines: []]
# TODO: Where should this metadata live?
# TODO: lineIDs assumed to be unique across graphs
completionLineID = 'level-completions'
playtimeLineID = 'level-playtime'
helpsLineID = 'helps-clicked'
videosLineID = 'help-videos'
lineMetadata = {}
lineMetadata[completionLineID] =
description: 'Level Completion (%)'
color: 'red'
lineMetadata[playtimeLineID] =
description: 'Average Playtime (s)'
color: 'green'
lineMetadata[helpsLineID] =
description: 'Help click rate (%)'
color: 'blue'
lineMetadata[videosLineID] =
description: 'Help video rate (%)'
color: 'purple'
# Use this days aggregate to fill in missing days from the analytics data
days = {}
days["#{day.created[0..3]}-#{day.created[4..5]}-#{day.created[6..7]}"] = true for day in @analytics.levelCompletions.data if @analytics?.levelCompletions?.data?
days[day.created] = true for day in @analytics.levelPlaytimes.data if @analytics?.levelPlaytimes?.data?
days["#{day.day[0..3]}-#{day.day[4..5]}-#{day.day[6..7]}"] = true for day in @analytics.levelHelps.data if @analytics?.levelHelps?.data?
days = Object.keys(days).sort (a, b) -> if a < b then -1 else 1
if days.length > 0
currentIndex = 0
currentDay = days[currentIndex]
currentDate = new Date(currentDay + "T00:00:00.000Z")
lastDay = days[days.length - 1]
while currentDay isnt lastDay
days.splice currentIndex, 0, currentDay if days[currentIndex] isnt currentDay
currentIndex++
currentDate.setUTCDate(currentDate.getUTCDate() + 1)
currentDay = currentDate.toISOString().substr(0, 10)
# Update level completion graph data
dayStartedMap = {}
if @analytics?.levelCompletions?.data?.length > 0
# Build line data
levelPoints = []
for day, i in @analytics.levelCompletions.data
dayStartedMap[day.created] = day.started
rate = parseFloat(day.rate)
levelPoints.push
x: i
y: rate
started: day.started
day: "#{day.created[0..3]}-#{day.created[4..5]}-#{day.created[6..7]}"
pointID: "#{completionLineID}#{i}"
values: ["Started: #{day.started}", "Finished: #{day.finished}", "Completion rate: #{rate.toFixed(2)}%"]
# Ensure points for each day
for day, i in days
if levelPoints.length <= i or levelPoints[i].day isnt day
levelPoints.splice i, 0,
y: 0.0
day: day
values: []
levelPoints[i].x = i
levelPoints[i].pointID = "#{completionLineID}#{i}"
@analytics.graphs[0].lines.push
lineID: completionLineID
enabled: true
points: levelPoints
description: lineMetadata[completionLineID].description
lineColor: lineMetadata[completionLineID].color
min: 0
max: 100.0
# Update average playtime graph data
if @analytics?.levelPlaytimes?.data?.length > 0
# Build line data
playtimePoints = []
for day, i in @analytics.levelPlaytimes.data
avg = parseFloat(day.average)
playtimePoints.push
x: i
y: avg
day: day.created
pointID: "#{playtimeLineID}#{i}"
values: ["Average playtime: #{avg.toFixed(2)}s"]
# Ensure points for each day
for day, i in days
if playtimePoints.length <= i or playtimePoints[i].day isnt day
playtimePoints.splice i, 0,
y: 0.0
day: day
values: []
playtimePoints[i].x = i
playtimePoints[i].pointID = "#{playtimeLineID}#{i}"
@analytics.graphs[0].lines.push
lineID: playtimeLineID
enabled: true
points: playtimePoints
description: lineMetadata[playtimeLineID].description
lineColor: lineMetadata[playtimeLineID].color
min: 0
max: d3.max(playtimePoints, (d) -> d.y)
# Update help graph data
if @analytics?.levelHelps?.data?.length > 0
# Build line data
helpPoints = []
videoPoints = []
for day, i in @analytics.levelHelps.data
helpCount = day.alertHelps + day.paletteHelps
started = dayStartedMap[day.day] ? 0
clickRate = if started > 0 then helpCount / started * 100 else 0
videoRate = day.videoStarts / helpCount * 100
helpPoints.push
x: i
y: clickRate
day: "#{day.day[0..3]}-#{day.day[4..5]}-#{day.day[6..7]}"
pointID: "#{helpsLineID}#{i}"
values: ["Helps clicked: #{helpCount}", "Helps click clickRate: #{clickRate.toFixed(2)}%"]
videoPoints.push
x: i
y: videoRate
day: "#{day.day[0..3]}-#{day.day[4..5]}-#{day.day[6..7]}"
pointID: "#{videosLineID}#{i}"
values: ["Help videos started: #{day.videoStarts}", "Help videos start rate: #{videoRate.toFixed(2)}%"]
# Ensure points for each day
for day, i in days
if helpPoints.length <= i or helpPoints[i].day isnt day
helpPoints.splice i, 0,
y: 0.0
day: day
values: []
helpPoints[i].x = i
helpPoints[i].pointID = "#{helpsLineID}#{i}"
if videoPoints.length <= i or videoPoints[i].day isnt day
videoPoints.splice i, 0,
y: 0.0
day: day
values: []
videoPoints[i].x = i
videoPoints[i].pointID = "#{videosLineID}#{i}"
if d3.max(helpPoints, (d) -> d.y) > 0
@analytics.graphs[0].lines.push
lineID: helpsLineID
enabled: true
points: helpPoints
description: lineMetadata[helpsLineID].description
lineColor: lineMetadata[helpsLineID].color
min: 0
max: 100.0
if d3.max(videoPoints, (d) -> d.y) > 0
@analytics.graphs[0].lines.push
lineID: videosLineID
enabled: true
points: videoPoints
description: lineMetadata[videosLineID].description
lineColor: lineMetadata[videosLineID].color
min: 0
max: 100.0
updateAnalyticsGraphs: ->
# Build d3 graphs
return unless @analytics?.graphs?.length > 0
containerSelector = '.line-graph-container'
# console.log 'updateAnalyticsGraphs', containerSelector, @analytics.graphs
margin = 20
keyHeight = 20
xAxisHeight = 20
yAxisWidth = 40
containerWidth = $(containerSelector).width()
containerHeight = $(containerSelector).height()
for graph in @analytics.graphs
graphLineCount = _.reduce graph.lines, ((sum, item) -> if item.enabled then sum + 1 else sum), 0
svg = d3.select(containerSelector).append("svg")
.attr("width", containerWidth)
.attr("height", containerHeight)
width = containerWidth - margin * 2 - yAxisWidth * graphLineCount
height = containerHeight - margin * 2 - xAxisHeight - keyHeight * graphLineCount
currentLine = 0
for line in graph.lines
continue unless line.enabled
xRange = d3.scale.linear().range([0, width]).domain([d3.min(line.points, (d) -> d.x), d3.max(line.points, (d) -> d.x)])
yRange = d3.scale.linear().range([height, 0]).domain([line.min, line.max])
# x-Axis and guideline once
if currentLine is 0
startDay = new Date(line.points[0].day)
endDay = new Date(line.points[line.points.length - 1].day)
xAxisRange = d3.time.scale()
.domain([startDay, endDay])
.range([0, width])
xAxis = d3.svg.axis()
.scale(xAxisRange)
svg.append("g")
.attr("class", "x axis")
.call(xAxis)
.selectAll("text")
.attr("dy", ".35em")
.attr("transform", "translate(" + (margin + yAxisWidth * (graphLineCount - 1)) + "," + (height + margin) + ")")
.style("text-anchor", "start")
# Horizontal guidelines
svg.selectAll(".line")
.data([10, 30, 50, 70, 90])
.enter()
.append("line")
.attr("x1", margin + yAxisWidth * graphLineCount)
.attr("y1", (d) -> margin + yRange(d))
.attr("x2", margin + yAxisWidth * graphLineCount + width)
.attr("y2", (d) -> margin + yRange(d))
.attr("stroke", line.lineColor)
.style("opacity", "0.5")
# y-Axis
yAxisRange = d3.scale.linear().range([height, 0]).domain([line.min, line.max])
yAxis = d3.svg.axis()
.scale(yRange)
.orient("left")
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + (margin + yAxisWidth * currentLine) + "," + margin + ")")
.style("color", line.lineColor)
.call(yAxis)
.selectAll("text")
.attr("y", 0)
.attr("x", 0)
.attr("fill", line.lineColor)
.style("text-anchor", "start")
# Key
svg.append("line")
.attr("x1", margin)
.attr("y1", margin + height + xAxisHeight + keyHeight * currentLine + keyHeight / 2)
.attr("x2", margin + 40)
.attr("y2", margin + height + xAxisHeight + keyHeight * currentLine + keyHeight / 2)
.attr("stroke", line.lineColor)
.attr("class", "key-line")
svg.append("text")
.attr("x", margin + 40 + 10)
.attr("y", margin + height + xAxisHeight + keyHeight * currentLine + (keyHeight + 10) / 2)
.attr("fill", line.lineColor)
.attr("class", "key-text")
.text(line.description)
# Path and points
svg.selectAll(".circle")
.data(line.points)
.enter()
.append("circle")
.attr("transform", "translate(" + (margin + yAxisWidth * graphLineCount) + "," + margin + ")")
.attr("cx", (d) -> xRange(d.x))
.attr("cy", (d) -> yRange(d.y))
.attr("r", (d) -> if d.started then Math.max(3, Math.min(10, Math.log(parseInt(d.started)))) + 2 else 6)
.attr("fill", line.lineColor)
.attr("stroke-width", 1)
.attr("class", "graph-point")
.attr("data-pointid", (d) -> "#{line.lineID}#{d.x}")
d3line = d3.svg.line()
.x((d) -> xRange(d.x))
.y((d) -> yRange(d.y))
.interpolate("linear")
svg.append("path")
.attr("d", d3line(line.points))
.attr("transform", "translate(" + (margin + yAxisWidth * graphLineCount) + "," + margin + ")")
.style("stroke-width", 1)
.style("stroke", line.lineColor)
.style("fill", "none")
currentLine++
getAnalytics: (startDay, endDay) =>
# Analytics APIs use 2 different day formats
if startDay?
startDayDashed = startDay
startDay = startDay.replace(/-/g, '')
@ -61,53 +362,40 @@ module.exports = class CampaignLevelView extends CocoView
if endDay?
endDayDashed = endDay
endDay = endDay.replace(/-/g, '')
else
else
endDay = utils.getUTCDay -1
endDayDashed = "#{endDay[0..3]}-#{endDay[4..5]}-#{endDay[6..7]}"
@analytics =
# Initialize
@analytics =
startDay: startDayDashed
endDay: endDayDashed
commonProblems:
levels: []
loading: true
levelCompletions:
levels: []
loading: true
levelHelps:
levels: []
loading: true
levelPlaytimes:
levels: []
loading: true
recentSessions:
levels: []
loading: true
@render()
commonProblems: {data: [], loading: true}
levelCompletions: {data: [], loading: true}
levelHelps: {data: [], loading: true}
levelPlaytimes: {data: [], loading: true}
recentSessions: {data: [], loading: true}
graphs: []
@render() # Hide old analytics data while we fetch new data
@getCommonLevelProblems startDayDashed, endDayDashed, () =>
@analytics.commonProblems.loading = false
@render()
@getLevelCompletions startDay, endDay, () =>
@analytics.levelCompletions.loading = false
@render()
@getLevelHelps startDay, endDay, () =>
@analytics.levelHelps.loading = false
@render()
@getLevelPlaytimes startDayDashed, endDayDashed, () =>
@analytics.levelPlaytimes.loading = false
@render()
@getRecentSessions () =>
@analytics.recentSessions.loading = false
@render()
makeFinishDataFetch = (data) =>
return =>
return if @destroyed
@updateAnalyticsGraphData()
data.loading = false
@render()
@getCommonLevelProblems startDayDashed, endDayDashed, makeFinishDataFetch(@analytics.commonProblems)
@getLevelCompletions startDay, endDay, makeFinishDataFetch(@analytics.levelCompletions)
@getLevelHelps startDay, endDay, makeFinishDataFetch(@analytics.levelHelps)
@getLevelPlaytimes startDayDashed, endDayDashed, makeFinishDataFetch(@analytics.levelPlaytimes)
@getRecentSessions makeFinishDataFetch(@analytics.recentSessions)
getCommonLevelProblems: (startDay, endDay, doneCallback) ->
success = (data) =>
return doneCallback() if @destroyed
@analytics.commonProblems.levels = data
# console.log 'getCommonLevelProblems', data
@analytics.commonProblems.data = data
doneCallback()
# TODO: Why do we need this url dash?
request = @supermodel.addRequestResource 'common_problems', {
url: '/db/user_code_problem/-/common_problems'
data: {startDay: startDay, endDay: endDay, slug: @levelSlug}
@ -119,13 +407,13 @@ module.exports = class CampaignLevelView extends CocoView
getLevelCompletions: (startDay, endDay, doneCallback) ->
success = (data) =>
return doneCallback() if @destroyed
data.sort (a, b) -> if a.created < b.created then 1 else -1
mapFn = (item) ->
item.rate = (item.finished / item.started * 100).toFixed(2)
# console.log 'getLevelCompletions', data
data.sort (a, b) -> if a.created < b.created then -1 else 1
mapFn = (item) ->
item.rate = if item.started > 0 then item.finished / item.started * 100 else 0
item
@analytics.levelCompletions.levels = _.map data, mapFn, @
@analytics.levelCompletions.data = _.map data, mapFn, @
doneCallback()
request = @supermodel.addRequestResource 'level_completions', {
url: '/db/analytics_perday/-/level_completions'
data: {startDay: startDay, endDay: endDay, slug: @levelSlug}
@ -137,9 +425,9 @@ module.exports = class CampaignLevelView extends CocoView
getLevelHelps: (startDay, endDay, doneCallback) ->
success = (data) =>
return doneCallback() if @destroyed
@analytics.levelHelps.levels = data.sort (a, b) -> if a.day < b.day then 1 else -1
# console.log 'getLevelHelps', data
@analytics.levelHelps.data = data.sort (a, b) -> if a.day < b.day then -1 else 1
doneCallback()
request = @supermodel.addRequestResource 'level_helps', {
url: '/db/analytics_perday/-/level_helps'
data: {startDay: startDay, endDay: endDay, slugs: [@levelSlug]}
@ -151,9 +439,9 @@ module.exports = class CampaignLevelView extends CocoView
getLevelPlaytimes: (startDay, endDay, doneCallback) ->
success = (data) =>
return doneCallback() if @destroyed
@analytics.levelPlaytimes.levels = data.sort (a, b) -> if a.created < b.created then 1 else -1
# console.log 'getLevelPlaytimes', data
@analytics.levelPlaytimes.data = data.sort (a, b) -> if a.created < b.created then -1 else 1
doneCallback()
request = @supermodel.addRequestResource 'playtime_averages', {
url: '/db/level/-/playtime_averages'
data: {startDay: startDay, endDay: endDay, slugs: [@levelSlug]}
@ -164,17 +452,15 @@ module.exports = class CampaignLevelView extends CocoView
getRecentSessions: (doneCallback) ->
limit = 100
success = (data) =>
return doneCallback() if @destroyed
@analytics.recentSessions.levels = data
# console.log 'getRecentSessions', data
@analytics.recentSessions.data = data
doneCallback()
# TODO: Why do we need this url dash?
request = @supermodel.addRequestResource 'level_sessions_recent', {
url: "/db/level_session/-/recent"
data: {slug: @levelSlug, limit: limit}
method: 'POST'
success: success
}, 0
request.load()
request.load()

View file

@ -343,7 +343,7 @@ module.exports = class ThangComponentsEditView extends CocoView
componentSystems = (c.get('system') for c in componentModels when c)
for system in componentSystems
if system not in extantSystems
if system isnt 'misc' and system not in extantSystems
s = "Component requires system <strong>#{system}</strong> which is currently not included in this level."
noty({
text: s,

View file

@ -7,6 +7,6 @@ sleep 5
cd $_scriptDir
cd ../
until node_modules/karma/bin/karma start; do
echo "Karma crashed with exit code $?. Respawning.." >&2
echo "Karma crashed with exit code $?. Respawning..." >&2
sleep 1
done

View file

@ -126,7 +126,7 @@ class AnalyticsPerDayHandler extends Handler
levelStringIDSlugMap[doc._id] = doc.v for doc in documents
getCompletions orderedLevelSlugs, levelStringIDSlugMap
# 1. Get campaign levels
# 1. Get campaign levels
Campaign.find({slug: campaignSlug}).exec (err, documents) =>
if err? then return @sendDatabaseError res, err
campaignLevels = []
@ -250,6 +250,8 @@ class AnalyticsPerDayHandler extends Handler
completions = []
for day of dayEventCounts
started = 0
finished = 0
for eventID of dayEventCounts[day]
eventID = parseInt eventID
started = dayEventCounts[day][eventID] if eventID is startEventID
@ -322,6 +324,9 @@ class AnalyticsPerDayHandler extends Handler
helps = []
for levelID of levelEventCounts
for day of levelEventCounts[levelID]
alertHelps = 0
paletteHelps = 0
videoStarts = 0
for eventID of levelEventCounts[levelID][day]
alertHelps = levelEventCounts[levelID][day][eventID] if parseInt(eventID) is alertHelpEventID
paletteHelps = levelEventCounts[levelID][day][eventID] if parseInt(eventID) is palettteHelpEventID
@ -329,9 +334,9 @@ class AnalyticsPerDayHandler extends Handler
helps.push
level: levelStringIDSlugMap[levelID]
day: day
alertHelps: alertHelps ? 0
paletteHelps: paletteHelps ? 0
videoStarts: videoStarts ? 0
alertHelps: alertHelps
paletteHelps: paletteHelps
videoStarts: videoStarts
@levelHelpsCache[cacheKey] = helps
@sendSuccess res, helps
@ -394,13 +399,15 @@ class AnalyticsPerDayHandler extends Handler
subscriptions = []
for levelID of levelEventCounts
subsShown = 0
subsPurchased = 0
for eventID of levelEventCounts[levelID]
subsShown = levelEventCounts[levelID][eventID] if parseInt(eventID) is showSubEventID
subsPurchased = levelEventCounts[levelID][eventID] if parseInt(eventID) is finishSubEventID
subscriptions.push
level: levelStringIDSlugMap[levelID]
shown: subsShown ? 0
purchased: subsPurchased ? 0
shown: subsShown
purchased: subsPurchased
@levelSubscriptionsCache[cacheKey] = subscriptions
@sendSuccess res, subscriptions

View file

@ -358,6 +358,8 @@ LevelHandler = class LevelHandler extends Handler
# TODO: An uncached call takes about 5s for dungeons-of-kithgard locally
# TODO: This is very similar to getLevelCompletionsBySlugs(), time to generalize analytics APIs?
# TODO: exclude admin data
levelSlugs = req.query.slugs or req.body.slugs
startDay = req.query.startDay or req.body.startDay
endDay = req.query.endDay or req.body.endDay

View file

@ -23,7 +23,7 @@ LevelSessionSchema.post 'init', (doc) ->
LevelSessionSchema.pre 'save', (next) ->
User = require '../../users/User' # Avoid mutual inclusion cycles
@set('changed', new Date().toISOString())
@set('changed', new Date())
id = @get('id')
initd = id of previous