mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-02-17 17:02:18 -05:00
Merge branch 'master' into production
This commit is contained in:
commit
814ea967be
17 changed files with 501 additions and 159 deletions
|
@ -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).
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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'
|
||||
]
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
.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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue