Merge remote-tracking branch 'codecombat/master' into flip-hero-logic

This commit is contained in:
Cat Sync 2016-05-13 13:32:46 -04:00
commit 59408c0de2
36 changed files with 1091 additions and 559 deletions

65
Vagrantfile vendored
View file

@ -1,32 +1,33 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Original content copyright (c) 2014 dpen2000 licensed under the MIT license
VAGRANTFILE_API_VERSION = "2"
Vagrant.require_version ">= 1.5.0"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
# Ubuntu 14.04 compatible with both VirtualBox and VMWare Fusion
# see https://github.com/phusion/open-vagrant-boxes#readme
config.vm.box = "phusion/ubuntu-14.04-amd64"
config.vm.hostname = "coco-dev"
config.vm.network "forwarded_port", guest: 3000, host: 3000
config.vm.network "forwarded_port", guest: 9485, host: 9485
config.vm.provision "shell", path: "scripts/vagrant/provision.sh", privileged: false
config.vm.provider "virtualbox" do |v|
v.memory = 2048
v.cpus = 2
end
config.vm.provider "vmware_fusion" do |v|
v.vmx["memsize"] = "2048"
v.vmx["numvcpus"] = 2
end
end
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Original content copyright (c) 2014 dpen2000 licensed under the MIT license
VAGRANTFILE_API_VERSION = "2"
Vagrant.require_version ">= 1.5.0"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
# Ubuntu 14.04 compatible with both VirtualBox and VMWare Fusion
# see https://github.com/phusion/open-vagrant-boxes#readme
config.vm.box = "phusion/ubuntu-14.04-amd64"
config.vm.hostname = "coco-dev"
config.vm.network "forwarded_port", guest: 3000, host: 13000
config.vm.network "forwarded_port", guest: 9485, host: 19485
config.vm.provision "shell", path: "scripts/vagrant/provision.sh", privileged: false
config.vm.provider "virtualbox" do |v|
v.memory = 2048
v.cpus = 2
#v.gui = true
end
config.vm.provider "vmware_fusion" do |v|
v.vmx["memsize"] = "2048"
v.vmx["numvcpus"] = 2
end
end

View file

@ -37,6 +37,7 @@ module.exports = class CocoRouter extends Backbone.Router
'admin/level-sessions': go('admin/LevelSessionsView')
'admin/users': go('admin/UsersView')
'admin/base': go('admin/BaseView')
'admin/demo-requests': go('admin/DemoRequestsView')
'admin/trial-requests': go('admin/TrialRequestsView')
'admin/user-code-problems': go('admin/UserCodeProblemsView')
'admin/pending-patches': go('admin/PendingPatchesView')

View file

@ -8,12 +8,12 @@ module.exports.createAetherOptions = (options) ->
throw new Error 'Specify a code language to create an Aether instance' unless options.codeLanguage
useInterpreter = options.useInterpreter
defaultToEsper = switch options.codeLanguage
when 'python' then me.level() < 15 # Esper currently works well until using range()
when 'javascript' then me.level() < 22 # Esper currently works well until using hero.myFn = function() pattern
when 'lua' then me.level() < 10 # Functions don't work in Esper yet, can't play forest function levels
when 'coffeescript' then false # CoffeeScript has a toNative error if it ever finishes plan(), and also @fn = -> pattern doesn't work
when 'clojure' then false # No Clojure support
defaultToEsper = true #switch options.codeLanguage
# when 'python' then me.level() < 15 # Esper currently works well until using range()
# when 'javascript' then me.level() < 22 # Esper currently works well until using hero.myFn = function() pattern
# when 'lua' then me.level() < 10 # Functions don't work in Esper yet, can't play forest function levels
# when 'coffeescript' then false # CoffeeScript has a toNative error if it ever finishes plan(), and also @fn = -> pattern doesn't work
# when 'clojure' then false # No Clojure support
useInterpreter ?= !!utils.getQueryVariable 'esper', defaultToEsper
aetherOptions =
functionName: options.functionName

View file

@ -1,4 +1,4 @@
module.exports = nativeDescription: "English", englishDescription: "English", translation:
module.exports = nativeDescription: "English", englishDescription: "English", translation:
home:
slogan: "Learn to Code by Playing a Game"
no_ie: "CodeCombat does not run in Internet Explorer 8 or older. Sorry!" # Warning that only shows up in IE8 and older
@ -57,15 +57,11 @@
real_game: "A real game, played with real coding."
great_game: "A great game is more than just badges and achievements - its about a players journey, well-designed puzzles, and the ability to tackle challenges with agency and confidence."
agency: "CodeCombat is a game that gives players that agency and confidence with our robust typed code engine, which helps beginner and advanced students alike write proper, valid code."
curious: "Curious? Request a demo and we'll show you the ropes"
request_demo_title: "Get your students started today!"
request_demo_subtitle: "Request a demo and get your students started in less than an hour."
get_started_title: "Set up your class today"
get_started_subtitle: "Set up a class, add your students, and monitor their progress as they learn computer science."
create_class: "Or create a class and see it for yourself!"
teacher_screenshots_hint: "Students write code and see their changes update in real-time"
request_demo: "Request a Demo"
create_a_class: "Create a Class"
setup_a_class: "Set Up a Class"
have_an_account: "Have an account?"
logged_in_as: "You are currently logged in as"
@ -91,10 +87,9 @@
start_playing_for_free: "Start Playing for Free!"
students_and_players: "Students & Players"
goto_classes: "Go to My Classes"
educator_wiki: "Educator wiki"
view_profile: "View My Profile"
view_progress: "View Progress"
check_out_wiki: "Check out our new educator Wiki"
check_out_wiki: "Check out our new Educator Wiki"
want_coco: "Want CodeCombat at your school?"
form_select_role: "Select primary role"
form_select_range: "Select class size"
@ -190,20 +185,52 @@
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: "elif"
elif: "else if"
while: "while"
loop: "loop"
for: "for"
break: "break"
continue: "continue"
pass: "pass"
return: "return"
then: "then"
do: "do"
end: "end"
function: "function"
def: "def" # (short for "define")
def: "define"
var: "variable"
self: "self"
hero: "hero"
this: "this"
or: "or"
"||": "or"
and: "and"
"&&": "and"
not: "not"
"!": "not"
"=": "assign" # For this section, conjugate it like it's the verb part of a sentence when possible
"==": "equals"
"===": "strictly equals"
"!=": "does not equal"
"!==": "does not strictly equal"
">": "is greater than"
">=": "is greater than or equal"
"<": "is less than"
"<=": "is less than or equal"
"*": "multiplied by"
"/": "divided by"
"+": "plus"
"-": "minus"
"+=": "add and assign"
"-=": "subtract and assign"
True: "True"
true: "true"
False: "False"
false: "false"
undefined: "undefined"
null: "null"
nil: "nil"
None: "None"
share_progress_modal:
blurb: "Youre making great progress! Tell your parent how much you've learned with CodeCombat."
@ -755,9 +782,6 @@
jobs_title: "Come work with us and help write CodeCombat history!"
jobs_subtitle: """Don't see a good fit but interested in keeping in touch? See our "Create Your Own" listing."""
jobs_benefits: "Employee Benefits"
jobs_benefit_1: "Competitive salary and options"
jobs_benefit_2: "15 day minimum vacation policy, excluding company holidays"
jobs_benefit_3: "Flex time and flexible work-from-home"
jobs_benefit_4: "Unlimited vacation"
jobs_benefit_5: "Professional development and continuing education support free books and games!"
jobs_benefit_6: "Medical (gold), dental, vision"

View file

@ -14,90 +14,90 @@ module.exports = nativeDescription: "Nederlands (België)", englishDescription:
for_developers: "Voor ontwikkelaars" # Not currently shown on home page.
or_ipad: "Of download voor iPad"
# new_home:
# slogan: "The most engaging game for learning programming."
# classroom_edition: "Classroom Edition:"
# learn_to_code: "Learn to code:"
# teacher: "Teacher"
# student: "Student"
# play_now: "Play Now"
# im_a_teacher: "I'm a Teacher"
# im_a_student: "I'm a Student"
# learn_more: "Learn more"
# classroom_in_a_box: "A classroom in-a-box for teaching computer science."
# codecombat_is: "CodeCombat is a platform <strong>for students</strong> to learn computer science while playing through a real game."
# our_courses: "Our courses have been specifically playtested to <strong>excel in the classroom</strong>, even by teachers with little to no prior programming experience."
# top_screenshots_hint: "Students write code and see their changes update in real-time"
# designed_with: "Designed with teachers in mind"
# real_code: "Real, typed code"
# from_the_first_level: "from the first level"
# getting_students: "Getting students to typed code as quickly as possible is critical to learning programming syntax and proper structure."
# educator_resources: "Educator resources"
# course_guides: "and course guides"
# teaching_computer_science: "Teaching computer science does not require a costly degree, because we provide tools to support educators of all backgrounds."
# accessible_to: "Accessible to"
# everyone: "everyone"
# democratizing: "Democratizing the process of learning coding is at the core of our philosophy. Everyone should be able to learn to code."
# forgot_learning: "I think they actually forgot that they were actually learning something."
# wanted_to_do: " Coding is something I've always wanted to do, and I never thought I would be able to learn it in school."
# why_games: "Why is learning through games important?"
# games_reward: "Games reward the productive struggle."
# encourage: "Gaming is a medium that encourages interaction, discovery, and trial-and-error. A good game challenges the player to master skills over time, which is the same critical process students go through as they learn."
# excel: "Games excel at rewarding"
# struggle: "productive struggle"
# kind_of_struggle: "the kind of struggle that results in learning thats engaging and"
# motivating: "motivating"
# not_tedious: "not tedious."
# gaming_is_good: "Studies suggest gaming is good for childrens brains. (its true!)"
# game_based: "When game-based learning systems are"
# compared: "compared"
# conventional: "against conventional assessment methods, the difference is clear: games are better at helping students retain knowledge, concentrate and"
# perform_at_higher_level: "perform at a higher level of achievement"
# feedback: "Games also provide real-time feedback that allows students to adjust their solution path and understand concepts more holistically, instead of being limited to just “correct” or “incorrect” answers."
# real_game: "A real game, played with real coding."
# great_game: "A great game is more than just badges and achievements - its about a players journey, well-designed puzzles, and the ability to tackle challenges with agency and confidence."
# agency: "CodeCombat is a game that gives players that agency and confidence with our robust typed code engine, which helps beginner and advanced students alike write proper, valid code."
# curious: "Curious? Request a demo and we'll show you the ropes"
# request_demo_title: "Get your students started today!"
# request_demo_subtitle: "Request a demo and get your students started in less than an hour."
# get_started_title: "Set up your class today"
# get_started_subtitle: "Set up a class, add your students, and monitor their progress as they learn computer science."
# create_class: "Or create a class and see it for yourself!"
# teacher_screenshots_hint: "Students write code and see their changes update in real-time"
# request_demo: "Request a Demo"
# create_a_class: "Create a Class"
# setup_a_class: "Set Up a Class"
# have_an_account: "Have an account?"
# logged_in_as: "You are currently logged in as"
# view_my_classes: "View my classes"
# computer_science: "Computer science courses for all ages"
# show_me_lesson_time: "Show me lesson time estimates for:"
# curriculum: "Total curriculum hours:"
# ffa: "Free for all students"
# lesson_time: "Lesson time:"
# coming_soon: "Coming soon!"
# 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."
# run_class: "Everything you need to run a computer science class in your school today, no CS background required."
# teachers: "Teachers!"
# teachers_and_educators: "Teachers & Educators"
# class_in_box: "Learn how our classroom-in-a-box platform fits into your curriculum."
# get_started: "Get Started"
# students: "Students:"
# join_class: "Join Class"
# role: "Your role:"
# student_count: "Number of students:"
# start_playing_for_free: "Start Playing for Free!"
# students_and_players: "Students & Players"
# goto_classes: "Go to My Classes"
# educator_wiki: "Educator wiki"
# view_profile: "View My Profile"
# view_progress: "View Progress"
# check_out_wiki: "Check out our new educator Wiki"
# want_coco: "Want CodeCombat at your school?"
# form_select_role: "Select primary role"
# form_select_range: "Select class size"
new_home:
slogan: "Het meest uitdagende spel om mee te leren programmeren."
classroom_edition: "Klas versie:"
learn_to_code: "Leer programmeren:"
teacher: "Leraar"
student: "Leerling"
play_now: "Speel"
im_a_teacher: "Ik ben een leraar"
im_a_student: "Ik ben een leerling"
learn_more: "Lees verder"
classroom_in_a_box: "Een kant-en-klare digitale klas voor programmeerlessen."
codecombat_is: "CodeCombat is een platform waarmee leerlingen leren programmeren door het spelen van een spel." # {change}
our_courses: "Onze lessen zijn specifiek ontwikkeld voor een klasomgeving, zelfs voor leraren zonder programmeerervaring." # {change}
top_screenshots_hint: "Leerlingen schrijven code en zien direct het resultaat van de verandering."
designed_with: "Gemaakt voor leraren"
real_code: "Echte, getypte code"
from_the_first_level: "vanaf het eerste level"
getting_students: "Leerlingen zo snel mogelijk echte code laten schrijven is noodzakelijk voor het leren van programmeer syntax en correcte structuur."
educator_resources: "Lesbrieven voor docenten"
course_guides: "en ondersteuningsmateriaal"
teaching_computer_science: "Je hebt geen informatica diploma nodig om te kunnen programmeren, wij verschaffen de materialen waarmee elke docent programmeerles kan geven."
accessible_to: "Bereikbaar voor"
everyone: "iedereen"
democratizing: "Programmeerles toegankelijk maken is onze filosofie. Iedereen moet de kans krijgen om te leren programmeren."
forgot_learning: "Volgens mij hadden ze niet meer door dat ze eigenlijk bezig waren met leren."
wanted_to_do: " Ik wilde altijd al leren programmeren, maar op school was hier nooit aandacht voor."
why_games: "Waarom is spelenderwijs leren belangrijk?"
games_reward: "Games vergroten de productiviteit."
encourage: "Gaming is een middel dat interactie, nieuwschierigheid, en trial-and-error aanmoedigt. Een goed spel daagt de speler uit zijn vaardigheden te perfectioneren, wat hetzelfde noodzakelijke proces is waar leerlingen doorheen gaan wanneer zij iets leren."
excel: "Games helpen bij de"
struggle: "productiviteit-strijd"
kind_of_struggle: "het soort worsteling dat uitmondt in een leerproces dat uitdagend is en "
motivating: "motiveert"
not_tedious: "niet vervelend."
gaming_is_good: "Studies geven aan dat speels leren goed is voor de hersenen van kinderen. (En dat klopt!)"
game_based: "Wanneer spel-gebaseerde leersystemen worden"
compared: "vergeleken"
conventional: "met conventionele assessment methodes, is het verschil duidelijk: games zijn beter in het stimuleren van leerlingen bij het onthouden van kennis, concentratie en het"
perform_at_higher_level: "niveau van hun prestaties."
feedback: "Games verschaffen directe feedback, wat leerlingen in staat stelt hun oplossingen te verbeteren en zij een holistisch begrip van de concepten krijgen, in plaats van beperkt zijn tot antwoorden als “correct” of “incorrect”."
real_game: "Een echt spel, wat je speelt met echte programmeertaal."
great_game: "Een goed spel is meer dan alleen medailles en prestaties - het gaat om een reis, nauwkeurig ontworpen puzzels, en de mogelijkheid om uitdagingen vol zelfvertrouwen aan te pakken."
agency: "CodeCombat is een game die de mogelijkheid en het zelfvertrouwen geeft om met echte code te werken, wat zowel beginners als gevorderde helpt bij het schrijven van goede, valide code."
curious: "Nieuwsgierig? Vraag een demo aan en we laten je zien hoe het werkt."
request_demo_title: "Laat je leerlingen vandaag nog starten!"
request_demo_subtitle: "Vraag een demo aan en start binnen een uur met programmeerlessen."
get_started_title: "Maak vandaag nog een klas aan!"
get_started_subtitle: "Maak een klas aan, voeg je leerlingen toe, en monitor hun vooruitgang."
create_class: "Of maak een klas aan en ervaar het zelf!"
teacher_screenshots_hint: "Kinderen schrijven code en zien hun code live uitgevoerd worden."
request_demo: "Vraag een demo aan"
create_a_class: "Maak een klas aan"
setup_a_class: "Maak een klas aan"
have_an_account: "Heb je al een account?" # {change}
logged_in_as: "Je bent ingelogd als"
view_my_classes: "Bekijk mijn klassen"
computer_science: "Informatica lessen voor alle leeftijden"
show_me_lesson_time: "Geef geschatte lesduur weer:"
curriculum: "Totaal aantal lesuren:"
ffa: "Gratis voor alle leerlingen"
lesson_time: "Lesduur:"
coming_soon: "Binnenkort beschikbaar!"
courses_available_in: "Lessen zijn beschikbaar in JavaScript, Python, en Java (Java is binnenkort beschikbaar!)"
boast: "Uitdagende raadsels die zowel gamers als fanatieke programmeurs weten te prikkelen."
winning: "Een gouden combinatie van spel-elementen en programmeerhuiswerk, dat samen zorgt voor kind-vriendelijk en oprecht aangenaam onderwijs."
run_class: "Alles wat je nodig hebt om vandaag nog programmeerlessen in jouw klas te geven, geen voorkennis vereist."
teachers: "Docenten!"
teachers_and_educators: "Docenten & Mentoren"
class_in_box: "Lees hoe ons digitale lesplatform in uw curriculum past."
get_started: "Start nu"
students: "Leerlingen:"
join_class: "Inschrijven bij Klas"
role: "Uw rol:"
student_count: "Aantal leerlingen:"
start_playing_for_free: "Begin met gratis spelen!"
students_and_players: "Leerlingen & Spelers"
goto_classes: "Ga naar mijn klassen"
educator_wiki: "Leraren wiki"
view_profile: "Mijn Profiel"
view_progress: "Bekijk voortgang"
check_out_wiki: "Bekijk onze nieuwe leraren Wiki"
want_coco: "Wil je CodeCombat op jouw school?"
form_select_role: "Selecteer je rol"
form_select_range: "Selecteer klassengrootte"
nav:
play: "Levels" # The top nav bar entry where players choose which levels to play
@ -113,28 +113,28 @@ module.exports = nativeDescription: "Nederlands (België)", englishDescription:
code: "Code"
home: "Home"
contribute: "Bijdragen"
legal: "Legaal"
legal: "Juridisch"
about: "Over Ons"
contact: "Contact"
twitter_follow: "Volgen"
# students: "Students"
students: "Leerlingen"
teachers: "Docenten"
careers: "Carrière"
# facebook: "Facebook"
# twitter: "Twitter"
# create_a_class: "Create a Class"
# other: "Other"
# learn_to_code: "Learn to Code!"
# toggle_nav: "Toggle navigation"
# jobs: "Jobs"
# schools: "Schools"
# educator_wiki: "Educator Wiki"
# get_involved: "Get Involved"
# open_source: "Open source (GitHub)"
# support: "Support"
# faqs: "FAQs"
# help_pref: "Need help? Email"
# help_suff: "and we'll get in touch!"
careers: "Banen"
facebook: "Facebook"
twitter: "Twitter"
create_a_class: "Maak een Klas"
other: "Andere"
learn_to_code: "Leer te programmeren!"
toggle_nav: "Toggle navigatie"
jobs: "Banen"
schools: "Scholen"
educator_wiki: "Leraren Wiki"
get_involved: "Help Mee"
open_source: "Open source (GitHub)"
support: "Hulp / ondersteuning"
faqs: "FAQs"
help_pref: "Hulp nodig? E-mail ons"
help_suff: "en we nemen contact op!"
modal:
close: "Sluiten"
@ -146,22 +146,22 @@ module.exports = nativeDescription: "Nederlands (België)", englishDescription:
diplomat_suggestion:
title: "Help CodeCombat vertalen!" # This shows up when a player switches to a non-English language using the language selector.
sub_heading: "We hebben je taalvaardigheden nodig."
pitch_body: "We ontwikkelen CodeCombat in het Engels, maar we hebben al spelers van over de hele wereld. Veel van hen willen in het Nederlands spelen, maar kunnen geen Engels. Dus als je beiden spreekt, overweeg a.u.b. om je aan te melden als Diplomaat en help zowel de CodeCombat website als alle levels te vertalen naar het Nederlands."
pitch_body: "We ontwikkelen CodeCombat in het Engels, maar we hebben al spelers van over de hele wereld. Veel van hen willen in het Nederlands spelen, maar kunnen geen Engels. Dus als je beiden talen spreekt, overweeg a.u.b. om je aan te melden als Diplomaat en help zowel de CodeCombat website als alle levels te vertalen naar het Nederlands."
missing_translations: "Totdat we alles hebben vertaald naar het Nederlands zul je Engels zien waar Nederlands niet beschikbaar is."
learn_more: "Meer informatie over het zijn van een Diplomaat"
subscribe_as_diplomat: "Abonneren als Diplomaat"
play:
play_as: "Speel als " # Ladder page
compete: "Strijd!" # Course details page
compete: "Compleet!" # Course details page
spectate: "Toeschouwen" # Ladder page
players: "Spelers" # Hover over a level on /play
hours_played: "Gespeelde uren" # Hover over a level on /play
hours_played: "Speeltijd" # Hover over a level on /play
items: "Items" # Tooltip on item shop button from /play
unlock: "Ontsluit" # For purchasing items and heroes
confirm: "Bevestig"
unlock: "Koop" # For purchasing items and heroes
confirm: "Bevestigen"
owned: "In bezit" # For items you own
locked: "Gesloten"
locked: "Vergrendeld"
purchasable: "Te koop" # For a hero you unlocked but haven't purchased
available: "Beschikbaar"
skills_granted: "Verleende vaardigheden" # Property documentation details
@ -171,46 +171,46 @@ module.exports = nativeDescription: "Nederlands (België)", englishDescription:
settings: "Instellingen" # Tooltip on settings button from /play
poll: "Stemming" # Tooltip on poll button from /play
next: "Volgende" # Go from choose hero to choose inventory before playing a level
change_hero: "Held wisselen" # Go back from choose inventory to choose hero
buy_gems: "Koop edelstenen"
subscription_required: "Abbonement vereist"
anonymous: "Anonieme speler"
change_hero: "Verander held" # Go back from choose inventory to choose hero
buy_gems: "Edelstenen kopen"
subscription_required: "Abonnement nodig"
anonymous: "Anonieme Speler"
level_difficulty: "Moeilijkheidsgraad: "
play_classroom_version: "Speel de klassikale versie" # Choose a level in campaign version that you also can play in one of your courses
campaign_beginner: "Beginnercampagne"
awaiting_levels_adventurer_prefix: "Iedere week komen er nieuwe levels bij."
awaiting_levels_adventurer: "Registreer je als Avonturier"
awaiting_levels_adventurer_suffix: "en wees de eerste om de nieuwe levels te spelen."
campaign_beginner: "Beginnerscampagne"
awaiting_levels_adventurer_prefix: "We brengen 5 nieuwe levels per week uit." # {change}
awaiting_levels_adventurer: "Schrijf je in als Avonturier"
awaiting_levels_adventurer_suffix: "om de eerste te zijn die nieuwe levels speelt."
adjust_volume: "Volume aanpassen"
campaign_multiplayer: "Multiplayer Arena's"
campaign_multiplayer_description: "... waarin je direct tegen andere spelers speelt."
campaign_old_multiplayer: "(Afgekeurd) Oude multiplayer Arenas"
campaign_old_multiplayer_description: "Overblijfselen van een meer geciviliseerde tijd. Er zijn geen simulaties voor deze oudere, heldenloze multiplayer arenas."
campaign_old_multiplayer: "(Verouderde) Oude Multiplayer Arenas"
campaign_old_multiplayer_description: "Antieke overblijvselen van een beschaafder tijdperk. Deze oudere held-loze multiplayer arenas worden niet gesimuleert."
# 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: "elif"
# while: "while"
# loop: "loop"
# for: "for"
# break: "break"
# continue: "continue"
# then: "then"
# do: "do"
# end: "end"
# function: "function"
# def: "def"
# self: "self"
# hero: "hero"
# this: "this"
code:
if: "als" # 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: "anders"
elif: "anders als"
while: "wanneer"
loop: "lus"
for: "voor"
break: "breek"
continue: "doorgaan"
then: "daarna"
do: "doe"
end: "einde"
function: "functie"
def: "def"
self: "zelf"
hero: "held"
this: "dit"
share_progress_modal:
blurb: "Je gaat snel vooruit! Vertel aan je ouders hoeveel je met CodeCombat geleerd hebt."
blurb: "Je gaat snel vooruit! Vertel aan je ouders hoeveel je geleerd hebt met CodeCombat."
email_invalid: "Emailaddress klopt niet."
form_blurb: "Vul het emailadres van je ouders hieronder in en we zullen het ze laten zien!"
form_label: "Emailaddress"
placeholder: "emailaddress"
form_label: "E-mailaddress"
placeholder: "e-mailaddress"
title: "Goed werk, leerling"
login:
@ -236,15 +236,15 @@ module.exports = nativeDescription: "Nederlands (België)", englishDescription:
school_name: "Schoolnaam en stad"
optional: "optioneel"
school_name_placeholder: "Sint-Jan Berchmanscollege, Brussel"
# or_sign_up_with: "or sign up with"
# connected_gplus_header: "You've successfully connected with Google+!"
# connected_gplus_p: "Finish signing up so you can log in with your Google+ account."
# gplus_exists: "You already have an account associated with Google+!"
# connected_facebook_header: "You've successfully connected with Facebook!"
# connected_facebook_p: "Finish signing up so you can log in with your Facebook account."
# facebook_exists: "You already have an account associated with Facebook!"
# hey_students: "Students, enter the class code from your teacher."
# birthday: "Birthday"
or_sign_up_with: "of log in met"
connected_gplus_header: "Je bent ingelogd met Google+!"
connected_gplus_p: "Maak je inschrijving compleet zodat je in kan loggen met je Google+ account."
gplus_exists: "Jouw Google+ account is al gekoppeld!"
connected_facebook_header: "Je bent ingelogd met Facebook!"
connected_facebook_p: "Maak je inschrijving compleet zodat je in kan loggen met je Facebook account."
facebook_exists: "Jouw Facebook account is al gekoppeld!"
hey_students: "Leerlingen, voer hier de klassencode van je docent in."
birthday: "Verjaardag"
recover:
recover_account_title: "Herstel Account"
@ -314,7 +314,7 @@ module.exports = nativeDescription: "Nederlands (België)", englishDescription:
subject: "Onderwerp"
email: "Email"
password: "Wachtwoord"
# confirm_password: "Confirm Password"
confirm_password: "Bevestig wachtwoord"
message: "Bericht"
code: "Code"
ladder: "Ladder"
@ -333,9 +333,9 @@ module.exports = nativeDescription: "Nederlands (België)", englishDescription:
warrior: "Krijger"
ranger: "Boogschutter"
wizard: "Tovenaar"
# first_name: "First Name"
# last_name: "Last Name"
# username: "Username"
first_name: "Voornaam"
last_name: "Achternaam"
username: "Gebruikersnaam"
units:
second: "seconde"
@ -354,7 +354,7 @@ module.exports = nativeDescription: "Nederlands (België)", englishDescription:
years: "jaren"
play_level:
# level_complete: "Level Complete"
level_complete: "Level Voltooid"
completed_level: "Voltooid Level:"
course: "Les:"
done: "Klaar"

View file

@ -24,22 +24,22 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription:
im_a_teacher: "Sou um Professor"
im_a_student: "Sou um Estudante"
learn_more: "Saber mais"
# classroom_in_a_box: "A classroom in-a-box for teaching computer science."
# codecombat_is: "CodeCombat is a platform <strong>for students</strong> to learn computer science while playing through a real game."
# our_courses: "Our courses have been specifically playtested to <strong>excel in the classroom</strong>, even by teachers with little to no prior programming experience."
classroom_in_a_box: "Uma sala de aula num pacote para ensinar ciência da computação."
codecombat_is: "O CodeCombat é uma plataforma <strong>para estudantes</strong> para aprender ciência da computação enquanto se joga um jogo real."
our_courses: "Os nossos cursos foram especificamente testados para <strong>terem sucesso na sala de aula</strong>, até para professores com pouca ou nenhuma experiência anterior de programação."
top_screenshots_hint: "Os estudantes escrevem código e veem as alterações deles atualizarem em tempo real"
# designed_with: "Designed with teachers in mind"
# real_code: "Real, typed code"
# from_the_first_level: "from the first level"
designed_with: "Desenhado a pensar nos professores"
real_code: "Código real e escrito"
from_the_first_level: "desde o primeiro nível"
# getting_students: "Getting students to typed code as quickly as possible is critical to learning programming syntax and proper structure."
# educator_resources: "Educator resources"
# course_guides: "and course guides"
educator_resources: "Recursos para educadores"
course_guides: "e guias dos cursos"
# teaching_computer_science: "Teaching computer science does not require a costly degree, because we provide tools to support educators of all backgrounds."
# accessible_to: "Accessible to"
# everyone: "everyone"
accessible_to: "Acessível a"
everyone: "todos"
# democratizing: "Democratizing the process of learning coding is at the core of our philosophy. Everyone should be able to learn to code."
# forgot_learning: "I think they actually forgot that they were actually learning something."
# wanted_to_do: " Coding is something I've always wanted to do, and I never thought I would be able to learn it in school."
forgot_learning: "Acho que eles se esqueceram que estavam a aprender alguma coisa."
wanted_to_do: "Programar é algo que sempre quis fazer e nunca pensei que poderia aprender isso na escola."
why_games: "Porque é que aprender através de jogos é importante?"
# games_reward: "Games reward the productive struggle."
# encourage: "Gaming is a medium that encourages interaction, discovery, and trial-and-error. A good game challenges the player to master skills over time, which is the same critical process students go through as they learn."
@ -68,18 +68,18 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription:
# create_a_class: "Create a Class"
# setup_a_class: "Set Up a Class"
have_an_account: "Tens uma conta?"
# logged_in_as: "You are currently logged in as"
logged_in_as: "Atualmente tens sessão iniciada como"
# view_my_classes: "View my classes"
# computer_science: "Computer science courses for all ages"
# show_me_lesson_time: "Show me lesson time estimates for:"
# curriculum: "Total curriculum hours:"
# ffa: "Free for all students"
# lesson_time: "Lesson time:"
# coming_soon: "Coming soon!"
# courses_available_in: "Courses are available in JavaScript, Python, and Java (coming soon!)"
computer_science: "Cursos de ciência da computação para todas as idades"
show_me_lesson_time: "Mostrar estimativas do tempo de aula para:"
curriculum: "Horas totais do currículo:"
ffa: "Grátis para todos os estudantes"
lesson_time: "Tempo de aula:"
coming_soon: "Brevemente!"
courses_available_in: "Os cursos estão disponíveis em JavaScript, Python, e Java (brevemente!)"
# 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."
# run_class: "Everything you need to run a computer science class in your school today, no CS background required."
run_class: "Tudo o que precisas para teres uma turma de ciência da computação na tua escola hoje, sem serem necessárias bases de CC."
# teachers: "Teachers!"
# teachers_and_educators: "Teachers & Educators"
# class_in_box: "Learn how our classroom-in-a-box platform fits into your curriculum."
@ -93,7 +93,7 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription:
# goto_classes: "Go to My Classes"
# educator_wiki: "Educator wiki"
# view_profile: "View My Profile"
# view_progress: "View Progress"
view_progress: "Ver Progresso"
# check_out_wiki: "Check out our new educator Wiki"
# want_coco: "Want CodeCombat at your school?"
# form_select_role: "Select primary role"
@ -107,7 +107,7 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription:
blog: "Blog"
forum: "Fórum"
account: "Conta"
# my_account: "My Account"
my_account: "A Minha Conta"
profile: "Perfil"
stats: "Estatísticas"
code: "Código"
@ -621,7 +621,7 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription:
clojure_blurb: "Um Lisp moderno."
lua_blurb: "Linguagem para scripts de jogos."
io_blurb: "Simples mas obscuro."
# java_blurb: "(Subscriber Only) Android and enterprise."
java_blurb: "(Apenas para Subscritores) Android e empresas."
status: "Estado"
hero_type: "Tipo"
weapons: "Armas"
@ -1159,9 +1159,9 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription:
# last_level: "Last Level"
# welcome_to_hoc: "Adventurers, welcome to our Hour of Code!"
# logged_in_as: "Logged in as:"
# not_you: "Not you?"
not_you: "Não és tu?"
# welcome_back: "Hi adventurer, welcome back!"
# continue_playing: "Continue Playing"
continue_playing: "Continuar a Jogar"
# more_options: "More options:"
# option1_header: "Option 1: Invite students via email"
# 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."

View file

@ -14,24 +14,24 @@ module.exports = nativeDescription: "Українська", englishDescription:
for_developers: "Для розробників" # Not currently shown on home page.
or_ipad: "Або завантажте на iPad"
# new_home:
# slogan: "The most engaging game for learning programming."
# classroom_edition: "Classroom Edition:"
# learn_to_code: "Learn to code:"
# teacher: "Teacher"
# student: "Student"
# play_now: "Play Now"
# im_a_teacher: "I'm a Teacher"
# im_a_student: "I'm a Student"
# learn_more: "Learn more"
new_home:
slogan: "Найбільш захоплююча гра для вивчення програмування." # The most engaging game for learning programming
classroom_edition: "Класна версія:" # Classroom Edition:
learn_to_code: "Вчитися кодувати:" # Learn to code:
teacher: "Вчитель" # Teacher
student: "Учень" # Student
play_now: "Грати Зараз" # Play Now
im_a_teacher: "Я Вчитель" # I'm a Teacher
im_a_student: "Я Учень" # I'm a Student
learn_more: "Дізнатися більше" # Learn more
# classroom_in_a_box: "A classroom in-a-box for teaching computer science."
# codecombat_is: "CodeCombat is a platform <strong>for students</strong> to learn computer science while playing through a real game."
# our_courses: "Our courses have been specifically playtested to <strong>excel in the classroom</strong>, even by teachers with little to no prior programming experience."
# top_screenshots_hint: "Students write code and see their changes update in real-time"
# designed_with: "Designed with teachers in mind"
# real_code: "Real, typed code"
# from_the_first_level: "from the first level"
# getting_students: "Getting students to typed code as quickly as possible is critical to learning programming syntax and proper structure."
codecombat_is: "CodeCombat - це платформа <strong>для учнів</strong>, створена, щоб опановувати комп'ютерні науки під час захоплюючої гри." # CodeCombat is a platform <strong>for students</strong> to learn computer science while playing through a real game.
our_courses: "Наша програма була протестована, щоб бути <strong>ефективною в класі</strong>, навіть для вчителів з мінімальним досвідом програмування." # Our courses have been specifically playtested to <strong>excel in the classroom</strong>, even by teachers with little to no prior programming experience.
top_screenshots_hint: "Учні пишуть код і бачать усі зміни наживо" # Students write code and see their changes update in real-time
designed_with: "Зважаючи на потреби вчителя" # Designed with teachers in mind
real_code: "Кодування" # Real, typed code
from_the_first_level: "з першого рівня" # from the first level
getting_students: "Написання коду самостійно сприяє вивченню синтаксису та структури програми." # Getting students to typed code as quickly as possible is critical to learning programming syntax and proper structure.
# educator_resources: "Educator resources"
# course_guides: "and course guides"
# teaching_computer_science: "Teaching computer science does not require a costly degree, because we provide tools to support educators of all backgrounds."

View file

@ -55,9 +55,9 @@ _.extend LevelSessionSchema.properties,
title: 'Changed'
readOnly: true
dateFirstCompleted: c.stringDate
title: 'Completed'
readOnly: true
dateFirstCompleted: {} # c.stringDate
# title: 'Completed'
# readOnly: true
team: c.shortString()
level: LevelSessionLevelSchema

View file

@ -0,0 +1,23 @@
#admin-demo-requests-view
#site-content-area
width: 100%
td
max-width: 120px
overflow: hidden
.btn-deny
float: right
.status-cell
width: 120px
td.created
min-width: 90px
td.reviewed
min-width: 90px
th.number
max-width: 50px

View file

@ -1,2 +0,0 @@
#blog-view
@import "bootstrap/variables"

View file

@ -1,2 +0,0 @@
#contact-view
@import "bootstrap/variables"

View file

@ -60,10 +60,8 @@
a[disabled] .level
opacity: 0.7
a.complete .level-difficulty:after
content: " - Complete!"
a .level-difficulty .level-status-complete
color: $yellow
a.started .level-difficulty:after
content: " - Started"
a .level-difficulty .level-status-started
color: desaturate($yellow, 50%)

View file

@ -39,8 +39,17 @@ block content
li
a(href="/admin/users") Users
h4 Other
if me.isAdmin()
h4 Analytics
ul
li
a(href="/admin/analytics") Dashboard
li
a(href="/admin/analytics/subscriptions") Subscriptions
li
a(href="/admin/demo-requests") Teacher Demo Requests
h4 Other
ul
li
a(href="/admin/base") Base (for debugging base.jade)
@ -48,15 +57,7 @@ block content
a(href="/admin/clas") CLAs
li
a(href="/admin/pending-patches") Patches
if me.isAdmin()
li
a(href="/admin/analytics") Analytics
ul
li
a(href="/admin/analytics/subscriptions") Subscriptions
li
a(href="/admin/design-elements") Design Elements
if me.isAdmin()
hr
h3 Prepaids

View file

@ -0,0 +1,54 @@
extends /templates/base
block content
if !me.isAdmin()
div You must be logged in as an admin to view this page.
else
h2 Teacher Demo Requests
if view.trialRequests.models.length < 1
h4 Fetching trial requests...
else
h3 Incoming Rate
table.table.table-condensed
thead
tr
th Day
th Count
th 7-day Average
tbody
each dayCount in view.dayCounts
tr
td= dayCount.day
td= dayCount.count
td= dayCount.sevenAverage
h3 Student Counts
table.table.table-condensed
thead
tr
th Created
th NCES District
th School Name
th.number NCES District Schools
th.number NCES District Students
th.number NCES School Students
th.number School Students
th.number Teacher Students
th Site Origin
tbody
each trialRequest in view.trialRequests.models
if trialRequest.get('type') !== 'course'
- continue;
tr
td.created= trialRequest.get('created').substring(0, 10)
td= trialRequest.get('properties').nces_district || ''
td= trialRequest.get('properties').organization || ''
td= trialRequest.get('properties').nces_district_schools || ''
td= trialRequest.get('properties').nces_district_students || ''
td= trialRequest.get('properties').nces_students || ''
td= trialRequest.get('properties').numStudentsTotal || ''
td= trialRequest.get('properties').numStudents || ''
td= trialRequest.get('properties').siteOrigin

View file

@ -80,7 +80,7 @@ mixin box
div
if view.isTeacherWithDemo
h6(data-i18n="new_home.check_out_wiki")
a.btn.btn-primary.btn-lg.btn-block(href="https://sites.google.com/a/codecombat.com/teacher-guides/course-guides", data-i18n="new_home.educator_wiki")
a.btn.btn-primary.btn-lg.btn-block(href="https://sites.google.com/a/codecombat.com/teacher-guides/course-guides", data-i18n="nav.educator_wiki")
else
h6(data-i18n="new_home.want_coco")
a.btn.btn-primary.btn-lg.btn-block(href=view.demoRequestURL, data-i18n="new_home.get_started")
@ -267,7 +267,7 @@ block content
.teacher-screenshots
.screenshots
.hidden-sm.hidden-md.hidden-lg
small(data-i18n="new_home.teacher_screenshots_hint")
small(data-i18n="new_home.top_screenshots_hint")
.screenshot-grid(title='Click to view full size')
a.screen-thumbnail(data-toggle="modal", data-target="#screenshot-lightbox", data-index='1')
img(src="/images/pages/about/forest.png")
@ -276,7 +276,7 @@ block content
a.screen-thumbnail(data-toggle="modal", data-target="#screenshot-lightbox", data-index='3')
img(src="/images/pages/about/glacier.png")
.clearfix.hidden-xs
small(data-i18n="new_home.teacher_screenshots_hint")
small(data-i18n="new_home.top_screenshots_hint")
if view.isTeacherWithDemo
h4(data-i18n="new_home.get_started_subtitle")

View file

@ -23,5 +23,11 @@ block content
if playCount
span.spl.spr - #{playCount.sessions}
span(data-i18n="play.players") players
if (view.levelStatusMap[level.id]=='complete')
span.spl.spr -
span(class="level-status-#{view.levelStatusMap[level.id]}", data-i18n="clans.complete_2")
else if (view.levelStatusMap[level.id]=='started')
span.spl.spr -
span(class="level-status-#{view.levelStatusMap[level.id]}", data-i18n="clans.started_2")
.play-text-container
.overlay-text.play-text(data-i18n="common.play") Play

View file

@ -0,0 +1,36 @@
RootView = require 'views/core/RootView'
template = require 'templates/admin/demo-requests'
CocoCollection = require 'collections/CocoCollection'
TrialRequest = require 'models/TrialRequest'
module.exports = class DemoRequestsView extends RootView
id: 'admin-demo-requests-view'
template: template
constructor: (options) ->
super options
return unless me.isAdmin()
@trialRequests = new CocoCollection([], { url: '/db/trial.request?conditions[sort]="-created"&conditions[limit]=5000', model: TrialRequest })
@supermodel.loadCollection(@trialRequests, 'trial-requests', {cache: false})
@dayCounts = []
onLoaded: ->
return super() unless me.isAdmin()
dayCountMap = {}
for trialRequest in @trialRequests.models
day = trialRequest.get('created').substring(0, 10)
dayCountMap[day] ?= 0
dayCountMap[day]++
@dayCounts = []
for day, count of dayCountMap
@dayCounts.push(day: day, count: count)
@dayCounts.sort((a, b) -> b.day.localeCompare(a.day))
sevenCounts = []
for i in [@dayCounts.length - 1..0]
dayCount = @dayCounts[i]
sevenCounts.push(dayCount.count)
while sevenCounts.length > 7
sevenCounts.shift()
if sevenCounts.length is 7
dayCount.sevenAverage = Math.round(sevenCounts.reduce(((a, b) -> a + b), 0) / 7)
super()

View file

@ -146,10 +146,11 @@ module.exports = class CreateAccountModal extends ModalView
window.tracker?.trackEvent 'Finished Signup', category: "Signup", label: 'CodeCombat'
if @classCode
url = "/courses?_cc="+@classCode
application.router.navigate(url)
window.location.reload()
location.href = url
else
window.location.reload()
# Google Plus
onClickGPlusSignupButton: ->

View file

@ -69,7 +69,7 @@ module.exports = class CoursesView extends RootView
application.tracker?.trackEvent 'Started Student Login', category: 'Courses'
openSignUpModal: ->
modal = new CreateAccountModal()
modal = new CreateAccountModal({ initialValues: { classCode: utils.getQueryVariable('_cc', "") } })
@openModalView(modal)
application.tracker?.trackEvent 'Started Student Signup', category: 'Courses'

View file

@ -1,3 +1,5 @@
utils = require 'core/utils'
RootView = require 'views/core/RootView'
template = require 'templates/editor/verifier/verifier-view'
VerifierTest = require './VerifierTest'
@ -52,14 +54,14 @@ module.exports = class VerifierView extends RootView
#testLevels = testLevels.slice 0, 15
@linksQueryString = window.location.search
@levelIDs = if @levelID then [@levelID] else testLevels
languages = utils.getQueryVariable 'languages', 'python,javascript'
#supermodel = if @levelID then @supermodel else undefined
@tests = []
@taskList = []
@tasksList = _.flatten _.map @levelIDs, (v) ->
# TODO: offer good interface for choosing which languages, better performance for skipping missing solutions
#_.map ['python', 'javascript', 'coffeescript', 'lua'], (l) ->
_.map ['python', 'javascript'], (l) ->
_.map languages.split(','), (l) ->
#_.map ['javascript'], (l) ->
level: v, language: l

View file

@ -362,6 +362,15 @@ module.exports = class PlayLevelView extends RootView
onLevelStarted: ->
return unless @surface?
#TODO: Remove this at some point
if @session.get('codeLanguage') in ['clojure', 'io']
problem =
aetherProblem:
message: "Sorry, support for #{@session.get('codeLanguage')} has been removed."
Backbone.Mediator.publish 'tome:show-problem-alert', problem: problem
@loadingView.showReady()
@trackLevelLoadEnd()
if window.currentModal and not window.currentModal.destroyed and window.currentModal.constructor isnt VictoryModal

View file

@ -37,7 +37,7 @@ module.exports = class SpellTranslationView extends CocoView
@$el.show().css(@pos)
isIdentifier: (t) ->
t and (t.type in ['identifier', 'keyword'] or t.value is 'this')
t and (_.any([/identifier/, /keyword/], (regex) -> regex.test(t.type)) or t.value is 'this')
onMouseMove: (e) =>
return if @destroyed

View file

@ -73,11 +73,11 @@ if cluster.isMaster
for i in [0...numCPUs]
cluster.fork()
cluster.on 'exit', (worker, code, signal) ->
message = "Worker #{worker.id} died! #{deaths[Math.floor Math.random() * deaths.length]}"
message = "Worker #{worker.id} died!"
console.log message
try
slack = require './server/slack'
slack.sendSlackMessage(message, ['ops'], {papertrail: true})
slack.sendSlackMessage(message, ['eng'], {papertrail: true})
catch error
console.log "Couldn't send Slack message on server death:", error
cluster.fork()

View file

@ -1,13 +1,13 @@
{
"verbose": "true",
"ignore": [],
"events": {
},
"watch": [
"server_config.js",
"server_setup.coffee",
"app/schemas",
"./server"
],
"ext":"js coffee"
"verbose": "true",
"ignore": [],
"events": {
},
"watch": [
"server_config.js",
"server_setup.coffee",
"app/schemas",
"./server"
],
"ext":"js coffee"
}

View file

@ -36,7 +36,7 @@
"postinstall": "bower install && brunch build --env fast",
"brunch": "brunch",
"bower": "bower",
"dev": "brunch watch --server",
"dev": "brunch watch --server --env fast",
"nodemon": "nodemon",
"jasmine-node": "jasmine-node",
"multicore": "coffee multicore.coffee",
@ -93,7 +93,7 @@
"devDependencies": {
"after-brunch": "0.0.5",
"assetsmanager-brunch": "^1.8.1",
"auto-reload-brunch": "> 1.0 < 1.8",
"auto-reload-brunch": "^1.8.1",
"bower": "~1.6.4",
"brunch": "^1.8.5",
"coffee-script-brunch": "^1.8.3",

View file

@ -0,0 +1,97 @@
// Find users that may have incorrect roles based on classroom ownership and membership
// Usage:
// mongo <address>:<port>/<database> <script file> -u <username> -p <password>
// User buckets
// Classroom owner, no role
// Classroom owner, student role
// Classroom owner, teacher role (GOOD)
// Classroom member, no role
// Classroom member, teacher role
// Classroom member, student role (GOOD)
'use strict';
const scriptStartTime = new Date();
const classrooms = db.classrooms.find({}, {ownerID: 1, members: 1}).toArray();
print(classrooms.length, "classrooms");
const userIds = [];
const teacherIds = {};
const studentIds = {};
for (var classroom of classrooms) {
teacherIds[classroom.ownerID.valueOf()] = true;
userIds.push(classroom.ownerID);
for (var memberId of classroom.members) {
studentIds[memberId.valueOf()] = true;
userIds.push(memberId);
}
}
print(Object.keys(teacherIds).length, "users own classrooms");
print(Object.keys(studentIds).length, "users in classrooms");
print(Object.keys(teacherIds).length + Object.keys(studentIds).length, "total");
const users = db.users.find({$and: [{_id: {$in: userIds}}, {anonymous: false}]}, {email:1, role: 1}).toArray();
const studentOwnsClassroom = [];
const teacherInClassroom = [];
const individualInClassroom = [];
const individualOwnsClassroom = [];
for (var user of users) {
const userId = user._id.valueOf();
if (!user.email) {
printjson(user);
continue;
}
if (hasStudentRole(user.role)) {
if (teacherIds[userId]) {
studentOwnsClassroom.push(user.email);
}
}
else if (hasTeacherRole(user.role)) {
if (studentIds[userId]) {
teacherInClassroom.push(user.email);
}
}
else {
if (studentIds[userId]) {
individualInClassroom.push(user.email);
}
else if (teacherIds[userId]) {
individualOwnsClassroom.push(user.email);
}
else {
print("ERROR?", userId);
break;
}
}
}
print(studentOwnsClassroom.length, "Students own classroom:");
for (var email of studentOwnsClassroom) {
print(email);
}
print(teacherInClassroom.length, "Teachers in classroom:");
for (var email of teacherInClassroom) {
print(email);
}
print(individualInClassroom.length, "Individuals in classroom:");
for (var email of individualInClassroom) {
print(email);
}
print(individualOwnsClassroom.length, "Individuals own classroom:");
for (var email of individualOwnsClassroom) {
print(email);
}
print("Script runtime: " + (new Date() - scriptStartTime));
function hasTeacherRole(role) {
return ['teacher', 'technology coordinator', 'advisor', 'principal', 'superintendent', 'parent'].indexOf(role) >= 0;
}
function hasStudentRole(role) {
return ['student'].indexOf(role) >= 0;
}

View file

@ -11,6 +11,7 @@ if (process.argv.length !== 6) {
// TODO: 2nd follow up email activity does not handle paged activity results
// TODO: sendMail copied from updateCloseIoLeads.js
// TODO: template values copied from updateCloseIoLeads.js
// TODO: status change is not related to specific lead contacts
const createTeacherEmailTemplatesAuto1 = ['tmpl_i5bQ2dOlMdZTvZil21bhTx44JYoojPbFkciJ0F560mn', 'tmpl_CEZ9PuE1y4PRvlYiKB5kRbZAQcTIucxDvSeqvtQW57G'];
const demoRequestEmailTemplatesAuto1 = ['tmpl_s7BZiydyCHOMMeXAcqRZzqn0fOtk0yOFlXSZ412MSGm', 'tmpl_cGb6m4ssDvqjvYd8UaG6cacvtSXkZY3vj9b9lSmdQrf'];
@ -31,7 +32,9 @@ earliestDate.setUTCDate(earliestDate.getUTCDate() - 10);
// ** Main program
async.series([
sendSecondFollowupMails
sendSecondFollowupMails,
addCallTasks
// TODO: Cancel call tasks
],
(err, results) => {
if (err) console.error(err);
@ -69,6 +72,14 @@ function isCreateTeacherTemplateAuto1(template) {
return createTeacherEmailTemplatesAuto1.indexOf(template) >= 0;
}
function isDemoRequestTemplateAuto2(template) {
return demoRequestEmailTemplatesAuto2.indexOf(template) >= 0;
}
function isCreateTeacherTemplateAuto2(template) {
return createTeacherEmailTemplatesAuto2.indexOf(template) >= 0;
}
function log(str) {
console.log(new Date().toISOString() + " " + str);
}
@ -325,7 +336,9 @@ function sendSecondFollowupMails(done) {
if (error) return done(error);
try {
const results = JSON.parse(body);
console.log(`sendSecondFollowupMails total num leads ${results.total_results} has_more=${results.has_more}`);
if (skip === 0) {
console.log(`sendSecondFollowupMails total num leads ${results.total_results} has_more=${results.has_more}`);
}
has_more = results.has_more;
const tasks = [];
for (const lead of results.data) {
@ -357,3 +370,227 @@ function sendSecondFollowupMails(done) {
nextPage(0);
});
}
function createAddCallTaskFn(userApiKeyMap, latestDate, lead, email) {
// Check for activity since second auto mail and status update
// Add call task
// TODO: Very similar function to createSendFollowupMailFn
const auto1Statuses = ["Auto Attempt 1", "New US Schools Auto Attempt 1"];
const auto2Statuses = ["Auto Attempt 2", "New US Schools Auto Attempt 2"];
return (done) => {
// console.log("DEBUG: addCallTask", lead.id);
// Skip leads with tasks
const url = `https://${closeIoApiKey}:X@app.close.io/api/v1/task/?lead_id=${lead.id}`;
request.get(url, (error, response, body) => {
if (error) {
console.log(error);
return done();
}
try {
const results = JSON.parse(body);
if (results.total_results > 0) {
// console.log(`DEBUG: ${lead.id} has ${results.total_results} tasks`);
return done();
}
}
catch (err) {
return done(err);
}
// Find all lead activities
const url = `https://${closeIoApiKey}:X@app.close.io/api/v1/activity/?lead_id=${lead.id}`;
request.get(url, (error, response, body) => {
if (error) {
console.log(error);
return done();
}
try {
const results = JSON.parse(body);
if (results.has_more) {
console.log(`ERROR: ${lead.id} has more activities than returned!`);
return done();
}
// Find second auto mail and status change
let sentSecondCreateTeacherEmail = false;
let sentSecondDemoRequestEmail = false;
let secondMailActivity;
let statusUpdateActivity;
let contactReplyMail;
for (const activity of results.data) {
if (activity._type === 'Email' && activity.to[0] === email) {
if (isCreateTeacherTemplateAuto2(activity.template_id)) {
if (sentSecondCreateTeacherEmail || sentSecondDemoRequestEmail) {
console.log(`ERROR: ${lead.id} ${email} sent multiple auto2 emails!? ${sentSecondCreateTeacherEmail} ${sentSecondDemoRequestEmail}`);
return done();
}
sentSecondCreateTeacherEmail = true;
secondMailActivity = activity;
}
else if (isDemoRequestTemplateAuto2(activity.template_id)) {
if (sentSecondCreateTeacherEmail || sentSecondDemoRequestEmail) {
console.log(`ERROR: ${lead.id} ${email} sent multiple auto2 emails!? ${sentSecondCreateTeacherEmail} ${sentSecondDemoRequestEmail}`);
return done();
}
sentSecondDemoRequestEmail = true;
secondMailActivity = activity;
}
}
else if (activity._type === 'LeadStatusChange' && auto1Statuses.indexOf(activity.old_status_label) >= 0
&& auto2Statuses.indexOf(activity.new_status_label) >= 0) {
statusUpdateActivity = activity;
}
}
if (!secondMailActivity) {
// console.log(`DEBUG: No auto2 mail sent for ${lead.id} ${email}`);
return done();
}
if (!statusUpdateActivity) {
console.log(`ERROR: No status update for ${lead.id} ${email}`);
return done();
}
if (new Date(secondMailActivity.date_created) > latestDate) {
// console.log(`DEBUG: Second auto mail too recent ${secondMailActivity.date_created} ${lead.id}`);
return done();
}
if (sentSecondCreateTeacherEmail && sentSecondDemoRequestEmail) {
console.log(`ERROR: ${lead.id} ${email} sent multiple auto2 emails!? ${sentSecondCreateTeacherEmail} ${sentSecondDemoRequestEmail}`);
return done();
}
// console.log(secondMailActivity);
// Find activity since second auto mail and status update
// Skip email to a different contact's email
// Skip note about different contact
let recentActivity;
for (const activity of results.data) {
if (activity.id === secondMailActivity.id) continue;
if (activity.id === statusUpdateActivity.id) continue;
if (new Date(secondMailActivity.date_created) > new Date(activity.date_created)) continue;
if (new Date(statusUpdateActivity.date_created) > new Date(activity.date_created)) continue;
if (activity._type === 'Note' && activity.note
&& activity.note.indexOf('demo_email') >= 0 && activity.note.indexOf(email) < 0) {
// console.log(`DEBUG: Skipping ${lead.id} ${email} auto import note for different contact`);
// console.log(activity.note);
continue;
}
recentActivity = activity;
break;
}
// Create call task
if (!recentActivity) {
console.log(`DEBUG: adding call task for ${lead.id} ${email}`);
const postData = {
_type: "lead",
lead_id: lead.id,
assigned_to: secondMailActivity.user_id,
text: `Call ${email}`,
is_complete: false
};
const options = {
uri: `https://${closeIoApiKey}:X@app.close.io/api/v1/task/`,
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']) {
const errorMessage = `Create call task POST error for ${email} ${lead.id}`;
console.error(errorMessage);
// console.error(body);
// console.error(postData);
return done(errorMessage);
}
return done();
});
}
else {
// console.log(`DEBUG: Found recent activity after auto2 mail for ${lead.id} ${email}`);
// console.log(recentActivity);
return done();
}
}
catch (err) {
console.log(err);
console.log(body);
return done();
}
});
});
};
}
function addCallTasks(done) {
// Find all leads with auto 2 status, created since earliestDate
// TODO: Very similar function to sendSecondFollowupMails
// console.log("DEBUG: addCallTasks");
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();
});
};
}
const tasks = [];
for (const apiKey of closeIoMailApiKeys) {
tasks.push(createGetUserFn(apiKey));
}
async.parallel(tasks, (err, results) => {
if (err) console.log(err);
const latestDate = new Date();
latestDate.setUTCDate(latestDate.getUTCDate() - 3);
const query = `date_created > ${earliestDate.toISOString().substring(0, 19)} (lead_status:"Auto Attempt 2" or lead_status:"New US Schools Auto Attempt 2")"`;
const limit = 100;
const nextPage = (skip) => {
let has_more = false;
const url = `https://${closeIoApiKey}:X@app.close.io/api/v1/lead/?_skip=${skip}&_limit=${limit}&query=${encodeURIComponent(query)}/`;
request.get(url, (error, response, body) => {
if (error) return done(error);
try {
const results = JSON.parse(body);
if (skip === 0) {
console.log(`addCallTasks total num leads ${results.total_results} has_more=${results.has_more}`);
}
has_more = results.has_more;
const tasks = [];
for (const lead of results.data) {
// console.log(`${lead.id}\t${lead.status_label}\t${lead.name}`);
// if (lead.id !== 'lead_foo') continue;
const existingContacts = lead.contacts || [];
for (const contact of existingContacts) {
if (contact.emails && contact.emails.length > 0) {
if (contact.phones && contact.phones.length > 0) {
tasks.push(createAddCallTaskFn(userApiKeyMap, latestDate, lead, contact.emails[0].email.toLowerCase()));
}
}
else {
console.log(`ERROR: lead ${lead.id} contact has non-1 emails`);
}
}
// if (tasks.length > 10) break;
}
async.series(tasks, (err, results) => {
if (err) return done(err);
if (has_more) {
return nextPage(skip + limit);
}
return done(err);
});
}
catch (err) {
return done(err);
}
});
};
nextPage(0);
});
}

View file

@ -14,7 +14,7 @@ if (process.argv.length !== 7) {
// TODO: Reduce response data via _fields param
// TODO: Assumes 1:1 contact:email relationship (Close.io supports multiple emails for a single contact)
// Save as custom fields instead of user-specific lead notes
// Save as custom fields instead of user-specific lead notes (also saving nces_ props)
const commonTrialProperties = ['organization', 'city', 'state', 'country'];
// Old properties which are deprecated or moved
@ -90,13 +90,14 @@ function upsertLeads(done) {
// ** Utilities
function getInitialLeadStatusViaCountry(country) {
function getInitialLeadStatusViaCountry(country, trialRequests) {
if (/usa|america|united states/ig.test(country)) {
return 'New US Schools Auto Attempt 1';
const status = 'New US Schools Auto Attempt 1'
return isLowValueLead(status, trialRequests) ? `${status} Low` : status;
}
}
function getInitialLeadStatusViaEmails(emails) {
function getInitialLeadStatusViaEmails(emails, trialRequests) {
let currentStatus = null;
let currentRank = closeIoInitialLeadStatuses.length;
for (const email of emails) {
@ -109,7 +110,25 @@ function getInitialLeadStatusViaEmails(emails) {
}
}
}
return currentStatus ? currentStatus : closeIoInitialLeadStatuses[closeIoInitialLeadStatuses.length - 1].status;
currentStatus = currentStatus ? currentStatus : closeIoInitialLeadStatuses[closeIoInitialLeadStatuses.length - 1].status;
return isLowValueLead(currentStatus, trialRequests) ? `${currentStatus} Low` : currentStatus;
}
function isLowValueLead(status, trialRequests) {
if (['Auto Attempt 1', 'New US Schools Auto Attempt 1'].indexOf(status) >= 0) {
for (const trialRequest of trialRequests) {
if (parseInt(trialRequest.properties.nces_district_students) < 5000) {
return true;
}
}
for (const trialRequest of trialRequests) {
// Must match these values: https://github.com/codecombat/codecombat/blob/master/app/templates/teachers/request-quote-view.jade#L159
if (['1-500', '500-1,000'].indexOf(trialRequest.properties.numStudentsTotal) >= 0) {
return true;
}
}
}
return false;
}
function getRandomEmailApiKey() {
@ -246,6 +265,7 @@ class CocoLead {
this.contacts = {};
this.custom = {};
this.name = name;
this.trialRequests = [];
}
addClassroom(email, classroom) {
if (!this.contacts[email.toLowerCase()]) this.contacts[email.toLowerCase()] = {};
@ -270,6 +290,7 @@ class CocoLead {
this.contacts[email.toLowerCase()].name = trial.properties.name;
}
this.contacts[email.toLowerCase()].trial = trial;
this.trialRequests.push(trial);
}
addUser(email, user) {
this.contacts[email.toLowerCase()].user = user;
@ -278,11 +299,11 @@ class CocoLead {
for (const email in this.contacts) {
const props = this.contacts[email].trial.properties;
if (props && props['country']) {
const status = getInitialLeadStatusViaCountry(props['country']);
const status = getInitialLeadStatusViaCountry(props['country'], this.trialRequests);
if (status) return status;
}
}
return getInitialLeadStatusViaEmails(Object.keys(this.contacts));
return getInitialLeadStatusViaEmails(Object.keys(this.contacts), this.trialRequests);
}
getLeadPostData() {
const postData = {
@ -299,7 +320,7 @@ class CocoLead {
const props = this.contacts[email].trial.properties;
if (props) {
for (const prop in props) {
if (commonTrialProperties.indexOf(prop) >= 0) {
if (commonTrialProperties.indexOf(prop) >= 0 || /nces_/ig.test(prop)) {
postData.custom[`demo_${prop}`] = props[prop];
}
}
@ -318,8 +339,17 @@ class CocoLead {
for (const email in this.contacts) {
const props = this.contacts[email].trial.properties;
if (props) {
let haveNcesData = false;
for (const prop in props) {
if (commonTrialProperties.indexOf(prop) >= 0 && currentCustom[`demo_${prop}`] !== props[prop] && currentCustom[`demo_${prop}`].indexOf(props[prop]) < 0) {
if (/nces_/ig.test(prop)) {
haveNcesData = true;
putData[`custom.demo_${prop}`] = props[prop];
}
}
for (const prop in props) {
// Always overwrite common props if we have NCES data, because other fields more likely to be accurate
if (commonTrialProperties.indexOf(prop) >= 0
&& (haveNcesData || currentCustom[`demo_${prop}`] !== props[prop] && currentCustom[`demo_${prop}`].indexOf(props[prop]) < 0)) {
putData[`custom.demo_${prop}`] = props[prop];
}
}
@ -493,8 +523,8 @@ function updateExistingLead(lead, existingLead, done) {
}
function saveNewLead(lead, done) {
// console.log('DEBUG: saveNewLead', lead.name);
const postData = lead.getLeadPostData();
// console.log(`DEBUG: saveNewLead ${lead.name} ${postData.status}`);
const options = {
uri: `https://${closeIoApiKey}:X@app.close.io/api/v1/lead/`,
body: JSON.stringify(postData)

View file

@ -2,8 +2,7 @@
# Original content copyright (c) 2014 dpen2000 licensed under the MIT license
# some defaults
DISTRO="trusty"
NODE_VERSION="0.10" # 0.10 | 0.12 | 4.x | 5.x
NODE_VERSION="5.x" # 0.10 | 0.12 | 4.x | 5.x | 6.x
# inform apt that there's no user to answer interactive questions
export DEBIAN_FRONTEND=noninteractive
@ -17,42 +16,51 @@ sudo mv /tmp/limits.conf /etc/security/limits.conf
sudo chown root:root /etc/security/limits.conf
# install prerequisites
curl -s https://deb.nodesource.com/gpgkey/nodesource.gpg.key | sudo apt-key add -
echo "deb https://deb.nodesource.com/node_${NODE_VERSION} ${DISTRO} main
deb-src https://deb.nodesource.com/node_${NODE_VERSION} ${DISTRO} main" | sudo tee /etc/apt/sources.list.d/nodesource.list
curl -sL https://deb.nodesource.com/setup_${NODE_VERSION} | sudo -E bash -
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list
echo "updating apt sources..."
sudo apt-get -qq update
sudo apt-get update
echo "installing prerequisites..."
sudo apt-get -qqy install --no-install-recommends git g++ make curl wget
sudo apt-get -y install --no-install-recommends build-essential git g++ make curl wget python2.7 dos2unix
# install node.js
echo "installing node.js..."
sudo apt-get -qqy install nodejs
sudo apt-get -y install nodejs
npm config set python `which python2.7`
echo "upgrading npm..."
sudo npm install -g npm@latest # upgrade npm
sudo npm install -g geoip-lite
sudo npm install -g bower
sudo npm install -g brunch
sudo npm install -g geoip-lite
sudo npm install -g nodemon
# bind /vagrant/node_modules so that it does not leak through to the host file system
# which triggers symlink and path size issues on Windows hosts
mkdir -p /vagrant/node_modules
sudo mkdir -p /node_modules
sudo chown vagrant:vagrant /node_modules
sudo chown -R vagrant:vagrant /node_modules
sudo mount --bind /node_modules /vagrant/node_modules
cd /vagrant
# prepare
find /vagrant/app -type f -exec dos2unix {} \;
find /vagrant/vendor -type f -exec dos2unix {} \;
sudo chown -R vagrant:vagrant /home/vagrant
# install npm modules
echo "installing modules..."
cd /vagrant
npm install
bower install
# install mongo
echo "installing mongodb..."
sudo apt-get -qqy install --no-install-recommends mongodb-org
sudo apt-get -y install --no-install-recommends mongodb-org
# start mongodb
sudo service mongod start
# populate mongo
echo "populating mongodb..."

View file

@ -104,13 +104,15 @@ ClassroomHandler = class ClassroomHandler extends Handler
return @sendForbiddenError(res) unless classroom.get('ownerID').equals(req.user.get('_id'))
for email in req.body.emails
joinCode = (classroom.get('codeCamel') or classroom.get('code'))
context =
email_id: sendwithus.templates.course_invite_email
recipient:
address: email
email_data:
class_name: classroom.get('name')
join_link: "https://codecombat.com/courses?_cc=" + (classroom.get('codeCamel') or classroom.get('code'))
join_link: "https://codecombat.com/courses?_cc=" + joinCode
join_code: joinCode
sendwithus.api.send context, _.noop
return @sendSuccess(res, {})

View file

@ -401,7 +401,7 @@ UserHandler = class UserHandler extends Handler
name: sponsor.get('name')
# Get recipient subscription info
findStripeSubscription sponsor.get('stripe').customerID, userID: req.user.id, (subscription) =>
findStripeSubscription sponsor.get('stripe')?.customerID, userID: req.user.id, (subscription) =>
info.subscription = subscription
@sendDatabaseError(res, 'No sponsored subscription found') unless info.subscription?
@sendSuccess(res, info)

View file

@ -27,19 +27,24 @@ createMailContext = (req, done) ->
level = if user?.get('points') > 0 then Math.floor(5 * Math.log((1 / 100) * (user.get('points') + 100))) + 1 else 0
premium = user?.isPremium()
teacher = user?.isTeacher()
content = """
#{message}
--
<a href='http://codecombat.com/user/#{user.get('slug') or user.get('_id')}'>#{user.get('name') or 'Anonymous'}</a> - Level #{level}#{if premium then ' - Subscriber' else ''}#{if country then ' - ' + country else ''}
<a href='http://codecombat.com/user/#{user.get('slug') or user.get('_id')}'>#{user.get('name') or 'Anonymous'}</a> - Level #{level}#{if teacher then ' - Teacher' else ''}#{if premium then ' - Subscriber' else ''}#{if country then ' - ' + country else ''}
"""
if req.body.browser
content += "\n#{req.body.browser} - #{req.body.screenSize}"
address = switch
when teacher then config.mail.supportSchools
when premium then config.mail.supportPremium
else config.mail.supportPrimary
context =
email_id: sendwithus.templates.plain_text_email
recipient:
address: if premium then config.mail.supportPremium else config.mail.supportPrimary
address: address
sender:
address: config.mail.username
reply_to: sender or user.get('email')

View file

@ -27,7 +27,7 @@ module.exports.templates =
generic_email: 'tem_JhRnQ4pvTS4KdQjYoZdbei'
plain_text_email: 'tem_85UvKDCCNPXsFckERTig6Y'
next_steps_email: 'tem_RDHhTG5inXQi8pthyqWr5D'
course_invite_email: 'tem_u6D2EFWYC5Ptk38bSykjsU'
course_invite_email: 'tem_f5K7BXX5vQ9a7kwYTACbJa'
teacher_free_trial: 'tem_R7d9Hpoba9SceQNiYSXBak'
teacher_free_trial_hoc: 'tem_4ZSY9wsA9Qwn4wBFmZgPdc'
teacher_request_demo: 'tem_cwG3HZjEyb6QE493hZuUra'

View file

@ -55,6 +55,7 @@ config.mail =
username: process.env.COCO_MAIL_SERVICE_USERNAME or ''
supportPrimary: process.env.COCO_MAIL_SUPPORT_PRIMARY or ''
supportPremium: process.env.COCO_MAIL_SUPPORT_PREMIUM or ''
supportSchools: process.env.COCO_MAIL_SUPPORT_SCHOOLS or ''
mailchimpAPIKey: process.env.COCO_MAILCHIMP_API_KEY or ''
mailchimpWebhook: process.env.COCO_MAILCHIMP_WEBHOOK or '/mail/webhook'
sendwithusAPIKey: process.env.COCO_SENDWITHUS_API_KEY or ''

View file

@ -1,4 +1,4 @@
Problem = require 'views/play/level/tome/Problem'
Problem = require 'views/play/level/tome/Problem'
describe 'Problem', ->
# boilerplate problem params

View file

@ -1,236 +1,236 @@
var Spade = function Spade() {
this.stack = [];
}
Spade.prototype = {
track: function(_elem) {
this.target = _elem;
var spade = this;
var el = document.createElement("div");
keyHook = null;
if(_elem.textInput && _elem.textInput.getElement) {
keyHook = _elem.textInput.getElement();
} else {
keyHook = _elem;
}
keyHook.addEventListener("keydown", function(_event) {spade.createEvent(spade.target)});
//Maybe this is needed depending on Firefox/other browsers? Duplicate non-diff events get compiled down.
keyHook.addEventListener("keyup", function(_event) {spade.createEvent(spade.target)});
_elem.addEventListener("mouseup", function(_event) {spade.createEvent(spade.target)});
},
createEvent: function(_target) {
if(_target.getValue) {
this.stack.push({
"startPos":_target.selection.getCursor(),
"endPos":_target.selection.getSelectionAnchor(),
"content":_target.getValue(),
"timestamp":(new Date()).getTime()
});
} else {
this.stack.push({
"startPos":_target.selectionStart,
"endPos":_target.selectionEnd,
"content":_target.value,
"timestamp":(new Date()).getTime()
});
}
},
compile: function() {
var compiledStack = [];
if(this.stack.length > 0) {
var startTime = this.stack[0].timestamp;
var sum = 0;
var sum2 = 0;
for(var i = 0; i < this.stack.length; i++) {
var c = this.stack[i];
var adjustedTimestamp = c.timestamp - startTime;
var tString = ""; //The changed string.
var fIndex = null; //The first index of changes.
var eIndex = null; //The last index of changes.
var dCount = 0; //Amount of character changes.
if(i >= 1) {
var p = this.stack[i - 1];
var isOkay = false;
for(var key in p) {
if(key != "timestamp") {
if(typeof p[key] === "string") {
if(p[key] !== c[key]) {
isOkay = true;
}
} else {
for(var key2 in p[key]) {
if(c[key][key2] !== undefined) {
if(p[key][key2] !== c[key][key2]) {
isOkay = true;
}
} else {
console.warn("Warning: c[key][key2] doesn't exist, but p[key][key2] does.");
isOkay = true;
}
}
}
}
}
if(!isOkay) {
sum2++;
continue;
}
sum++;
if(p.content != c.content) {
//Check from the start to the end, which characters are different.
for(var j = 0; j < Math.max(p.content.length, c.content.length); j++) {
if(p.content.charAt(j) === c.content.charAt(j)) {
if(fIndex != null) {
tString += c.content.charAt(j);
dCount++;
}
} else {
tString += c.content.charAt(j);
if(fIndex === null) {
fIndex = j;
}
dCount++;
}
}
//Check from the end to the start, which characters are different.
for(var j = 0; j < Math.min(p.content.length, c.content.length) - fIndex; j++) {
if(p.content.charAt(p.content.length - 1 - j) !== c.content.charAt(c.content.length - 1 - j)) {
if(eIndex == null) {
eIndex = j;
break;
}
}
}
//This accounts for the fact when changing from "aa" to "aaa" (for example).
if(eIndex === null) {
eIndex = Math.min(p.content.length, c.content.length) - fIndex;
}
tString = tString.substring(0, tString.length - eIndex);
}
} else {
tString = c.content;
fIndex = 0;
eIndex = tString.length;
}
compiledStack.push({
"timestamp":adjustedTimestamp,
"difContent":tString,
"difFIndex":fIndex,
"difEIndex":eIndex,
"selFIndex":c.startPos,
"selEIndex":c.endPos
});
}
} else {
//Just return the empty array.
}
return compiledStack;
},
play: function(_stack, _elem) {
if(_stack.length === 0) {
console.warn("SPADE: No events to play.")
return
}
if(_elem.setValue) {
_elem.setValue(_stack[0].difContent);
} else {
_elem.value = _stack[0].difContent
}
_stack = _stack.slice();
_stack.shift();
var curTime, dTime;
var elapsedTime = 0;
var prevTime = (new Date()).getTime();
var playbackInterval = setInterval(function() {
curTime = (new Date()).getTime();
dTime = curTime - prevTime;
dTime *= 1; //Multiply for faster/slower playback speeds.
elapsedTime += dTime;
var tArray = _stack.filter(function(_event) {
return ((_event.timestamp) >= (elapsedTime - dTime)) && ((_event.timestamp) < (elapsedTime));
});
for(var i = 0; i < tArray.length; i++) {
var tEvent = tArray[i];
var oVal = null;
if(_elem.getValue) {
oVal = _elem.getValue();
} else {
oVal = _elem.value;
}
if(tEvent.difFIndex !== null && tEvent.difEIndex !== null) {
if(_elem.setValue) {
_elem.setValue(oVal.substring(0, tEvent.difFIndex) + tEvent.difContent + oVal.substring(oVal.length - tEvent.difEIndex, oVal.length));
} else {
_elem.value = oVal.substring(0, tEvent.difFIndex) + tEvent.difContent + oVal.substring(oVal.length - tEvent.difEIndex, oVal.length)
}
}
if(_elem.selection && _elem.selection.moveCursorToPosition) {
//Maybe this will work someday
_elem.selection.moveCursorToPosition(tEvent.selFIndex);
_elem.selection.setSelectionAnchor(tEvent.selEIndex.row, tEvent.selEIndex.column);
_elem.selection.selectTo(tEvent.selFIndex.row, tEvent.selFIndex.column);
} else {
//Likewise
_elem.focus();
_elem.setSelectionRange(tEvent.selFIndex, tEvent.selEIndex);
}
}
if(_stack[_stack.length - 1] === undefined || elapsedTime > _stack[_stack.length - 1].timestamp) {
clearInterval(playbackInterval);
}
prevTime = curTime;
}, 10);
},
debugPlay: function(_stack) {
var area = document.createElement('textarea');
area.zIndex = 9999;
area.style.width = "512px";
area.style.height = "512px";
area.style.position = "absolute";
area.style.left = "100px";
area.style.top = "100px";
document.body.appendChild(area);
this.play(_stack, area);
},
condense: function(_stack) {
var compressedArray = [];
for(var i = 0; i < _stack.length; i++) {
var u = _stack[i];
compressedArray.push([
u.timestamp,
u.difContent,
u.difFIndex,
u.difEIndex,
u.selFIndex.row,
u.selFIndex.column,
u.selEIndex.row,
u.selEIndex.column
]);
}
return compressedArray;
},
expand: function(_array) {
var uncompressedArray = [];
for(var i = 0 ; i < _array.length; i++) {
var c = _array[i];
uncompressedArray.push({
"timestamp":c[0],
"difContent":c[1],
"difFIndex":c[2],
"difEIndex":c[3],
"selFIndex":{
"row":c[4],
"column":c[5]
},
"selEIndex":{
"row":c[6],
"column":c[7]
},
});
}
return uncompressedArray;
}
}
var Spade = function Spade() {
this.stack = [];
}
Spade.prototype = {
track: function(_elem) {
this.target = _elem;
var spade = this;
var el = document.createElement("div");
keyHook = null;
if(_elem.textInput && _elem.textInput.getElement) {
keyHook = _elem.textInput.getElement();
} else {
keyHook = _elem;
}
keyHook.addEventListener("keydown", function(_event) {spade.createEvent(spade.target)});
//Maybe this is needed depending on Firefox/other browsers? Duplicate non-diff events get compiled down.
keyHook.addEventListener("keyup", function(_event) {spade.createEvent(spade.target)});
_elem.addEventListener("mouseup", function(_event) {spade.createEvent(spade.target)});
},
createEvent: function(_target) {
if(_target.getValue) {
this.stack.push({
"startPos":_target.selection.getCursor(),
"endPos":_target.selection.getSelectionAnchor(),
"content":_target.getValue(),
"timestamp":(new Date()).getTime()
});
} else {
this.stack.push({
"startPos":_target.selectionStart,
"endPos":_target.selectionEnd,
"content":_target.value,
"timestamp":(new Date()).getTime()
});
}
},
compile: function() {
var compiledStack = [];
if(this.stack.length > 0) {
var startTime = this.stack[0].timestamp;
var sum = 0;
var sum2 = 0;
for(var i = 0; i < this.stack.length; i++) {
var c = this.stack[i];
var adjustedTimestamp = c.timestamp - startTime;
var tString = ""; //The changed string.
var fIndex = null; //The first index of changes.
var eIndex = null; //The last index of changes.
var dCount = 0; //Amount of character changes.
if(i >= 1) {
var p = this.stack[i - 1];
var isOkay = false;
for(var key in p) {
if(key != "timestamp") {
if(typeof p[key] === "string") {
if(p[key] !== c[key]) {
isOkay = true;
}
} else {
for(var key2 in p[key]) {
if(c[key][key2] !== undefined) {
if(p[key][key2] !== c[key][key2]) {
isOkay = true;
}
} else {
console.warn("Warning: c[key][key2] doesn't exist, but p[key][key2] does.");
isOkay = true;
}
}
}
}
}
if(!isOkay) {
sum2++;
continue;
}
sum++;
if(p.content != c.content) {
//Check from the start to the end, which characters are different.
for(var j = 0; j < Math.max(p.content.length, c.content.length); j++) {
if(p.content.charAt(j) === c.content.charAt(j)) {
if(fIndex != null) {
tString += c.content.charAt(j);
dCount++;
}
} else {
tString += c.content.charAt(j);
if(fIndex === null) {
fIndex = j;
}
dCount++;
}
}
//Check from the end to the start, which characters are different.
for(var j = 0; j < Math.min(p.content.length, c.content.length) - fIndex; j++) {
if(p.content.charAt(p.content.length - 1 - j) !== c.content.charAt(c.content.length - 1 - j)) {
if(eIndex == null) {
eIndex = j;
break;
}
}
}
//This accounts for the fact when changing from "aa" to "aaa" (for example).
if(eIndex === null) {
eIndex = Math.min(p.content.length, c.content.length) - fIndex;
}
tString = tString.substring(0, tString.length - eIndex);
}
} else {
tString = c.content;
fIndex = 0;
eIndex = tString.length;
}
compiledStack.push({
"timestamp":adjustedTimestamp,
"difContent":tString,
"difFIndex":fIndex,
"difEIndex":eIndex,
"selFIndex":c.startPos,
"selEIndex":c.endPos
});
}
} else {
//Just return the empty array.
}
return compiledStack;
},
play: function(_stack, _elem) {
if(_stack.length === 0) {
console.warn("SPADE: No events to play.")
return
}
if(_elem.setValue) {
_elem.setValue(_stack[0].difContent);
} else {
_elem.value = _stack[0].difContent
}
_stack = _stack.slice();
_stack.shift();
var curTime, dTime;
var elapsedTime = 0;
var prevTime = (new Date()).getTime();
var playbackInterval = setInterval(function() {
curTime = (new Date()).getTime();
dTime = curTime - prevTime;
dTime *= 1; //Multiply for faster/slower playback speeds.
elapsedTime += dTime;
var tArray = _stack.filter(function(_event) {
return ((_event.timestamp) >= (elapsedTime - dTime)) && ((_event.timestamp) < (elapsedTime));
});
for(var i = 0; i < tArray.length; i++) {
var tEvent = tArray[i];
var oVal = null;
if(_elem.getValue) {
oVal = _elem.getValue();
} else {
oVal = _elem.value;
}
if(tEvent.difFIndex !== null && tEvent.difEIndex !== null) {
if(_elem.setValue) {
_elem.setValue(oVal.substring(0, tEvent.difFIndex) + tEvent.difContent + oVal.substring(oVal.length - tEvent.difEIndex, oVal.length));
} else {
_elem.value = oVal.substring(0, tEvent.difFIndex) + tEvent.difContent + oVal.substring(oVal.length - tEvent.difEIndex, oVal.length)
}
}
if(_elem.selection && _elem.selection.moveCursorToPosition) {
//Maybe this will work someday
_elem.selection.moveCursorToPosition(tEvent.selFIndex);
_elem.selection.setSelectionAnchor(tEvent.selEIndex.row, tEvent.selEIndex.column);
_elem.selection.selectTo(tEvent.selFIndex.row, tEvent.selFIndex.column);
} else {
//Likewise
_elem.focus();
_elem.setSelectionRange(tEvent.selFIndex, tEvent.selEIndex);
}
}
if(_stack[_stack.length - 1] === undefined || elapsedTime > _stack[_stack.length - 1].timestamp) {
clearInterval(playbackInterval);
}
prevTime = curTime;
}, 10);
},
debugPlay: function(_stack) {
var area = document.createElement('textarea');
area.zIndex = 9999;
area.style.width = "512px";
area.style.height = "512px";
area.style.position = "absolute";
area.style.left = "100px";
area.style.top = "100px";
document.body.appendChild(area);
this.play(_stack, area);
},
condense: function(_stack) {
var compressedArray = [];
for(var i = 0; i < _stack.length; i++) {
var u = _stack[i];
compressedArray.push([
u.timestamp,
u.difContent,
u.difFIndex,
u.difEIndex,
u.selFIndex.row,
u.selFIndex.column,
u.selEIndex.row,
u.selEIndex.column
]);
}
return compressedArray;
},
expand: function(_array) {
var uncompressedArray = [];
for(var i = 0 ; i < _array.length; i++) {
var c = _array[i];
uncompressedArray.push({
"timestamp":c[0],
"difContent":c[1],
"difFIndex":c[2],
"difEIndex":c[3],
"selFIndex":{
"row":c[4],
"column":c[5]
},
"selEIndex":{
"row":c[6],
"column":c[7]
},
});
}
return uncompressedArray;
}
}