diff --git a/.travis.yml b/.travis.yml index 10c619e33..0e998a59e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,12 @@ language: node_js node_js: - 0.10 +before_install: + - "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" + - "sudo apt-get update" + - "sudo apt-get install mongodb-org-server" + before_script: - "npm install" - export DISPLAY=:99.0 @@ -9,7 +15,11 @@ before_script: - "./node_modules/.bin/bower install" - "gem install sass" - "./node_modules/.bin/brunch b" + - "mkdir mongo" + - "mongod --dbpath=./mongo --fork --logpath ./mongodb.log" + - "node index.js --unittest &" + - "sleep 10" # to give node a chance to start script: + - "./node_modules/jasmine-node/bin/jasmine-node test/server/ --coffee --captureExceptions" - "./node_modules/karma/bin/karma start --browsers Firefox --single-run" - diff --git a/app/assets/images/pages/employer/employer_icon1.png b/app/assets/images/pages/employer/employer_icon1.png new file mode 100644 index 000000000..1b58ce8dd Binary files /dev/null and b/app/assets/images/pages/employer/employer_icon1.png differ diff --git a/app/assets/images/pages/employer/employer_icon2.png b/app/assets/images/pages/employer/employer_icon2.png new file mode 100644 index 000000000..3ea5b571d Binary files /dev/null and b/app/assets/images/pages/employer/employer_icon2.png differ diff --git a/app/assets/images/pages/employer/employer_icon3.png b/app/assets/images/pages/employer/employer_icon3.png new file mode 100644 index 000000000..d29b0e609 Binary files /dev/null and b/app/assets/images/pages/employer/employer_icon3.png differ diff --git a/app/assets/images/pages/employer/employer_icon4.png b/app/assets/images/pages/employer/employer_icon4.png new file mode 100644 index 000000000..df5c40222 Binary files /dev/null and b/app/assets/images/pages/employer/employer_icon4.png differ diff --git a/app/lib/FacebookHandler.coffee b/app/lib/FacebookHandler.coffee index 10a59c40c..e9bf6aadc 100644 --- a/app/lib/FacebookHandler.coffee +++ b/app/lib/FacebookHandler.coffee @@ -13,9 +13,6 @@ userPropsToSave = module.exports = FacebookHandler = class FacebookHandler extends CocoClass - constructor: -> - super() - subscriptions: 'facebook-logged-in':'onFacebookLogin' 'facebook-logged-out': 'onFacebookLogout' @@ -42,22 +39,18 @@ module.exports = FacebookHandler = class FacebookHandler extends CocoClass return oldEmail = me.get('email') - patch = {} - patch.firstName = r.first_name if r.first_name - patch.lastName = r.last_name if r.last_name - patch.gender = r.gender if r.gender - patch.email = r.email if r.email - patch.facebookID = r.id if r.id - me.set(patch) - patch._id = me.id - + me.set('firstName', r.first_name) if r.first_name + me.set('lastName', r.last_name) if r.last_name + me.set('gender', r.gender) if r.gender + me.set('email', r.email) if r.email + me.set('facebookID', r.id) if r.id + Backbone.Mediator.publish('logging-in-with-facebook') window.tracker?.trackEvent 'Facebook Login' window.tracker?.identify() - me.save(patch, { - patch: true + me.patch({ error: backboneFailure, - url: "/db/user?facebookID=#{r.id}&facebookAccessToken=#{@authResponse.accessToken}" + url: "/db/user/#{me.id}?facebookID=#{r.id}&facebookAccessToken=#{@authResponse.accessToken}" success: (model) -> window.location.reload() if model.get('email') isnt oldEmail }) diff --git a/app/lib/LevelBus.coffee b/app/lib/LevelBus.coffee index 9693df01c..810038242 100644 --- a/app/lib/LevelBus.coffee +++ b/app/lib/LevelBus.coffee @@ -22,13 +22,13 @@ module.exports = class LevelBus extends Bus 'tome:spell-changed': 'onSpellChanged' 'tome:spell-created': 'onSpellCreated' 'application:idle-changed': 'onIdleChanged' - + constructor: -> super(arguments...) @changedSessionProperties = {} @saveSession = _.debounce(@saveSession, 1000, {maxWait: 5000}) @playerIsIdle = false - + init: -> super() @fireScriptsRef = @fireRef?.child('scripts') @@ -36,7 +36,7 @@ module.exports = class LevelBus extends Bus setSession: (@session) -> @listenTo(@session, 'change:multiplayer', @onMultiplayerChanged) @timerIntervalID = setInterval(@incrementSessionPlaytime, 1000) - + onIdleChanged: (e) -> @playerIsIdle = e.idle @@ -44,7 +44,7 @@ module.exports = class LevelBus extends Bus if @playerIsIdle then return @changedSessionProperties.playtime = true @session.set("playtime",@session.get("playtime") + 1) - + onPoint: -> return true unless @session?.get('multiplayer') super() @@ -224,7 +224,7 @@ module.exports = class LevelBus extends Bus saveSession: -> return if _.isEmpty @changedSessionProperties - # don't let peaking admins mess with the session accidentally + # don't let peeking admins mess with the session accidentally return unless @session.get('multiplayer') or @session.get('creator') is me.id Backbone.Mediator.publish 'level:session-will-save', session: @session patch = {} diff --git a/app/lib/LocalMongo.coffee b/app/lib/LocalMongo.coffee index cdd8fd50e..2027e1c70 100644 --- a/app/lib/LocalMongo.coffee +++ b/app/lib/LocalMongo.coffee @@ -18,6 +18,7 @@ doQuerySelector = (value, operatorObj) -> when '$ne' then return false if mapred value, body, (l, r) -> l == r when '$in' then return false unless _.reduce value, ((result, val) -> result or val in body), false when '$nin' then return false if _.reduce value, ((result, val) -> result or val in body), false + when '$exists' then return false if value[0]? isnt body[0] else return false true @@ -34,11 +35,13 @@ matchesQuery = (target, queryObj) -> pieces = prop.split('.') obj = target for piece in pieces - return false unless piece of obj + unless piece of obj + obj = null + break obj = obj[piece] if typeof query != 'object' or _.isArray query return false unless obj == query or (query in obj if _.isArray obj) else return false unless doQuerySelector obj, query true -LocalMongo.matchesQuery = matchesQuery \ No newline at end of file +LocalMongo.matchesQuery = matchesQuery diff --git a/app/lib/auth.coffee b/app/lib/auth.coffee index c9d6fcefb..310727b65 100644 --- a/app/lib/auth.coffee +++ b/app/lib/auth.coffee @@ -10,7 +10,7 @@ init = -> if me and not me.get('testGroupNumber')? # Assign testGroupNumber to returning visitors; new ones in server/routes/auth me.set 'testGroupNumber', Math.floor(Math.random() * 256) - me.save() + me.patch() Backbone.listenTo(me, 'sync', Backbone.Mediator.publish('me:synced', {me:me})) diff --git a/app/lib/utils.coffee b/app/lib/utils.coffee index 78816990f..8dbae8cc2 100644 --- a/app/lib/utils.coffee +++ b/app/lib/utils.coffee @@ -75,3 +75,25 @@ module.exports.getByPath = (target, path) -> return undefined unless piece of obj obj = obj[piece] obj + +module.exports.round = _.curry (digits, n) -> + n = +n.toFixed(digits) + +positify = (func) -> (x) -> if x > 0 then func(x) else 0 + +# f(x) = ax + b +createLinearFunc = (params) -> + (x) -> (params.a or 1) * x + (params.b or 0) + +# f(x) = ax² + bx + c +createQuadraticFunc = (params) -> + (x) -> (params.a or 1) * x * x + (params.b or 1) * x + (params.c or 0) + +# f(x) = a log(b (x + c)) + d +createLogFunc = (params) -> + (x) -> if x > 0 then (params.a or 1) * Math.log((params.b or 1) * (x + (params.c or 0))) + (params.d or 0) else 0 + +module.exports.functionCreators = + linear: positify(createLinearFunc) + quadratic: positify(createQuadraticFunc) + logarithmic: positify(createLogFunc) diff --git a/app/lib/world/names.coffee b/app/lib/world/names.coffee index 6a01730e5..f8bb3a6b3 100644 --- a/app/lib/world/names.coffee +++ b/app/lib/world/names.coffee @@ -1,5 +1,6 @@ module.exports.thangNames = thangNames = "Soldier M": [ + "Duke" "William" "Lucas" "Marcus" @@ -66,6 +67,7 @@ module.exports.thangNames = thangNames = "Coco" "Buffy" "Allankrita" + "Kay" ] "Peasant M": [ "Yorik" @@ -355,6 +357,8 @@ module.exports.thangNames = thangNames = "Hank" "Jeph" "Neville" + "Alphonse" + "Edward" ] "Captain": [ "Anya" @@ -367,4 +371,5 @@ module.exports.thangNames = thangNames = "Jane" "Lia" "Hardcastle" + "Leona" ] diff --git a/app/locale/en.coffee b/app/locale/en.coffee index fd3c88b93..8baaf24ac 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -198,8 +198,8 @@ done_editing: "Done Editing" profile_for_prefix: "Profile for " profile_for_suffix: "" - approved: "Approved" - not_approved: "Not Approved" + featured: "Featured" + not_featured: "Not Featured" looking_for: "Looking for:" last_updated: "Last updated:" contact: "Contact" @@ -294,14 +294,25 @@ project_picture_help: "Upload a 230x115px or larger image showing off the project." project_link: "Link" project_link_help: "Link to the project." - + player_code: "Player Code" employers: - want_to_hire_our_players: "Want to hire expert CodeCombat players?" + want_to_hire_our_players: "Hire CodeCombat Players" + what: "What is CodeCombat?" + what_blurb: "CodeCombat is a multiplayer browser programming game. Players write code to control their forces in battle against other developers. We support JavaScript, Python, Lua, Clojure, CoffeeScript, and Io." + who: "Who Are the Players?" + who_blurb: "CodeCombateers are software developers who enjoy using their programming skills to play games. They range from college seniors at top 20 engineering programs to 20-year industry veterans." + how: "How Do We Find Developers?" + how_blurb: "We host competitive tournaments to attract competitive software engieneers. We then use in-house algorithms to identify the best players among the top 5% of tournament winners." + why: "Why Hire Through Us?" + why_blurb_1: "We will save you time. Every CodeCombateer we feaure is " + why_blurb_2: "looking for work" + why_blurb_3: ", has " + why_blurb_4: "demonstrated top notch technical skills" + why_blurb_5: ", and has been " + why_blurb_6: "personally screened by us" + why_blurb_7: ". Stop screening and start hiring." see_candidates: "Click here to see our candidates" - candidates_count_prefix: "We currently have " - candidates_count_many: "many" - candidates_count_suffix: "highly skilled and vetted developers looking for work." candidate_name: "Name" candidate_location: "Location" candidate_looking_for: "Looking For" @@ -309,8 +320,9 @@ candidate_top_skills: "Top Skills" candidate_years_experience: "Yrs Exp" candidate_last_updated: "Last Updated" - candidate_approved: "Us?" - candidate_active: "Them?" + featured_developers: "Featured Developers" + other_developers: "Other Developers" + inactive_developers: "Inactive Developers" play_level: done: "Done" @@ -436,6 +448,7 @@ av_entities_sub_title: "Entities" av_entities_users_url: "Users" av_entities_active_instances_url: "Active Instances" + av_entities_employer_list_url: "Employer List" av_other_sub_title: "Other" av_other_debug_base_url: "Base (for debugging base.jade)" u_title: "User List" @@ -505,7 +518,7 @@ new_thang_title_login: "Log In to Create a New Thang Type" new_level_title_login: "Log In to Create a New Level" new_achievement_title: "Create a New Achievement" - new_achievement_title_login: "Sign Up to Create a New Achievement" + new_achievement_title_login: "Log In to Create a New Achievement" article_search_title: "Search Articles Here" thang_search_title: "Search Thang Types Here" level_search_title: "Search Levels Here" @@ -784,9 +797,12 @@ watch_victory: "Watch your victory" defeat_the: "Defeat the" tournament_ends: "Tournament ends" + tournament_ended: "Tournament ended" tournament_rules: "Tournament Rules" tournament_blurb: "Write code, collect gold, build armies, crush foes, win prizes, and upgrade your career in our $40,000 Greed tournament! Check out the details" tournament_blurb_blog: "on our blog" + rules: "Rules" + winners: "Winners" ladder_prizes: title: "Tournament Prizes" @@ -808,7 +824,6 @@ license: "license" oreilly: "ebook of your choice" - multiplayer_launch: introducing_dungeon_arena: "Introducing Dungeon Arena" new_way: "The new way to compete with code." @@ -866,6 +881,7 @@ source_document: "Source Document" document: "Document" # note to diplomats: not a physical document, a document in MongoDB, ie a record in a database sprite_sheet: "Sprite Sheet" + candidate_sessions: "Candidate Sessions" delta: added: "Added" diff --git a/app/locale/fr.coffee b/app/locale/fr.coffee index d40d2806e..8f18cae99 100644 --- a/app/locale/fr.coffee +++ b/app/locale/fr.coffee @@ -16,7 +16,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t play: "Jouer" retry: "Reessayer" watch: "Regarder" -# unwatch: "Unwatch" + unwatch: "Ne plus regarder" submit_patch: "Soumettre un correctif" units: @@ -26,14 +26,14 @@ module.exports = nativeDescription: "français", englishDescription: "French", t minutes: "minutes" hour: "heure" hours: "heures" -# day: "day" -# days: "days" -# week: "week" -# weeks: "weeks" -# month: "month" -# months: "months" -# year: "year" -# years: "years" + day: "jour" + days: "jours" + week: "semaine" + weeks: "semaines" + month: "mois" + months: "mois" + year: "année" + years: "années" modal: close: "Fermer" @@ -128,8 +128,8 @@ module.exports = nativeDescription: "français", englishDescription: "French", t forum_page: "notre forum" forum_suffix: " À la place." send: "Envoyer un commentaire" -# contact_candidate: "Contact Candidate" -# recruitment_reminder: "Use this form to reach out to candidates you are interested in interviewing. Remember that CodeCombat charges 15% of first-year salary. The fee is due upon hiring the employee and is refundable for 90 days if the employee does not remain employed. Part time, remote, and contract employees are free, as are interns." + contact_candidate: "Contacter le candidat" + recruitment_reminder: "Utilisez ce formulaire pour entrer en contact avec le candidat qui vous interesse. Souvenez-vous que CodeCombat facture 15% de la première année de salaire. Ces frais sont dues à l'embauche de l'employé, ils sont remboursable pendant 90 jours si l'employé ne reste pas employé. Les employés à temps partiel, à distance ou contractuel sont gratuits en tant que stagiaires." diplomat_suggestion: title: "Aidez à traduire CodeCombat!" @@ -173,11 +173,11 @@ module.exports = nativeDescription: "français", englishDescription: "French", t email_announcements: "Annonces" email_announcements_description: "Recevoir des mails sur les dernières actualités et sur le développement de CodeCombat." email_notifications: "Notifications" -# email_notifications_summary: "Controls for personalized, automatic email notifications related to your CodeCombat activity." -# email_any_notes: "Any Notifications" + email_notifications_summary: "Commandes pour personaliser les notifications automatiques d'email liées à votre activité sur CodeCombat." + email_any_notes: "Toutes Notifications" email_any_notes_description: "Désactivez pour ne plus recevoir de notifications par e-mail." -# email_recruit_notes: "Job Opportunities" -# email_recruit_notes_description: "If you play really well, we may contact you about getting you a (better) job." + email_recruit_notes: "Offres d'emploi" + email_recruit_notes_description: "Si vous jouez vraiment bien, nous pouvons vous contacter pour vous proposer un (meilleur) emploi." contributor_emails: "Emails des contributeurs" contribute_prefix: "Nous recherchons des personnes pour se joindre à notre groupe! Consultez la " contribute_page: "page de contributions" @@ -186,15 +186,15 @@ module.exports = nativeDescription: "français", englishDescription: "French", t error_saving: "Problème d'enregistrement" saved: "Changements sauvegardés" password_mismatch: "Le mot de passe ne correspond pas." -# job_profile: "Job Profile" -# job_profile_approved: "Your job profile has been approved by CodeCombat. Employers will be able to see it until you either mark it inactive or it has not been changed for four weeks." -# job_profile_explanation: "Hi! Fill this out, and we will get in touch about finding you a software developer job." -# sample_profile: "See a sample profile" + job_profile: "Profil d'emploi" + job_profile_approved: "Votre profil d'emploi a été approuvé par CodeCombat. Les employeurs seront en mesure de voir votre profil jusqu'à ce que vous le marquez inactif ou qu'il n'a pas été changé pendant quatre semaines." + job_profile_explanation: "Salut! Remplissez-le et nous prendrons contact pour vous trouver un emploi de développeur de logiciels." + sample_profile: "Voir un exemple de profil" view_profile: "Voir votre profil" account_profile: edit_settings: "Éditer les préférences" -# done_editing_settings: "Done Editing" + done_editing_settings: "Edition terminée" profile_for_prefix: "Profil pour " profile_for_suffix: "" approved: "Approuvé" @@ -202,57 +202,57 @@ module.exports = nativeDescription: "français", englishDescription: "French", t looking_for: "à la recherche de:" last_updated: "Dernière Mise à jour:" contact: "Contact" -# active: "Looking for interview offers now" -# inactive: "Not looking for offers right now" -# complete: "complete" -# next: "Next" -# next_city: "city?" -# next_country: "pick your country." -# next_name: "name?" -# next_short_description: "summarize yourself at a glance." -# next_long_description: "describe the work you're looking for." -# next_skills: "list at least five skills." -# next_work: "list your work experience." -# next_education: "recount your educational ordeals." -# next_projects: "show off up to three projects you've worked on." -# next_links: "add any personal or social links." -# next_photo: "add an optional professional photo." -# next_active: "mark yourself open to offers to show up in searches." -# example_blog: "Your Blog" -# example_github: "Your GitHub" -# links_header: "Personal Links" -# links_blurb: "Link any other sites or profiles you want to highlight, like your GitHub, your LinkedIn, or your blog." -# links_name: "Link Name" -# links_name_help: "What are you linking to?" -# links_link_blurb: "Link URL" -# basics_header: "Update basic info" -# basics_active: "Open to Offers" -# basics_active_help: "Want interview offers right now?" -# basics_job_title: "Desired Job Title" -# basics_job_title_help: "What role are you looking for?" -# basics_city: "City" -# basics_city_help: "City you want to work in (or live in now)." -# basics_country: "Country" -# basics_country_help: "Country you want to work in (or live in now)." -# basics_visa: "US Work Status" -# basics_visa_help: "Are you authorized to work in the US, or do you need visa sponsorship?" -# basics_looking_for: "Looking For" -# basics_looking_for_full_time: "Full-time" -# basics_looking_for_part_time: "Part-time" -# basics_looking_for_remote: "Remote" -# basics_looking_for_contracting: "Contracting" -# basics_looking_for_internship: "Internship" -# basics_looking_for_help: "What kind of developer position do you want?" -# name_header: "Fill in your name" -# name_anonymous: "Anonymous Developer" -# name_help: "Name you want employers to see, like 'Nick Winter'." -# short_description_header: "Write a short description of yourself" + active: "En recherche d'offres" + inactive: "Ne recherche pas d'offres" + complete: "terminé" + next: "Suivant" + next_city: "ville ?" + next_country: "choisissez votre pays." + next_name: "nom ?" + next_short_description: "résumez votre profil en quelques mots." + next_long_description: "décrivez le travail que vous cherchez." + next_skills: "listez au moins 5 compétances." + next_work: "décrivez votre expérience professionnelle." + next_education: "raconter votre scolarité." + next_projects: "décrivez jusqu'à 3 projets sur lesquels vous avez travaillé." + next_links: "ajouter des liens internet vers des sites personnels ou des réseaux sociaux." + next_photo: "ajouter une photo professionelle (optionnel)." + next_active: "déclarez vous ouvert aux offres pour apparaitre dans les recherches." + example_blog: "Votre blog" + example_github: "Votre GitHub" + links_header: "Liens personnels" + links_blurb: "Lien vers d'autres sites ou profils que vous souhaitez mettre en avant, comme votre GitHub, LinkedIn ou votre blog." + links_name: "Nom du lien" + links_name_help: "A quoi êtes vous lié ?" + links_link_blurb: "Lien URL" + basics_header: "Mettre à jour les information basiques" + basics_active: "Ouvert aux propositions" + basics_active_help: "Voulez-vous des offres maintenant ?" # "Want interview offers right now?" + basics_job_title: "Titre du poste souhaité" + basics_job_title_help: "Quel est le rôle que vous cherchez ?" + basics_city: "Ville" + basics_city_help: "Ville dans laquelle vous souhaitez travailler (ou dans laquelle vous vivez actuellement)." + basics_country: "Pays" + basics_country_help: "Pays dans lequel vous souhaitez travailler (ou dans lequel vous vivez actuellement)." + basics_visa: "Status de travail aux Etats-Unis" + basics_visa_help: "Etes vous autorisé à travailler aux Etats-Unis ou avez vous besoin d'un parrainage pour le visa ?" + basics_looking_for: "Recherche" + basics_looking_for_full_time: "Temps plein" + basics_looking_for_part_time: "Temps partiel" + basics_looking_for_remote: "A distance" + basics_looking_for_contracting: "Contrat" + basics_looking_for_internship: "Stage" + basics_looking_for_help: "Quel genre de poste de développeur voulez-vous ?" + name_header: "Remplissez votre nom" + name_anonymous: "Developpeur Anonyme" + name_help: "Le nom que vous souhaitez que l'employeur voie, par exemple 'Chuck Norris'." + short_description_header: "Décrivez vous en quelques mots" # short_description_blurb: "Add a blurb here that will show, at a glance, whether you might be just the developer that an employer is looking for." -# short_description: "Short Description" -# short_description_help: "Who are you, and what are you looking for? 140 characters max." -# skills_header: "Skills" -# skills_help: "Tag relevant developer skills in order of proficiency." -# long_description_header: "Detail your desired position" + short_description: "Description courte" + short_description_help: "Qui êtes vous et que recherchez vous ? 140 caractères max." + skills_header: "Compétences" + skills_help: "Notez vos compétence de développement par ordre de maitrise." + long_description_header: "Détaillez votre poste souhaité" # long_description_blurb_1: "Write a little longer section here to describe the role you would like to pursue next." # long_description_blurb_2: "Talk about how awesome you are and why it would be a good idea to hire you." # long_description: "Description" @@ -322,7 +322,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t goals: "Objectifs" success: "Succès" incomplete: "Imcoplet" -# timed_out: "Ran out of time" + timed_out: "Plus de temps" failing: "Echec" action_timeline: "Action sur la ligne de temps" click_to_select: "Clique sur une unité pour la sélectionner." @@ -465,8 +465,8 @@ module.exports = nativeDescription: "français", englishDescription: "French", t back: "Retour" revert: "Annuler" revert_models: "Annuler les modèles" -# fork_title: "Fork New Version" -# fork_creating: "Creating Fork..." + fork_title: "Fork une nouvelle version" + fork_creating: "Créer un Fork..." more: "Plus" wiki: "Wiki" live_chat: "Chat en live" @@ -533,7 +533,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t message: "Message" code: "Code" ladder: "Companion" - when: "Lorsuqe" + when: "Quand" opponent: "Adversaire" rank: "Rang" score: "Score" @@ -744,8 +744,8 @@ module.exports = nativeDescription: "français", englishDescription: "French", t simulation_explanation: "En simulant une partie, tu peux classer ton rang plus rapidement!" simulate_games: "Simuler une Partie!" simulate_all: "REINITIALISER ET SIMULER DES PARTIES" -# games_simulated_by: "Games simulated by you:" -# games_simulated_for: "Games simulated for you:" + games_simulated_by: "Parties que vous avez simulé :" + games_simulated_for: "parties simulées pour vous :" games_simulated: "Partie simulée" games_played: "Parties jouées" ratio: "Moyenne" @@ -776,11 +776,11 @@ module.exports = nativeDescription: "français", englishDescription: "French", t warmup: "Préchauffe" vs: "VS" # friends_playing: "Friends Playing" -# log_in_for_friends: "Log in to play with your friends!" -# social_connect_blurb: "Connect and play against your friends!" + log_in_for_friends: "Connectez vous pour jouer avec vos amis!" + social_connect_blurb: "Connectez vous pour jouer contre vos amis!" # invite_friends_to_battle: "Invite your friends to join you in battle!" -# fight: "Fight!" -# watch_victory: "Watch your victory" + fight: "Combattez !" + watch_victory: "Regardez votre victoire" # defeat_the: "Defeat the" tournament_ends: "Fin du tournoi" tournament_rules: "Règles du tournoi" @@ -835,7 +835,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t unknown: "Erreur inconnue." resources: -# your_sessions: "Your Sessions" + your_sessions: "vos Sessions" level: "Niveau" # social_network_apis: "Social Network APIs" facebook_status: "Statut Facebook" @@ -857,11 +857,11 @@ module.exports = nativeDescription: "français", englishDescription: "French", t # level_session: "Your Session" # opponent_session: "Opponent Session" article: "Article" -# user_names: "User Names" + user_names: "Nom d'utilisateur" # thang_names: "Thang Names" files: "Fichiers" top_simulators: "Top Simulateurs" -# source_document: "Source Document" + source_document: "Document Source" document: "Document" # sprite_sheet: "Sprite Sheet" @@ -869,7 +869,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t added: "Ajouté" modified: "Modifié" deleted: "Supprimé" -# moved_index: "Moved Index" -# text_diff: "Text Diff" -# merge_conflict_with: "MERGE CONFLICT WITH" + moved_index: "Index changé" + text_diff: "Différence de texte" + merge_conflict_with: "Fusionner les conflits avec" no_changes: "Aucuns Changements" diff --git a/app/models/Achievement.coffee b/app/models/Achievement.coffee index 5777f7bd5..88603c6ef 100644 --- a/app/models/Achievement.coffee +++ b/app/models/Achievement.coffee @@ -1,4 +1,5 @@ CocoModel = require './CocoModel' +util = require '../lib/utils' module.exports = class Achievement extends CocoModel @className: 'Achievement' @@ -6,4 +7,10 @@ module.exports = class Achievement extends CocoModel urlRoot: '/db/achievement' isRepeatable: -> - @get('proportionalTo')? \ No newline at end of file + @get('proportionalTo')? + + # TODO logic is duplicated in Mongoose Achievement schema + getExpFunction: -> + kind = @get('function')?.kind or @schema.function.default.kind + parameters = @get('function')?.parameters or @schema.function.default.parameters + return utils.functionCreators[kind](parameters) if kind of utils.functionCreators diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee index f10035168..e2cefa09d 100644 --- a/app/models/CocoModel.coffee +++ b/app/models/CocoModel.coffee @@ -97,6 +97,22 @@ class CocoModel extends Backbone.Model noty text: "#{errorMessage}: #{res.status} #{res.statusText}", layout: 'topCenter', type: 'error', killer: false, timeout: 10000 @trigger "save", @ return super attrs, options + + patch: (options) -> + return false unless @_revertAttributes + options ?= {} + options.patch = true + + attrs = {_id: @id} + keys = [] + for key in _.keys @attributes + unless _.isEqual @attributes[key], @_revertAttributes[key] + attrs[key] = @attributes[key] + keys.push key + + return unless keys.length + console.debug 'Patching', @get('name') or @, keys + @save(attrs, options) fetch: -> @jqxhr = super(arguments...) @@ -104,7 +120,6 @@ class CocoModel extends Backbone.Model @jqxhr markToRevert: -> - console.debug "Saving _revertAttributes for #{@constructor.className}: '#{@get('name')}'" if @type() is 'ThangType' @_revertAttributes = _.clone @attributes # No deep clones for these! else diff --git a/app/models/LevelComponent.coffee b/app/models/LevelComponent.coffee index 9a7569ad0..3f6ef20b6 100644 --- a/app/models/LevelComponent.coffee +++ b/app/models/LevelComponent.coffee @@ -19,8 +19,8 @@ module.exports = class LevelComponent extends CocoModel @set 'js', @compile(@get 'code') unless @get 'js' compile: (code) -> - if @get('language') and @get('language') isnt 'coffeescript' - return console.error("Can't compile", @get('language'), "-- only CoffeeScript.", @) + if @get('codeLanguage') and @get('codeLanguage') isnt 'coffeescript' + return console.error("Can't compile", @get('codeLanguage'), "-- only CoffeeScript.", @) try js = CoffeeScript.compile(code, bare: true) catch e diff --git a/app/models/LevelSystem.coffee b/app/models/LevelSystem.coffee index f2ca539b4..8ba7bb264 100644 --- a/app/models/LevelSystem.coffee +++ b/app/models/LevelSystem.coffee @@ -21,8 +21,8 @@ module.exports = class LevelSystem extends CocoModel SystemNameLoader.setName @ compile: (code) -> - if @get('language') and @get('language') isnt 'coffeescript' - return console.error("Can't compile", @get('language'), "-- only CoffeeScript.", @) + if @get('codeLanguage') and @get('codeLanguage') isnt 'coffeescript' + return console.error("Can't compile", @get('codeLanguage'), "-- only CoffeeScript.", @) try js = CoffeeScript.compile(code, bare: true) catch e diff --git a/app/schemas/models/achievement.coffee b/app/schemas/models/achievement.coffee index c064e969f..d02e9c8fd 100644 --- a/app/schemas/models/achievement.coffee +++ b/app/schemas/models/achievement.coffee @@ -52,22 +52,17 @@ _.extend(AchievementSchema.properties, description: 'For repeatables only. Denotes the field a repeatable achievement needs for its calculations' function: type: 'object' - oneOf: [ - linear: + properties: + kind: {enum: ['linear', 'logarithmic'], default: 'linear'} + parameters: type: 'object' - properties: - a: {type: 'number', default: 1}, - required: ['a'] - description: 'f(x) = a * x' - logarithmic: - type:'object' properties: a: {type: 'number', default: 1} b: {type: 'number', default: 1} - required: ['a', 'b'] - description: 'f(x) = a * ln(1/b * (x + b))' - ] - default: linear: a: 1 + c: {type: 'number', default: 1} + default: {kind: 'linear', parameters: a: 1} + required: ['kind', 'parameters'] + additionalProperties: false ) AchievementSchema.definitions = {} diff --git a/app/schemas/models/earned_achievement.coffee b/app/schemas/models/earned_achievement.coffee index 9b2c50c19..e250834d4 100644 --- a/app/schemas/models/earned_achievement.coffee +++ b/app/schemas/models/earned_achievement.coffee @@ -20,15 +20,11 @@ module.exports = href: '/db/achievement/{($)}' } ] - collection: - type: 'string' - achievementName: - type: 'string' - created: - type: 'date' - changed: - type: 'date' - achievedAmount: - type: 'number' - notified: - type: 'boolean' \ No newline at end of file + collection: type: 'string' + achievementName: type: 'string' + created: type: 'date' + changed: type: 'date' + achievedAmount: type: 'number' + earnedPoints: type: 'number' + previouslyAchievedAmount: {type: 'number', default: 0} + notified: type: 'boolean' diff --git a/app/schemas/models/level_component.coffee b/app/schemas/models/level_component.coffee index 74d83dcbe..ea8d777ae 100644 --- a/app/schemas/models/level_component.coffee +++ b/app/schemas/models/level_component.coffee @@ -70,13 +70,13 @@ DependencySchema = c.object { LevelComponentSchema = c.object { title: "Component" description: "A Component which can affect Thang behavior." - required: ["system", "name", "description", "code", "dependencies", "propertyDocumentation", "language"] + required: ["system", "name", "description", "code", "dependencies", "propertyDocumentation", "codeLanguage"] "default": system: "ai" name: "AttacksSelf" description: "This Component makes the Thang attack itself." code: attackSelfCode - language: "coffeescript" + codeLanguage: "coffeescript" dependencies: [] # TODO: should depend on something by default propertyDocumentation: [] } @@ -95,7 +95,7 @@ _.extend LevelComponentSchema.properties, type: "string" maxLength: 2000 "default": "This Component makes the Thang attack itself." - language: + codeLanguage: type: "string" title: "Language" description: "Which programming language this Component is written in." diff --git a/app/schemas/models/level_system.coffee b/app/schemas/models/level_system.coffee index 1804de363..63c12919b 100644 --- a/app/schemas/models/level_system.coffee +++ b/app/schemas/models/level_system.coffee @@ -54,12 +54,12 @@ DependencySchema = c.object { LevelSystemSchema = c.object { title: "System" description: "A System which can affect Level behavior." - required: ["name", "description", "code", "dependencies", "propertyDocumentation", "language"] + required: ["name", "description", "code", "dependencies", "propertyDocumentation", "codeLanguage"] "default": name: "JitterSystem" description: "This System makes all idle, movable Thangs jitter around." code: jitterSystemCode - language: "coffeescript" + codeLanguage: "coffeescript" dependencies: [] # TODO: should depend on something by default propertyDocumentation: [] } @@ -72,7 +72,7 @@ _.extend LevelSystemSchema.properties, type: "string" maxLength: 2000 "default": "This System doesn't do anything yet." - language: + codeLanguage: type: "string" title: "Language" description: "Which programming language this System is written in." diff --git a/app/schemas/models/user.coffee b/app/schemas/models/user.coffee index 5a0209a39..623cf58f4 100644 --- a/app/schemas/models/user.coffee +++ b/app/schemas/models/user.coffee @@ -113,8 +113,10 @@ UserSchema = c.object {}, signedEmployerAgreement: c.object {}, linkedinID: c.shortString {title:"LinkedInID", description: "The user's LinkedIn ID when they signed the contract."} date: c.date {title: "Date signed employer agreement"} - data: c.object {description: "Cached LinkedIn data slurped from profile."} + data: c.object {description: "Cached LinkedIn data slurped from profile.", additionalProperties: true} points: {type:'number'} + activity: {type: 'object', description: 'Summary statistics about user activity', additionalProperties: c.activity} + c.extendBasicProperties UserSchema, 'user' diff --git a/app/schemas/schemas.coffee b/app/schemas/schemas.coffee index 04771e017..d2a72551e 100644 --- a/app/schemas/schemas.coffee +++ b/app/schemas/schemas.coffee @@ -8,7 +8,7 @@ combine = (base, ext) -> return base unless ext? return _.extend(base, ext) -urlPattern = '^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-‌​\.\?\,\'\/\\\+&%\$#_=]*)?$' +urlPattern = '^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&%\$#_=]*)?$' # Common schema properties me.object = (ext, props) -> combine {type: 'object', additionalProperties: false, properties: props or {}}, ext @@ -17,7 +17,7 @@ me.shortString = (ext) -> combine({type: 'string', maxLength: 100}, ext) me.pct = (ext) -> combine({type: 'number', maximum: 1.0, minimum: 0.0}, ext) me.date = (ext) -> combine({type: ['object', 'string'], format: 'date-time'}, ext) # should just be string (Mongo ID), but sometimes mongoose turns them into objects representing those, so we are lenient -me.objectId = (ext) -> schema = combine(['object', 'string'], ext) +me.objectId = (ext) -> schema = combine({type: ['object', 'string'] }, ext) me.url = (ext) -> combine({type: 'string', format: 'url', pattern: urlPattern}, ext) PointSchema = me.object {title: "Point", description: "An {x, y} coordinate point.", format: "point2d", required: ["x", "y"]}, @@ -54,7 +54,7 @@ basicProps = (linkFragment) -> me.extendBasicProperties = (schema, linkFragment) -> schema.properties = {} unless schema.properties? _.extend(schema.properties, basicProps(linkFragment)) - + # PATCHABLE patchableProps = -> @@ -65,7 +65,7 @@ patchableProps = -> allowPatches: { type: 'boolean' } watchers: me.array({title:'Watchers'}, me.objectId(links: [{rel: 'extra', href: "/db/user/{($)}"}])) - + me.extendPatchableProperties = (schema) -> schema.properties = {} unless schema.properties? _.extend(schema.properties, patchableProps()) @@ -176,3 +176,9 @@ me.codeSnippet = (mode) -> code: {type: 'string', title: 'Snippet', default: '', description: 'Code snippet. Use ${1:defaultValue} syntax to add flexible arguments'} # code: {type: 'string', format: 'ace', aceMode: 'ace/mode/'+mode, title: 'Snippet', default: '', description: 'Code snippet. Use ${1:defaultValue} syntax to add flexible arguments'} tab: {type: 'string', description: 'Tab completion text. Will be expanded to the snippet if typed and hit tab.'} + +me.activity = me.object {description: "Stats on an activity"}, + first: me.date() + last: me.date() + count: {type: 'integer', minimum: 0} + diff --git a/app/styles/account/profile.sass b/app/styles/account/profile.sass index 8774604a9..797bcad4d 100644 --- a/app/styles/account/profile.sass +++ b/app/styles/account/profile.sass @@ -75,7 +75,7 @@ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif color: #555 - ul.links, ul.projects + ul.links, ul.projects, ul.sessions margin: 0 padding: 0 @@ -140,7 +140,7 @@ background-color: rgb(177, 55, 25) padding: 15px font-size: 20px - + .middle-column width: $middle-width - 2 * $middle-padding padding-left: $middle-padding diff --git a/app/styles/editor/achievement/edit.sass b/app/styles/editor/achievement/edit.sass new file mode 100644 index 000000000..7177978d3 --- /dev/null +++ b/app/styles/editor/achievement/edit.sass @@ -0,0 +1,12 @@ +#editor-achievement-edit-view + .treema-root + margin: 28px 0px 20px + + button + float: right + margin-top: 15px + margin-left: 10px + + textarea + width: 92% + height: 300px diff --git a/app/styles/employers.sass b/app/styles/employers.sass index 1c7538cf2..c04ddf56e 100644 --- a/app/styles/employers.sass +++ b/app/styles/employers.sass @@ -1,6 +1,32 @@ #employers-view - #see-candidates - cursor: pointer + + h1, h2, h3 + font: Arial + + .see-candidates-header + margin: 30px + text-align: center + + #see-candidates + cursor: pointer + + .employer_icon + width: 125px + float: left + margin: 0px 15px 15px 0px + + .information_row + height: 150px + padding-right: 15px + + #leftside + width: 500px + float: left + + #rightside + width: 500px + float: left + .tablesorter //img // display: none @@ -36,3 +62,9 @@ min-width: 50px td:nth-child(7) select min-width: 100px + + #outer-content-wrapper, #intermediate-content-wrapper, #inner-content-wrapper + background: #949494 + + .main-content-area + background-color: #EAEAEA diff --git a/app/styles/notify.sass b/app/styles/notify.sass index 0a0b47cf1..abf50bb70 100644 --- a/app/styles/notify.sass +++ b/app/styles/notify.sass @@ -45,3 +45,6 @@ font-family: Bangers font-size: 16px float: right + +.progress-bar-white + background-color: white diff --git a/app/styles/play/ladder/ladder.sass b/app/styles/play/ladder/ladder.sass index 51ecbd02e..565db3a94 100644 --- a/app/styles/play/ladder/ladder.sass +++ b/app/styles/play/ladder/ladder.sass @@ -101,9 +101,6 @@ background-image: none color: white - td - padding: 1px 2px - #must-log-in button margin-right: 10px @@ -135,6 +132,12 @@ img margin-right: 10px + #winners + .win + color: #172 + .loss + color: #712 + @media only screen and (max-width: 800px) #ladder-view #level-column img diff --git a/app/styles/play/ladder/ladder_tab.sass b/app/styles/play/ladder/ladder_tab.sass index f722faf18..cdc5025aa 100644 --- a/app/styles/play/ladder/ladder_tab.sass +++ b/app/styles/play/ladder/ladder_tab.sass @@ -47,3 +47,6 @@ position: absolute right: 15px bottom: -5px + + td + padding: 1px 2px diff --git a/app/styles/play/ladder/my_matches_tab.sass b/app/styles/play/ladder/my_matches_tab.sass index 66b03d72b..99e01f997 100644 --- a/app/styles/play/ladder/my_matches_tab.sass +++ b/app/styles/play/ladder/my_matches_tab.sass @@ -35,4 +35,5 @@ tr.loss .state-cell color: #712 - + td + padding: 1px 2px diff --git a/app/templates/account/profile.jade b/app/templates/account/profile.jade index 58c88944f..0b16838a3 100644 --- a/app/templates/account/profile.jade +++ b/app/templates/account/profile.jade @@ -37,18 +37,13 @@ block content if profileApproved button.btn.btn-success#toggle-job-profile-approved(disabled=!me.isAdmin()) i.icon-eye-open - span(data-i18n='account_profile.approved') Approved + span(data-i18n='account_profile.featured') Featured else if me.isAdmin() button.btn#toggle-job-profile-approved i.icon-eye-close - span(data-i18n='account_profile.not_approved') Not Approved + span(data-i18n='account_profile.not_featured') Not Featured if me.isAdmin() && !myProfile button.btn.edit-settings-button#enter-espionage-mode 007 - //if editing && myProfile - // a.sample-profile(href="http://codecombat.com/images/pages/account/profile/sample_profile.png", target="_blank") - // button.btn - // i.icon-user - // span(data-i18n="account_settings.sample_profile") See a sample profile if profile && allowedToViewJobProfile div(class="job-profile-container" + (editing ? " editable-profile" : "")) @@ -175,6 +170,19 @@ block content span(data-i18n="account_profile.contact") Contact | #{profile.name.split(' ')[0]} + if !editing && sessions.length + h3(data-i18n="account_profile.player_code") Player Code + ul.sessions + each session in sessions + li + - var sessionLink = "/play/level/" + session.levelID + "?team=" + (session.team || 'humans') + (myProfile ? '' : "&session=" + session._id); + a(href=sessionLink) + span= session.levelName + if session.team + span #{session.team} + if session.codeLanguage != 'javascript' + span - #{{coffeescript: 'CoffeeScript', python: 'Python', lua: 'Lua', io: 'Io', clojure: 'Clojure'}[session.codeLanguage]} + .middle-column.full-height-column .sub-column #name-container.editable-section @@ -222,11 +230,11 @@ block content h3.edit-label Tag your programming skills each skill in ["python", "coffeescript", "node", "ios", "objective-c", "javascript", "app-engine", "mongodb", "web dev", "django", "backbone"] code.edit-example-tag= skill - span + span else each skill in profile.skills code= skill - span + span form.editable-form .editable-icon.glyphicon.glyphicon-remove @@ -267,7 +275,7 @@ block content img.header-icon(src="/images/pages/account/profile/work.png", alt="") span(data-i18n="account_profile.work_experience") Work Experience | - #{profile.experience} - | + | span(data-i18n=profile.experience == 1 ? "units.year" : "units.years") each job in profile.work if job.role && job.employer @@ -454,9 +462,9 @@ block content else if user .public-profile-container h2 - span(data-i18n="account_profile.profile_for_prefix") Profile for + span(data-i18n="account_profile.profile_for_prefix") Profile for span= user.get('name') || "Anonymous Wizard" - span(data-i18n="account_profile.profile_for_suffix") + span(data-i18n="account_profile.profile_for_suffix") img.profile-photo(src=user.getPhotoURL(256)) @@ -465,8 +473,8 @@ block content else .public-profile-container h2 - span(data-i18n="account_profile.profile_for_prefix") Profile for + span(data-i18n="account_profile.profile_for_prefix") Profile for span= userID - span(data-i18n="account_profile.profile_for_suffix") - | + span(data-i18n="account_profile.profile_for_suffix") + | span(data-i18n="loading_error.not_found") diff --git a/app/templates/admin.jade b/app/templates/admin.jade index 229f51607..11ee67ee8 100644 --- a/app/templates/admin.jade +++ b/app/templates/admin.jade @@ -23,6 +23,8 @@ block content a(href="/admin/users", data-i18n="admin.av_entities_users_url") Users li a(href="/admin/level_sessions", data-i18n="admin.av_entities_active_instances_url") Active Instances + li + a(href="/admin/employer_list", data-i18n="admin.av_entities_employer_list_url") Employer List h4(data-i18n="admin.av_other_sub_title") Other @@ -31,3 +33,11 @@ block content a(href="/admin/base", data-i18n="admin.av_other_debug_base_url") Base (for debugging base.jade) li a(href="/admin/clas", data-i18n="admin.clas") CLAs + + hr + + h3 Achievements + p This is just some stuff for temporary achievement testing. Should be replaced by a demo system. + + input#increment-field(type="text") + a.btn.btn-secondary#increment-button(href="#") Increment diff --git a/app/templates/admin/employer_list.jade b/app/templates/admin/employer_list.jade new file mode 100644 index 000000000..678d7d228 --- /dev/null +++ b/app/templates/admin/employer_list.jade @@ -0,0 +1,65 @@ +extends /templates/base + +block content + + if !me.isAdmin() + h1 Admin Only + else + h1(data-i18n="admin.av_entities_employer_list_url") Employer List + + p + | We currently have + if employers.length + | #{employers.length} + else + | ... + | employers in the system. + + if employers.length + table.table.table-condensed.table-hover.table-responsive.tablesorter + thead + tr + th(data-i18n="general.name") Name + th Company + th(data-i18n="general.email") Email + th Logins + th Candidates Viewed + th Candidates Contacted + th Signed Up + + tbody + for employer, index in employers + - var activity = employer.get('activity') || {}; + - var linkedIn = employer.get('signedEmployerAgreement').data + tr(data-employer-id=employer.id) + td + img(src=employer.getPhotoURL(50), height=50) + p + if employer.get('firstName') + span= employer.get('firstName') + ' ' + employer.get('lastName') + if employer.get('name') + | - + else if linkedIn.firstName + span= linkedIn.firstName + ' ' + linkedIn.lastName + if employer.get('name') + | - + if employer.get('name') + span= employer.get('name') + if !employer.get('firstName') && !linkedIn.firstName && !employer.get('name') + | Anoner + td + a(href=employer.get('signedEmployerAgreement').data.publicProfileUrl)= employer.get('employerAt') + td= employer.get('email') + for a in ['login', 'view_candidate', 'contact_candidate'] + - var act = activity[a]; + if act + td + strong= act.count + | + br + span= moment(act.first).fromNow() + br + span= moment(act.last).fromNow() + else + td 0 + td(data-employer-age=(new Date() - new Date(employer.get('signedEmployerAgreement').date)) / 86400 / 1000)= moment(employer.get('signedEmployerAgreement').date).fromNow() diff --git a/app/templates/editor/achievement/edit.jade b/app/templates/editor/achievement/edit.jade index 2cee658bc..42b59de17 100644 --- a/app/templates/editor/achievement/edit.jade +++ b/app/templates/editor/achievement/edit.jade @@ -11,19 +11,21 @@ block content li.active | #{achievement.attributes.name} - button(data-i18n="common.save", disabled=authorized === true ? undefined : "true").btn.btn-primary#save-button Save + button(data-i18n="", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#recalculate-button Recalculate + button(data-i18n="common.save", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#save-button Save - h3(data-i18n="achievement.edit_achievement_title") Edit Achievement - span - |: "#{achievement.attributes.name}" + h3(data-i18n="achievement.edit_achievement_title") Edit Achievement + span + |: "#{achievement.attributes.name}" - #achievement-treema + #achievement-treema - #achievement-view + #achievement-view - hr + hr + + div#error-view - div#error-view else .alert.alert-danger span Admin only. Turn around. diff --git a/app/templates/employers.jade b/app/templates/employers.jade index dcb6d2cac..774e1251f 100644 --- a/app/templates/employers.jade +++ b/app/templates/employers.jade @@ -2,66 +2,117 @@ extends /templates/base block content - h1(data-i18n="employers.want_to_hire_our_players") Want to hire expert CodeCombat players? + h1(data-i18n="employers.want_to_hire_our_players") Hire CodeCombat Players + + div#info_wrapper + + div#leftside + + div.information_row + + img(class="employer_icon" src="/images/pages/employer/employer_icon1.png") + + h2(data-i18n="employers.what") What is CodeCombat? + + p(data-i18n="employers.what_blurb") CodeCombat is a multiplayer browser programming game. Players write code to control their forces in battle against other developers. We support JavaScript, Python, Lua, Clojure, CoffeeScript, and Io. + + div.information_row + + img(class="employer_icon" src="/images/pages/employer/employer_icon3.png") + + h2(data-i18n="employers.who") Who Are the Players? + + p(data-i18n="employers.who_blurb") CodeCombateers are software developers who enjoy using their programming skills to play games. They range from college seniors at top 20 engineering programs to 20-year industry veterans. + + div#rightside + + div.information_row + + img(class="employer_icon" src="/images/pages/employer/employer_icon2.png") + + h2(data-i18n="employers.how") How Do We Find Developers? + + p(data-i18n="employers.how_blurb") We host competitive tournaments to attract competitive software engieneers. We then use in-house algorithms to identify the best players among the top 5% of tournament winners. + + div.information_row + + img(class="employer_icon" src="/images/pages/employer/employer_icon4.png") + + h2(data-i18n="employers.why") Why Hire Through Us? + + p + span(data-i18n="employers.why_blurb_1") We will save you time. Every CodeCombateer we feaure is + strong(data-i18n="employers.why_blurb_2") looking for work + span(data-i18n="employers.why_blurb_3") , has + strong(data-i18n="employers.why_blurb_4") demonstrated top notch technical skills + span(data-i18n="employers.why_blurb_5") , and has been + strong(data-i18n="employers.why_blurb_6") personally screened by us + span(data-i18n="employers.why_blurb_7") . Stop screening and start hiring. + + if !isEmployer && !me.isAdmin() + h3.see-candidates-header + a#see-candidates(title='Contact', tabindex=-1, data-toggle="coco-modal", data-target="modal/employer_signup", data-i18n="employers.see_candidates") Click here to see our candidates - p - span(data-i18n="employers.candidates_count_prefix") We currently have - if candidates.length - | #{candidates.length} - else - span(data-i18n="employers.candidates_count_many") many - | - span(data-i18n="employers.candidates_count_suffix") highly skilled and vetted developers looking for work. - if !isEmployer - - h3 - a#see-candidates(title='Contact', tabindex=-1, data-toggle="coco-modal", data-target="modal/employer_signup", data-i18n="employers.see_candidates") Click here to see our candidates - if candidates.length - table.table.table-condensed.table-hover.table-responsive.tablesorter - thead - tr - th(data-i18n="general.name") Name - th(data-i18n="employers.candidate_location") Location - th(data-i18n="employers.candidate_looking_for") Looking For - th(data-i18n="employers.candidate_role") Role - th(data-i18n="employers.candidate_top_skills") Top Skills - th(data-i18n="employers.candidate_years_experience") Yrs Exp - th(data-i18n="employers.candidate_last_updated") Last Updated - if me.isAdmin() - th(data-i18n="employers.candidate_approved") Us? - th(data-i18n="employers.candidate_active") Them? - - tbody - for candidate, index in candidates - - var profile = candidate.get('jobProfile'); - - var authorized = candidate.id; // If we have the id, then we are authorized. - tr(data-candidate-id=candidate.id, id=candidate.id) - td - if authorized - img(src=candidate.getPhotoURL(50), alt=profile.name, title=profile.name, height=50) - p= profile.name - else - img(src="/images/pages/contribute/archmage.png", alt="", title="Sign up as an employer to see our candidates", width=50) - p Developer ##{index + 1} - if profile.country == 'USA' - td= profile.city - else - td= profile.country - td= profile.lookingFor - td= profile.jobTitle - td - each skill in profile.skills.slice(0, 10) - code= skill - span - td= profile.experience - td(data-profile-age=(new Date() - new Date(profile.updated)) / 86400 / 1000)= moment(profile.updated).fromNow() - if me.isAdmin() - if candidate.get('jobProfileApproved') - td ✓ - else - td ✗ - if profile.active - td ✓ - else - td ✗ \ No newline at end of file + + ul.nav.nav-pills + li.active + a(href="#featured-candidates", data-toggle="tab") + span(data-i18n="employers.featured_developers") Featured Developers + | (#{featuredCandidates.length}) + if otherCandidates.length + li + a(href="#other-candidates", data-toggle="tab") + span(data-i18n="employers.other_developers") Other Developers + | (#{otherCandidates.length}) + if me.isAdmin() && inactiveCandidates.length + li + a(href="#inactive-candidates", data-toggle="tab") + span(data-i18n="employers.inactive_developers") Inactive Developers + | (#{inactiveCandidates.length}) + + div.tab-content + for area, tabIndex in [{id: "featured-candidates", candidates: featuredCandidates}, {id: "other-candidates", candidates: otherCandidates}, {id: "inactive-candidates", candidates: inactiveCandidates}] + div(class="tab-pane well" + (tabIndex ? "" : " active"), id=area.id) + table.table.table-condensed.table-hover.table-responsive.tablesorter + thead + tr + th(data-i18n="general.name") Name + th(data-i18n="employers.candidate_location") Location + th(data-i18n="employers.candidate_looking_for") Looking For + th(data-i18n="employers.candidate_role") Role + th(data-i18n="employers.candidate_top_skills") Top Skills + th(data-i18n="employers.candidate_years_experience") Yrs Exp + th(data-i18n="employers.candidate_last_updated") Last Updated + if me.isAdmin() && area.id == 'inactive-candidates' + th ✓? + + tbody + for candidate, index in area.candidates + - var profile = candidate.get('jobProfile'); + - var authorized = candidate.id; // If we have the id, then we are authorized. + tr(data-candidate-id=candidate.id, id=candidate.id) + td + if authorized + img(src=candidate.getPhotoURL(50), alt=profile.name, title=profile.name, height=50) + p= profile.name + else + img(src="/images/pages/contribute/archmage.png", alt="", title="Sign up as an employer to see our candidates", width=50) + p Developer ##{index + 1 + (area.id == 'featured-candidates' ? 0 : featuredCandidates.length)} + if profile.country == 'USA' + td= profile.city + else + td= profile.country + td= profile.lookingFor + td= profile.jobTitle + td + each skill in profile.skills.slice(0, 10) + code= skill + span + td= profile.experience + td(data-profile-age=(new Date() - new Date(profile.updated)) / 86400 / 1000)= moment(profile.updated).fromNow() + if me.isAdmin() && area.id == 'inactive-candidates' + if candidate.get('jobProfileApproved') + td ✓ + else + td ✗ diff --git a/app/templates/kinds/search.jade b/app/templates/kinds/search.jade index bbf7de514..59a9c880c 100644 --- a/app/templates/kinds/search.jade +++ b/app/templates/kinds/search.jade @@ -37,6 +37,7 @@ block content h3(data-i18n="play_level.tip_reticulating") Reticulating Splines... .progress.progress-striped.active .progress-bar + else .alert.alert-danger span Admin only. Turn around. diff --git a/app/templates/modal/confirm.jade b/app/templates/modal/confirm.jade new file mode 100644 index 000000000..f7d746de4 --- /dev/null +++ b/app/templates/modal/confirm.jade @@ -0,0 +1,11 @@ +extends /templates/modal/modal_base + +block modal-header-content + h3 #{confirmTitle} + +block modal-body-content + p #{confirmBody} + +block modal-footer-content + button.btn.btn-secondary#decline-button(type="button", data-dismiss="modal") #{confirmDecline} + button.btn.btn-primary#confirm-button(type="button", data-dismiss=closeOnConfirm === true ? "modal" : undefined) #{confirmConfirm} diff --git a/app/templates/modal/modal_base.jade b/app/templates/modal/modal_base.jade index e2c2d527f..c8374033e 100644 --- a/app/templates/modal/modal_base.jade +++ b/app/templates/modal/modal_base.jade @@ -24,4 +24,4 @@ block modal-footer .modal-footer block modal-footer-content - button.btn.btn-primary(type="button", data-dismiss="modal", aria-hidden="true", data-i18n="modal.okay") Okay \ No newline at end of file + button.btn.btn-primary(type="button", data-dismiss="modal", aria-hidden="true", data-i18n="modal.okay") Okay diff --git a/app/templates/play/ladder/ladder.jade b/app/templates/play/ladder/ladder.jade index c786ed655..42f55cf29 100644 --- a/app/templates/play/ladder/ladder.jade +++ b/app/templates/play/ladder/ladder.jade @@ -11,13 +11,22 @@ block content if level.get('name') == 'Greed' .tournament-blurb h2 - span(data-i18n="ladder.tournament_ends") Tournament ends + //span(data-i18n="ladder.tournament_ends") Tournament ends + span(data-i18n="ladder.tournament_ended") Tournament ended | #{tournamentTimeLeft} p span(data-i18n="ladder.tournament_blurb") Write code, collect gold, build armies, crush foes, win prizes, and upgrade your career in our $40,000 Greed tournament! Check out the details | a(href="http://blog.codecombat.com/multiplayer-programming-tournament", data-i18n="ladder.tournament_blurb_blog") on our blog | . + p + strong Tournament ended! + a(href="#winners") Behold the winners + | . Thanks for playing! + p + | Want to commiserate? Head over to + a(href="http://discourse.codecombat.com/") the forum + | and discuss your strategies, your triumphs, and your turmoils. .sponsor-logos a(href="https://heapanalytics.com/") @@ -59,9 +68,11 @@ block content a(href="#simulate", data-toggle="tab", data-i18n="ladder.simulate") Simulate if level.get('name') == 'Greed' li - a(href="#prizes", data-toggle="tab", data-i18n="ladder.prizes") Prizes + a(href="#prizes", data-toggle="tab", data-i18n="ladder_prizes.prizes") Prizes li a(href="#rules", data-toggle="tab", data-i18n="ladder.rules") Rules + li + a(href="#winners", data-toggle="tab", data-i18n="ladder.winners") Winners div.tab-content .tab-pane.active.well#ladder @@ -70,628 +81,668 @@ block content #my-matches-tab-view .tab-pane.well#simulate #simulate-tab-view - .tab-pane.well#prizes - h1(data-i18n="ladder_prizes.title") Tournament Prizes - p - span(data-i18n="ladder_prizes.blurb_1") These prizes will be awarded according to - | - a(href="#rules", data-i18n="ladder_prizes.blurb_2") the tournament rules - | - span(data-i18n="ladder_prizes.blurb_3") to the top human and ogre players. - | - strong(data-i18n="ladder_prizes.blurb_4") Two teams means double the prizes! - | - span(data-i18n="ladder_prizes.blurb_5") (There will be two first place winners, two second-place winners, etc.) - - table#prize_table.table - thead - tr - td(data-i18n="ladder_prizes.rank") Rank - td(data-i18n="ladder_prizes.prizes") Prizes - td(data-i18n="ladder_prizes.total_value") Total Value - tbody - tr - td 1st - td - ul.list-unstyled - li - img(src=base + "cash1.png") - span $512 - span(data-i18n="ladder_prizes.in_cash") in cash - li - img(src=base + "custom_wizard.png") - span(data-i18n="ladder_prizes.custom_wizard") Custom CodeCombat Wizard - li - img(src=base + "custom_avatar.png") - span(data-i18n="ladder_prizes.custom_avatar") Custom CodeCombat avatar - li - img(src=base + "heap.png") - span - a(href="https://heapanalytics.com/") Heap Analytics - | - span(data-i18n="ladder_prizes.heap") for six months of "Startup" access - | - $354 - li - img(src=base + "firebase.png") - span - a(href="https://www.firebase.com/") Firebase - | - span(data-i18n="ladder_prizes.credits") credits - | - $300 - li - img(src=base + "one_month.png") - span - a(href="https://onemonthrails.com/") One Month Rails - | - span(data-i18n="ladder_prizes.one_month_coupon") coupon: choose either Rails or HTML - | - $99 - li - img(src=base + "webstorm.png") - span - a(href="http://www.jetbrains.com/webstorm/") Webstorm - | - span(data-i18n="ladder_prizes.license") license - | - $49 - li - img(src=base + "oreilly.png") - span - a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly - | - span(data-i18n="ladder_prizes.oreilly") ebook of your choice - | - $40 - li - img(src=base + "aws.png") - span - a(href="http://aws.amazon.com/") Amazon Web Services - | - span(data-i18n="ladder_prizes.credits") credits - | - $50 - td $2054 - tr - td 2nd - td - ul.list-unstyled - li - img(src=base + "cash2.png") - span $256 - span(data-i18n="ladder_prizes.in_cash") in cash - li - img(src=base + "custom_avatar.png") - span(data-i18n="ladder_prizes.custom_avatar") Custom CodeCombat avatar - li - img(src=base + "heap.png") - span - a(href="https://heapanalytics.com/") Heap Analytics - | - span(data-i18n="ladder_prizes.heap") for six months of "Startup" access - | - $354 - li - img(src=base + "firebase.png") - span - a(href="https://www.firebase.com/") Firebase - | - span(data-i18n="ladder_prizes.credits") credits - | - $300 - li - img(src=base + "one_month.png") - span - a(href="https://onemonthrails.com/") One Month Rails - | - span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML - | - $30 - li - img(src=base + "webstorm.png") - span - a(href="http://www.jetbrains.com/webstorm/") Webstorm - | - span(data-i18n="ladder_prizes.license") license - | - $49 - li - img(src=base + "oreilly.png") - span - a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly - | - span(data-i18n="ladder_prizes.oreilly") ebook of your choice - | - $40 - li - img(src=base + "aws.png") - span - a(href="http://aws.amazon.com/") Amazon Web Services - | - span(data-i18n="ladder_prizes.credits") credits - | - $50 - td $1229 - tr - td 3rd - td - ul.list-unstyled - li - img(src=base + "cash2.png") - span $128 - span(data-i18n="ladder_prizes.in_cash") in cash - li - img(src=base + "custom_avatar.png") - span(data-i18n="ladder_prizes.custom_avatar") Custom CodeCombat avatar - li - img(src=base + "heap.png") - span - a(href="https://heapanalytics.com/") Heap Analytics - | - span(data-i18n="ladder_prizes.heap") for six months of "Startup" access - | - $354 - li - img(src=base + "firebase.png") - span - a(href="https://www.firebase.com/") Firebase - | - span(data-i18n="ladder_prizes.credits") credits - | - $300 - li - img(src=base + "one_month.png") - span - a(href="https://onemonthrails.com/") One Month Rails - | - span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML - | - $30 - li - img(src=base + "webstorm.png") - span - a(href="http://www.jetbrains.com/webstorm/") Webstorm - | - span(data-i18n="ladder_prizes.license") license - | - $49 - li - img(src=base + "oreilly.png") - span - a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly - | - span(data-i18n="ladder_prizes.oreilly") ebook of your choice - | - $40 - li - img(src=base + "aws.png") - span - a(href="http://aws.amazon.com/") Amazon Web Services - | - span(data-i18n="ladder_prizes.credits") credits - | - $50 - td $1101 - tr - td 4th - td - ul.list-unstyled - li - img(src=base + "cash3.png") - span $64 - span(data-i18n="ladder_prizes.in_cash") in cash - li - img(src=base + "heap.png") - span - a(href="https://heapanalytics.com/") Heap Analytics - | - span(data-i18n="ladder_prizes.heap") for six months of "Startup" access - | - $354 - li - img(src=base + "firebase.png") - span - a(href="https://www.firebase.com/") Firebase - | - span(data-i18n="ladder_prizes.credits") credits - | - $300 - li - img(src=base + "one_month.png") - span - a(href="https://onemonthrails.com/") One Month Rails - | - span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML - | - $30 - li - img(src=base + "webstorm.png") - span - a(href="http://www.jetbrains.com/webstorm/") Webstorm - | - span(data-i18n="ladder_prizes.license") license - | - $49 - li - img(src=base + "oreilly.png") - span - a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly - | - span(data-i18n="ladder_prizes.oreilly") ebook of your choice - | - $40 - li - img(src=base + "aws.png") - span - a(href="http://aws.amazon.com/") Amazon Web Services - | - span(data-i18n="ladder_prizes.credits") credits - | - $50 - td $887 - tr - td 5th - td - ul.list-unstyled - li - img(src=base + "cash3.png") - span $32 - span(data-i18n="ladder_prizes.in_cash") in cash - li - img(src=base + "heap.png") - span - a(href="https://heapanalytics.com/") Heap Analytics - | - span(data-i18n="ladder_prizes.heap") for six months of "Startup" access - | - $354 - li - img(src=base + "firebase.png") - span - a(href="https://www.firebase.com/") Firebase - | - span(data-i18n="ladder_prizes.credits") credits - | - $300 - li - img(src=base + "one_month.png") - span - a(href="https://onemonthrails.com/") One Month Rails - | - span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML - | - $30 - li - img(src=base + "webstorm.png") - span - a(href="http://www.jetbrains.com/webstorm/") Webstorm - | - span(data-i18n="ladder_prizes.license") license - | - $49 - li - img(src=base + "oreilly.png") - span - a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly - | - span(data-i18n="ladder_prizes.oreilly") ebook of your choice - | - $40 - li - img(src=base + "aws.png") - span - a(href="http://aws.amazon.com/") Amazon Web Services - | - span(data-i18n="ladder_prizes.credits") credits - | - $50 - td $855 - tr - td 6th - td - ul.list-unstyled - li - img(src=base + "cash3.png") - span $16 - span(data-i18n="ladder_prizes.in_cash") in cash - li - img(src=base + "firebase.png") - span - a(href="https://www.firebase.com/") Firebase - | - span(data-i18n="ladder_prizes.credits") credits - | - $300 - li - img(src=base + "one_month.png") - span - a(href="https://onemonthrails.com/") One Month Rails - | - span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML - | - $30 - li - img(src=base + "webstorm.png") - span - a(href="http://www.jetbrains.com/webstorm/") Webstorm - | - span(data-i18n="ladder_prizes.license") license - | - $49 - li - img(src=base + "oreilly.png") - span - a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly - | - span(data-i18n="ladder_prizes.oreilly") ebook of your choice - | - $40 - li - img(src=base + "aws.png") - span - a(href="http://aws.amazon.com/") Amazon Web Services - | - span(data-i18n="ladder_prizes.credits") credits - | - $50 - td $485 - tr - td 7th - td - ul.list-unstyled - li - img(src=base + "cash3.png") - span $8 - span(data-i18n="ladder_prizes.in_cash") in cash - li - img(src=base + "firebase.png") - span - a(href="https://www.firebase.com/") Firebase - | - span(data-i18n="ladder_prizes.credits") credits - | - $300 - li - img(src=base + "one_month.png") - span - a(href="https://onemonthrails.com/") One Month Rails - | - span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML - | - $30 - li - img(src=base + "webstorm.png") - span - a(href="http://www.jetbrains.com/webstorm/") Webstorm - | - span(data-i18n="ladder_prizes.license") license - | - $49 - li - img(src=base + "oreilly.png") - span - a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly - | - span(data-i18n="ladder_prizes.oreilly") ebook of your choice - | - $40 - li - img(src=base + "aws.png") - span - a(href="http://aws.amazon.com/") Amazon Web Services - | - span(data-i18n="ladder_prizes.credits") credits - | - $50 - td $477 - tr - td 8th - td - ul.list-unstyled - li - img(src=base + "cash3.png") - span $4 - span(data-i18n="ladder_prizes.in_cash") in cash - li - img(src=base + "firebase.png") - span - a(href="https://www.firebase.com/") Firebase - | - span(data-i18n="ladder_prizes.credits") credits - | - $300 - li - img(src=base + "one_month.png") - span - a(href="https://onemonthrails.com/") One Month Rails - | - span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML - | - $30 - li - img(src=base + "webstorm.png") - span - a(href="http://www.jetbrains.com/webstorm/") Webstorm - | - span(data-i18n="ladder_prizes.license") license - | - $49 - li - img(src=base + "oreilly.png") - span - a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly - | - span(data-i18n="ladder_prizes.oreilly") ebook of your choice - | - $40 - li - img(src=base + "aws.png") - span - a(href="http://aws.amazon.com/") Amazon Web Services - | - span(data-i18n="ladder_prizes.credits") credits - | - $50 - td $473 - tr - td 9th - td - ul.list-unstyled - li - img(src=base + "cash3.png") - span $2 - span(data-i18n="ladder_prizes.in_cash") in cash - li - img(src=base + "firebase.png") - span - a(href="https://www.firebase.com/") Firebase - | - span(data-i18n="ladder_prizes.credits") credits - | - $300 - li - img(src=base + "one_month.png") - span - a(href="https://onemonthrails.com/") One Month Rails - | - span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML - | - $30 - li - img(src=base + "webstorm.png") - span - a(href="http://www.jetbrains.com/webstorm/") Webstorm - | - span(data-i18n="ladder_prizes.license") license - | - $49 - li - img(src=base + "oreilly.png") - span - a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly - | - span(data-i18n="ladder_prizes.oreilly") ebook of your choice - | - $40 - li - img(src=base + "aws.png") - span - a(href="http://aws.amazon.com/") Amazon Web Services - | - span(data-i18n="ladder_prizes.credits") credits - | - $50 - td $471 - tr - td 10th - td - ul.list-unstyled - li - img(src=base + "cash3.png") - span $1 - span(data-i18n="ladder_prizes.in_cash") in cash - li - img(src=base + "firebase.png") - span - a(href="https://www.firebase.com/") Firebase - | - span(data-i18n="ladder_prizes.credits") credits - | - $300 - li - img(src=base + "one_month.png") - span - a(href="https://onemonthrails.com/") One Month Rails - | - span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML - | - $30 - li - img(src=base + "webstorm.png") - span - a(href="http://www.jetbrains.com/webstorm/") Webstorm - | - span(data-i18n="ladder_prizes.license") license - | - $49 - li - img(src=base + "oreilly.png") - span - a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly - | - span(data-i18n="ladder_prizes.oreilly") ebook of your choice - | - $40 - li - img(src=base + "aws.png") - span - a(href="http://aws.amazon.com/") Amazon Web Services - | - span(data-i18n="ladder_prizes.credits") credits - | - $50 - td $470 - tr - td 11 - 40 - td - ul.list-unstyled - li - img(src=base + "webstorm.png") - span - a(href="http://www.jetbrains.com/webstorm/") Webstorm - | - span(data-i18n="ladder_prizes.license") license - | - $49 - li - img(src=base + "oreilly.png") - span - a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly - | - span(data-i18n="ladder_prizes.oreilly") ebook of your choice - | - $40 - li - img(src=base + "aws.png") - span - a(href="http://aws.amazon.com/") Amazon Web Services - | - span(data-i18n="ladder_prizes.credits") credits - | - $50 - td $139 - tr - td 41 - 100 - td - ul.list-unstyled - li - img(src=base + "oreilly.png") - span - a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly - | - span(data-i18n="ladder_prizes.oreilly") ebook of your choice - | - $40 - li - img(src=base + "aws.png") - span - a(href="http://aws.amazon.com/") Amazon Web Services - | - span(data-i18n="ladder_prizes.credits") credits - | - $50 - td $90 - tr - td 101+ - td - ul.list-unstyled - li - img(src=base + "aws.png") - span - a(href="http://aws.amazon.com/") Amazon Web Services - | - span(data-i18n="ladder_prizes.credits") credits - | - $50 - td $50 - - .tab-pane.well#rules - h1(data-i18n="ladder.tournament_rules") Tournament Rules - h2 General - p You don't have to buy anything to participate in the tournament, and trying to pay us won't increase your odds of winning. Although we don't anticipate the rules changing, they are subject to change. - - h2 Dates and Times - p The tournament starts on Tuesday, May 20 at 8:30AM and ends on Tuesday, June 10 at 5:00PM PDT. After the tournament finishes, we will check the games manually to prevent duplicate entries and cheating. We will email all the winners within two weeks of the end date. - - h2 Eligilibity - p The tournament is open to anyone over the age of 13. Players are allowed to form teams to compete, but we will only be rewarding submissions, so if a team of 10 wins, they will need to split the prize. - - p The tournament is NOT open to people who live in countries or states that prohibit participating or receiving a prize in a challenge (these include, but are not limited to Brazil, Quebec, Italy, Cuba, Sudan, Iran, North Korea, and Syria). To clarify, people from the aforementioned places are welcome to play the Greed level, but cannot receive prizes. Organizations involved in putting the tournament together (namely CodeCombat and all of our employees) are excluded from participating/winning prizes. - - h2 Submission Requirements - p - | To be eligible to win prizes, players must submit their code to the Greed ladder for ranking AND defeat our default AI. Every player that submits their code to the ladder and beats our default AI will receive $50 in AWS credits as described on the - a(href="#prizes", data-i18n="ladder_prizes.tournament_prizes") Tournament Prizes - | page. - - p - | There are some restrictions regarding who can use the AWS credits. Please see the additional rules of use on - a(href="https://aws.amazon.com/awscredits") Amazon's AWS credits page. - - h2 Submission Rights - p We reserve the right to use your submission and site identity (including username, avatar, and any information you mark as public) to promote the tournament. This is in keeping with our overall site terms of service. - - h2 Judging Criteria - p - | We will calculate final rankings by running the top games from the public leaderboard from both teams against each other and sorting solutions by wins and losses. The number of games from each side to be used in the final ranking is yet to be determined, but is probably around 150. The final ranking will be performed with a snapshot of solutions taken the end of the contest. The final ranking methedology is subject to change. We will not be evaluating code in any manual way for common traits like adequate documentation, cleanliness, etc. We reserve the right to disqualify any player for any reason. The public leaderboards are a good proxy for your final rank, but are not guaranteed to be accurate. To repeat, - strong the leaderboards are only a preliminary proxy for your final rank - | . - - p - | Your rank will change as players submit more solutions and more matches are played according to - a(href="https://github.com/codecombat/bayesian-battle") our open-source ranking library, Bayesian Battle - | , but our final ranking will use an exhaustive pairwise matching round amongst the top players as described above. - - h2 Prizes - p - | Prizes will be awarded to everyone that achieves a rank covered on the - a(href="#prizes", data-i18n="ladder.prizes") Tournament Prizes - | page. - - p Please remember that the player ranks listed on the prize page refer to ranks WITHIN a leaderboard. So if you are the #2 Ogre player, you will win the #2 prize. Similarly, if you are the #3 Human player, you will receive the #3 prize. If you have submissions on both leaderboards, we will only count your highest submission for the purposes of distributing prizes. As a result, your final ranking may be higher than your preliminary ranking due to removing duplicate submissions above you. - - h2 Verifying Potential Winners - p We may ask players to identify themselves so that we can detect duplicate entries. This may be done in the form of a Facebook, Google+, or LinkedIn profile, but we may need more information. All players eligible for prizes agree that refusing to provide us with identifying information may lead to ineligibility for prizes. - - p On a related note, if we have reason to believe that a player has intentionally submitted duplicate entries for the purpose of receiving more prizes or manipulating the leaderboards in any way, we will remove that player and all submissions we believe to be associated with them. We want this to be fair for everyone. - - h2 Prize Distribution - p Different sponsors require different ways of claiming their prizes, and we will work with winners to ensure they are able to redeem their prizes in a timely fashion. For cash prizes, we will deliver the money via PayPal. We will not ship checks, money orders, or cash through the mail. We will assume reasonable international money transfer costs to deliver cash prizes through Paypal. - - p Winners are responsible for any taxes associated with claiming their prizes. CodeCombat is not responsible for filing paperwork on behalf of winners for tax claims. - - h2 Contact - p - | If you have any questions or would like to get in touch with us for any other reason, we can be reached at team@codecombat.com. You can also post to our public - a(href="http://discourse.codecombat.com/") Discourse forum - | . + if level.get('name') == 'Greed' + .tab-pane.well#prizes + h1(data-i18n="ladder_prizes.title") Tournament Prizes + p + span(data-i18n="ladder_prizes.blurb_1") These prizes will be awarded according to + | + a(href="#rules", data-i18n="ladder_prizes.blurb_2") the tournament rules + | + span(data-i18n="ladder_prizes.blurb_3") to the top human and ogre players. + | + strong(data-i18n="ladder_prizes.blurb_4") Two teams means double the prizes! + | + span(data-i18n="ladder_prizes.blurb_5") (There will be two first place winners, two second-place winners, etc.) + + table#prize_table.table + thead + tr + td(data-i18n="ladder_prizes.rank") Rank + td(data-i18n="ladder_prizes.prizes") Prizes + td(data-i18n="ladder_prizes.total_value") Total Value + tbody + tr + td 1st + td + ul.list-unstyled + li + img(src=base + "cash1.png") + span $512 + span(data-i18n="ladder_prizes.in_cash") in cash + li + img(src=base + "custom_wizard.png") + span(data-i18n="ladder_prizes.custom_wizard") Custom CodeCombat Wizard + li + img(src=base + "custom_avatar.png") + span(data-i18n="ladder_prizes.custom_avatar") Custom CodeCombat avatar + li + img(src=base + "heap.png") + span + a(href="https://heapanalytics.com/") Heap Analytics + | + span(data-i18n="ladder_prizes.heap") for six months of "Startup" access + | - $354 + li + img(src=base + "firebase.png") + span + a(href="https://www.firebase.com/") Firebase + | + span(data-i18n="ladder_prizes.credits") credits + | - $300 + li + img(src=base + "one_month.png") + span + a(href="https://onemonthrails.com/") One Month Rails + | + span(data-i18n="ladder_prizes.one_month_coupon") coupon: choose either Rails or HTML + | - $99 + li + img(src=base + "webstorm.png") + span + a(href="http://www.jetbrains.com/webstorm/") Webstorm + | + span(data-i18n="ladder_prizes.license") license + | - $49 + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $2054 + tr + td 2nd + td + ul.list-unstyled + li + img(src=base + "cash2.png") + span $256 + span(data-i18n="ladder_prizes.in_cash") in cash + li + img(src=base + "custom_avatar.png") + span(data-i18n="ladder_prizes.custom_avatar") Custom CodeCombat avatar + li + img(src=base + "heap.png") + span + a(href="https://heapanalytics.com/") Heap Analytics + | + span(data-i18n="ladder_prizes.heap") for six months of "Startup" access + | - $354 + li + img(src=base + "firebase.png") + span + a(href="https://www.firebase.com/") Firebase + | + span(data-i18n="ladder_prizes.credits") credits + | - $300 + li + img(src=base + "one_month.png") + span + a(href="https://onemonthrails.com/") One Month Rails + | + span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML + | - $30 + li + img(src=base + "webstorm.png") + span + a(href="http://www.jetbrains.com/webstorm/") Webstorm + | + span(data-i18n="ladder_prizes.license") license + | - $49 + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $1229 + tr + td 3rd + td + ul.list-unstyled + li + img(src=base + "cash2.png") + span $128 + span(data-i18n="ladder_prizes.in_cash") in cash + li + img(src=base + "custom_avatar.png") + span(data-i18n="ladder_prizes.custom_avatar") Custom CodeCombat avatar + li + img(src=base + "heap.png") + span + a(href="https://heapanalytics.com/") Heap Analytics + | + span(data-i18n="ladder_prizes.heap") for six months of "Startup" access + | - $354 + li + img(src=base + "firebase.png") + span + a(href="https://www.firebase.com/") Firebase + | + span(data-i18n="ladder_prizes.credits") credits + | - $300 + li + img(src=base + "one_month.png") + span + a(href="https://onemonthrails.com/") One Month Rails + | + span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML + | - $30 + li + img(src=base + "webstorm.png") + span + a(href="http://www.jetbrains.com/webstorm/") Webstorm + | + span(data-i18n="ladder_prizes.license") license + | - $49 + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $1101 + tr + td 4th + td + ul.list-unstyled + li + img(src=base + "cash3.png") + span $64 + span(data-i18n="ladder_prizes.in_cash") in cash + li + img(src=base + "heap.png") + span + a(href="https://heapanalytics.com/") Heap Analytics + | + span(data-i18n="ladder_prizes.heap") for six months of "Startup" access + | - $354 + li + img(src=base + "firebase.png") + span + a(href="https://www.firebase.com/") Firebase + | + span(data-i18n="ladder_prizes.credits") credits + | - $300 + li + img(src=base + "one_month.png") + span + a(href="https://onemonthrails.com/") One Month Rails + | + span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML + | - $30 + li + img(src=base + "webstorm.png") + span + a(href="http://www.jetbrains.com/webstorm/") Webstorm + | + span(data-i18n="ladder_prizes.license") license + | - $49 + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $887 + tr + td 5th + td + ul.list-unstyled + li + img(src=base + "cash3.png") + span $32 + span(data-i18n="ladder_prizes.in_cash") in cash + li + img(src=base + "heap.png") + span + a(href="https://heapanalytics.com/") Heap Analytics + | + span(data-i18n="ladder_prizes.heap") for six months of "Startup" access + | - $354 + li + img(src=base + "firebase.png") + span + a(href="https://www.firebase.com/") Firebase + | + span(data-i18n="ladder_prizes.credits") credits + | - $300 + li + img(src=base + "one_month.png") + span + a(href="https://onemonthrails.com/") One Month Rails + | + span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML + | - $30 + li + img(src=base + "webstorm.png") + span + a(href="http://www.jetbrains.com/webstorm/") Webstorm + | + span(data-i18n="ladder_prizes.license") license + | - $49 + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $855 + tr + td 6th + td + ul.list-unstyled + li + img(src=base + "cash3.png") + span $16 + span(data-i18n="ladder_prizes.in_cash") in cash + li + img(src=base + "firebase.png") + span + a(href="https://www.firebase.com/") Firebase + | + span(data-i18n="ladder_prizes.credits") credits + | - $300 + li + img(src=base + "one_month.png") + span + a(href="https://onemonthrails.com/") One Month Rails + | + span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML + | - $30 + li + img(src=base + "webstorm.png") + span + a(href="http://www.jetbrains.com/webstorm/") Webstorm + | + span(data-i18n="ladder_prizes.license") license + | - $49 + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $485 + tr + td 7th + td + ul.list-unstyled + li + img(src=base + "cash3.png") + span $8 + span(data-i18n="ladder_prizes.in_cash") in cash + li + img(src=base + "firebase.png") + span + a(href="https://www.firebase.com/") Firebase + | + span(data-i18n="ladder_prizes.credits") credits + | - $300 + li + img(src=base + "one_month.png") + span + a(href="https://onemonthrails.com/") One Month Rails + | + span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML + | - $30 + li + img(src=base + "webstorm.png") + span + a(href="http://www.jetbrains.com/webstorm/") Webstorm + | + span(data-i18n="ladder_prizes.license") license + | - $49 + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $477 + tr + td 8th + td + ul.list-unstyled + li + img(src=base + "cash3.png") + span $4 + span(data-i18n="ladder_prizes.in_cash") in cash + li + img(src=base + "firebase.png") + span + a(href="https://www.firebase.com/") Firebase + | + span(data-i18n="ladder_prizes.credits") credits + | - $300 + li + img(src=base + "one_month.png") + span + a(href="https://onemonthrails.com/") One Month Rails + | + span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML + | - $30 + li + img(src=base + "webstorm.png") + span + a(href="http://www.jetbrains.com/webstorm/") Webstorm + | + span(data-i18n="ladder_prizes.license") license + | - $49 + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $473 + tr + td 9th + td + ul.list-unstyled + li + img(src=base + "cash3.png") + span $2 + span(data-i18n="ladder_prizes.in_cash") in cash + li + img(src=base + "firebase.png") + span + a(href="https://www.firebase.com/") Firebase + | + span(data-i18n="ladder_prizes.credits") credits + | - $300 + li + img(src=base + "one_month.png") + span + a(href="https://onemonthrails.com/") One Month Rails + | + span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML + | - $30 + li + img(src=base + "webstorm.png") + span + a(href="http://www.jetbrains.com/webstorm/") Webstorm + | + span(data-i18n="ladder_prizes.license") license + | - $49 + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $471 + tr + td 10th + td + ul.list-unstyled + li + img(src=base + "cash3.png") + span $1 + span(data-i18n="ladder_prizes.in_cash") in cash + li + img(src=base + "firebase.png") + span + a(href="https://www.firebase.com/") Firebase + | + span(data-i18n="ladder_prizes.credits") credits + | - $300 + li + img(src=base + "one_month.png") + span + a(href="https://onemonthrails.com/") One Month Rails + | + span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML + | - $30 + li + img(src=base + "webstorm.png") + span + a(href="http://www.jetbrains.com/webstorm/") Webstorm + | + span(data-i18n="ladder_prizes.license") license + | - $49 + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $470 + tr + td 11 - 40 + td + ul.list-unstyled + li + img(src=base + "webstorm.png") + span + a(href="http://www.jetbrains.com/webstorm/") Webstorm + | + span(data-i18n="ladder_prizes.license") license + | - $49 + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $139 + tr + td 41 - 100 + td + ul.list-unstyled + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $90 + tr + td 101+ + td + ul.list-unstyled + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $50 + + .tab-pane.well#rules + h1(data-i18n="ladder.tournament_rules") Tournament Rules + h2 General + p You don't have to buy anything to participate in the tournament, and trying to pay us won't increase your odds of winning. Although we don't anticipate the rules changing, they are subject to change. + + h2 Dates and Times + p The tournament starts on Tuesday, May 20 at 8:30AM and ends on Tuesday, June 10 at 5:00PM PDT. After the tournament finishes, we will check the games manually to prevent duplicate entries and cheating. We will email all the winners within two weeks of the end date. + + h2 Eligilibity + p The tournament is open to anyone over the age of 13. Players are allowed to form teams to compete, but we will only be rewarding submissions, so if a team of 10 wins, they will need to split the prize. + + p The tournament is NOT open to people who live in countries or states that prohibit participating or receiving a prize in a challenge (these include, but are not limited to Brazil, Quebec, Italy, Cuba, Sudan, Iran, North Korea, and Syria). To clarify, people from the aforementioned places are welcome to play the Greed level, but cannot receive prizes. Organizations involved in putting the tournament together (namely CodeCombat and all of our employees) are excluded from participating/winning prizes. + + h2 Submission Requirements + p + | To be eligible to win prizes, players must submit their code to the Greed ladder for ranking AND defeat our default AI. Every player that submits their code to the ladder and beats our default AI will receive $50 in AWS credits as described on the + a(href="#prizes", data-i18n="ladder_prizes.tournament_prizes") Tournament Prizes + | page. + + p + | There are some restrictions regarding who can use the AWS credits. Please see the additional rules of use on + a(href="https://aws.amazon.com/awscredits") Amazon's AWS credits page. + + h2 Submission Rights + p We reserve the right to use your submission and site identity (including username, avatar, and any information you mark as public) to promote the tournament. This is in keeping with our overall site terms of service. + + h2 Judging Criteria + p + | We will calculate final rankings by running the top games from the public leaderboard from both teams against each other and sorting solutions by wins and losses. The number of games from each side to be used in the final ranking is yet to be determined, but is probably around 150. The final ranking will be performed with a snapshot of solutions taken the end of the contest. The final ranking methedology is subject to change. We will not be evaluating code in any manual way for common traits like adequate documentation, cleanliness, etc. We reserve the right to disqualify any player for any reason. The public leaderboards are a good proxy for your final rank, but are not guaranteed to be accurate. To repeat, + strong the leaderboards are only a preliminary proxy for your final rank + | . + + p + | Your rank will change as players submit more solutions and more matches are played according to + a(href="https://github.com/codecombat/bayesian-battle") our open-source ranking library, Bayesian Battle + | , but our final ranking will use an exhaustive pairwise matching round amongst the top players as described above. + + h2 Prizes + p + | Prizes will be awarded to everyone that achieves a rank covered on the + a(href="#prizes", data-i18n="ladder.prizes") Tournament Prizes + | page. + + p Please remember that the player ranks listed on the prize page refer to ranks WITHIN a leaderboard. So if you are the #2 Ogre player, you will win the #2 prize. Similarly, if you are the #3 Human player, you will receive the #3 prize. If you have submissions on both leaderboards, we will only count your highest submission for the purposes of distributing prizes. As a result, your final ranking may be higher than your preliminary ranking due to removing duplicate submissions above you. + + h2 Verifying Potential Winners + p We may ask players to identify themselves so that we can detect duplicate entries. This may be done in the form of a Facebook, Google+, or LinkedIn profile, but we may need more information. All players eligible for prizes agree that refusing to provide us with identifying information may lead to ineligibility for prizes. + + p On a related note, if we have reason to believe that a player has intentionally submitted duplicate entries for the purpose of receiving more prizes or manipulating the leaderboards in any way, we will remove that player and all submissions we believe to be associated with them. We want this to be fair for everyone. + + h2 Prize Distribution + p Different sponsors require different ways of claiming their prizes, and we will work with winners to ensure they are able to redeem their prizes in a timely fashion. For cash prizes, we will deliver the money via PayPal. We will not ship checks, money orders, or cash through the mail. We will assume reasonable international money transfer costs to deliver cash prizes through Paypal. + + p Winners are responsible for any taxes associated with claiming their prizes. CodeCombat is not responsible for filing paperwork on behalf of winners for tax claims. + + h2 Contact + p + | If you have any questions or would like to get in touch with us for any other reason, we can be reached at team@codecombat.com. You can also post to our public + a(href="http://discourse.codecombat.com/") Discourse forum + | . + + .tab-pane.well#winners + h1(data-i18n="ladder.winners") Winners + + table.table.table-hover.table-condensed + thead + tr + th(data-i18n="ladder_prizes.rank") Rank + th Human + th Human wins/losses/ties + th Ogre + th Ogre wins/losses/ties + th Spectate + tbody + each human, index in winners.humans + - var ogre = winners.ogres[index] + tr + td= human.rank + td= human.name + td + span.win= human.wins + | - + span.loss= human.losses + | - + span.tie= 377 - human.wins - human.losses + if ogre + td= ogre.name + td + span.win= ogre.wins + | - + span.loss= ogre.losses + | - + span.tie= 407 - ogre.wins - ogre.losses + td + a(href="/play/spectate/" + level.get('slug') + "?session-one=" + human.sessionID + "&session-two=" + ogre.sessionID) Watch the battle + else + td + td + td \ No newline at end of file diff --git a/app/templates/play/spectate.jade b/app/templates/play/spectate.jade index e6a9650f9..ee9408d4c 100644 --- a/app/templates/play/spectate.jade +++ b/app/templates/play/spectate.jade @@ -3,7 +3,7 @@ .level-content #control-bar-view #canvas-wrapper - canvas(width=924, height=589)#surface + canvas(width=1848, height=1178)#surface #canvas-left-gradient.gradient #canvas-top-gradient.gradient #gold-view.secret.expanded diff --git a/app/views/account/profile_view.coffee b/app/views/account/profile_view.coffee index cd66cbe17..ef49b583c 100644 --- a/app/views/account/profile_view.coffee +++ b/app/views/account/profile_view.coffee @@ -1,11 +1,19 @@ View = require 'views/kinds/RootView' template = require 'templates/account/profile' User = require 'models/User' +LevelSession = require 'models/LevelSession' +CocoCollection = require 'collections/CocoCollection' {me} = require 'lib/auth' JobProfileContactView = require 'views/modal/job_profile_contact_modal' JobProfileView = require 'views/account/job_profile_view' forms = require 'lib/forms' +class LevelSessionsCollection extends CocoCollection + url: -> "/db/user/#{@userID}/level.sessions/employer" + model: LevelSession + constructor: (@userID) -> + super() + module.exports = class ProfileView extends View id: "profile-view" template: template @@ -42,9 +50,7 @@ module.exports = class ProfileView extends View if User.isObjectID @userID @finishInit() else - console.log "getting", @userID $.ajax "/db/user/#{@userID}/nameToID", success: (@userID) => - console.log " got", @userID @finishInit() unless @destroyed @render() @@ -59,8 +65,11 @@ module.exports = class ProfileView extends View @user.fetch() @listenTo @user, "sync", => @render() + $.post "/db/user/#{me.id}/track/view_candidate" + $.post "/db/user/#{@userID}/track/viewed_by_employer" unless me.isAdmin() else @user = User.getByID(@userID) + @sessions = @supermodel.loadCollection(new LevelSessionsCollection(@userID), 'candidate_sessions').model onLinkedInLoaded: => @linkedinLoaded = true @@ -78,11 +87,11 @@ module.exports = class ProfileView extends View @renderLinkedInButton() else @waitingForLinkedIn = true + importLinkedIn: => overwriteConfirm = confirm("Importing LinkedIn data will overwrite your current work experience, skills, name, descriptions, and education. Continue?") unless overwriteConfirm then return application.linkedinHandler.getProfileData (err, profileData) => - console.log profileData @processLinkedInProfileData profileData jobProfileSchema: -> @user.schema().properties.jobProfile.properties @@ -113,7 +122,7 @@ module.exports = class ProfileView extends View for position in p["positions"]["values"] workObj = {} descriptionMaxLength = workSchema.description.maxLength - + workObj.description = position.summary?.slice(0,descriptionMaxLength) workObj.description ?= "" if position.startDate?.year? @@ -215,6 +224,11 @@ module.exports = class ProfileView extends View links = ($.extend(true, {}, link) for link in links) link.icon = @iconForLink link for link in links context.profileLinks = _.sortBy links, (link) -> not link.icon # icons first + if @sessions + context.sessions = (s.attributes for s in @sessions.models when (s.get('submitted') or s.get('level-id') is 'gridmancer')) + context.sessions.sort (a, b) -> (b.playtime ? 0) - (a.playtime ? 0) + else + context.sessions = [] context afterRender: -> @@ -301,7 +315,7 @@ module.exports = class ProfileView extends View errors = @user.validate() return @showErrors errors if errors jobProfile = @user.get('jobProfile') - jobProfile.updated = (new Date()).toISOString() + jobProfile.updated = (new Date()).toISOString() if @user is me @user.set 'jobProfile', jobProfile return unless res = @user.save() res.error => diff --git a/app/views/account/settings_view.coffee b/app/views/account/settings_view.coffee index 73f952ad6..377c590eb 100644 --- a/app/views/account/settings_view.coffee +++ b/app/views/account/settings_view.coffee @@ -113,7 +113,7 @@ module.exports = class SettingsView extends View return unless me.hasLocalChanges() - res = me.save() + res = me.patch() return unless res save = $('#save-button', @$el).text($.i18n.t('common.saving', defaultValue: 'Saving...')) .removeClass('btn-danger').addClass('btn-success').show() diff --git a/app/views/admin/employer_list_view.coffee b/app/views/admin/employer_list_view.coffee new file mode 100644 index 000000000..fc7d2f2af --- /dev/null +++ b/app/views/admin/employer_list_view.coffee @@ -0,0 +1,160 @@ +View = require 'views/kinds/RootView' +template = require 'templates/admin/employer_list' +app = require 'application' +User = require 'models/User' +{me} = require 'lib/auth' +CocoCollection = require 'collections/CocoCollection' +ModelModal = require 'views/modal/model_modal' + +class EmployersCollection extends CocoCollection + url: '/db/user/x/employers' + model: User + +module.exports = class EmployersView extends View + id: "employers-view" + template: template + + events: + 'click tbody tr td:first-child': 'onEmployerClicked' + + constructor: (options) -> + super options + @getEmployers() + + afterRender: -> + super() + @sortTable() if @employers.models.length + + getRenderData: -> + ctx = super() + ctx.employers = @employers.models + ctx.moment = moment + ctx + + getEmployers: -> + @employers = new EmployersCollection() + @employers.fetch() + # Re-render when we have fetched them, but don't wait and show a progress bar while loading. + @listenToOnce @employers, 'all', => @render() + + sortTable: -> + # http://mottie.github.io/tablesorter/docs/example-widget-bootstrap-theme.html + $.extend $.tablesorter.themes.bootstrap, + # these classes are added to the table. To see other table classes available, + # look here: http://twitter.github.com/bootstrap/base-css.html#tables + table: "table table-bordered" + caption: "caption" + header: "bootstrap-header" # give the header a gradient background + footerRow: "" + footerCells: "" + icons: "" # add "icon-white" to make them white; this icon class is added to the in the header + sortNone: "bootstrap-icon-unsorted" + sortAsc: "icon-chevron-up" # glyphicon glyphicon-chevron-up" # we are still using v2 icons + sortDesc: "icon-chevron-down" # glyphicon-chevron-down" # we are still using v2 icons + active: "" # applied when column is sorted + hover: "" # use custom css here - bootstrap class may not override it + filterRow: "" # filter row class + even: "" # odd row zebra striping + odd: "" # even row zebra striping + + + # e = exact text from cell + # n = normalized value returned by the column parser + # f = search filter input value + # i = column index + # $r = ??? + filterSelectExactMatch = (e, n, f, i, $r) -> e is f + + # call the tablesorter plugin and apply the uitheme widget + @$el.find(".tablesorter").tablesorter + theme: "bootstrap" + widthFixed: true + headerTemplate: "{content} {icon}" + textSorter: + 6: (a, b, direction, column, table) -> + days = [] + for s in [a, b] + n = parseInt s + n = 0 unless _.isNumber n + n = 1 if /^a/.test s + for [duration, factor] in [ + [/second/i, 1 / (86400 * 1000)] + [/minute/i, 1 / 1440] + [/hour/i, 1 / 24] + [/week/i, 7] + [/month/i, 30.42] + [/year/i, 365.2425] + ] + if duration.test s + n *= factor + break + if /^in /i.test s + n *= -1 + days.push n + days[0] - days[1] + sortList: [[6, 0]] + # widget code contained in the jquery.tablesorter.widgets.js file + # use the zebra stripe widget if you plan on hiding any rows (filter widget) + widgets: ["uitheme", "zebra", "filter"] + widgetOptions: + # using the default zebra striping class name, so it actually isn't included in the theme variable above + # this is ONLY needed for bootstrap theming if you are using the filter widget, because rows are hidden + zebra: ["even", "odd"] + + # extra css class applied to the table row containing the filters & the inputs within that row + filter_cssFilter: "" + + # If there are child rows in the table (rows with class name from "cssChildRow" option) + # and this option is true and a match is found anywhere in the child row, then it will make that row + # visible; default is false + filter_childRows: false + + # if true, filters are collapsed initially, but can be revealed by hovering over the grey bar immediately + # below the header row. Additionally, tabbing through the document will open the filter row when an input gets focus + filter_hideFilters: false + + # Set this option to false to make the searches case sensitive + filter_ignoreCase: true + + # jQuery selector string of an element used to reset the filters + filter_reset: ".reset" + + # Use the $.tablesorter.storage utility to save the most recent filters + filter_saveFilters: true + + # Delay in milliseconds before the filter widget starts searching; This option prevents searching for + # every character while typing and should make searching large tables faster. + filter_searchDelay: 150 + + # Set this option to true to use the filter to find text from the start of the column + # So typing in "a" will find "albert" but not "frank", both have a's; default is false + filter_startsWith: false + + filter_functions: + 3: + "0-1": (e, n, f, i, $r) -> parseInt(e) <= 1 + "2-5": (e, n, f, i, $r) -> 2 <= parseInt(e) <= 5 + "6+": (e, n, f, i, $r) -> 6 <= parseInt(e) + 4: + "0-1": (e, n, f, i, $r) -> parseInt(e) <= 1 + "2-5": (e, n, f, i, $r) -> 2 <= parseInt(e) <= 5 + "6+": (e, n, f, i, $r) -> 6 <= parseInt(e) + 5: + "0-1": (e, n, f, i, $r) -> parseInt(e) <= 1 + "2-5": (e, n, f, i, $r) -> 2 <= parseInt(e) <= 5 + "6+": (e, n, f, i, $r) -> 6 <= parseInt(e) + 6: + "Last day": (e, n, f, i, $r) -> + days = parseFloat $($r.find('td')[i]).data('employer-age') + days <= 1 + "Last week": (e, n, f, i, $r) -> + days = parseFloat $($r.find('td')[i]).data('employer-age') + days <= 7 + "Last 4 weeks": (e, n, f, i, $r) -> + days = parseFloat $($r.find('td')[i]).data('employer-age') + days <= 28 + + onEmployerClicked: (e) -> + return unless id = $(e.target).closest('tr').data('employer-id') + employer = new User _id: id + @openModalView new ModelModal models: [employer] diff --git a/app/views/admin_view.coffee b/app/views/admin_view.coffee index 8c93ff616..eab1bbc42 100644 --- a/app/views/admin_view.coffee +++ b/app/views/admin_view.coffee @@ -8,6 +8,7 @@ module.exports = class AdminView extends View events: 'click #enter-espionage-mode': 'enterEspionageMode' + 'click #increment-button': 'incrementUserAttribute' enterEspionageMode: -> userEmail = $("#user-email").val().toLowerCase() @@ -29,3 +30,8 @@ module.exports = class AdminView extends View espionageFailure: (jqxhr, status,error)-> console.log "There was an error entering espionage mode: #{error}" + + incrementUserAttribute: (e) -> + val = $('#increment-field').val() + me.set(val, me.get(val) + 1) + me.save() diff --git a/app/views/contribute/contribute_class_view.coffee b/app/views/contribute/contribute_class_view.coffee index d9110c3fd..ae6fd59af 100644 --- a/app/views/contribute/contribute_class_view.coffee +++ b/app/views/contribute/contribute_class_view.coffee @@ -36,7 +36,7 @@ module.exports = class ContributeClassView extends View subscription = el.attr('name') me.setEmailSubscription subscription+'News', checked - me.save() + me.patch() @openModalView new SignupModalView() if me.get 'anonymous' el.parent().find('.saved-notification').finish().show('fast').delay(3000).fadeOut(2000) diff --git a/app/views/editor/achievement/edit.coffee b/app/views/editor/achievement/edit.coffee index 186eb206d..76988a586 100644 --- a/app/views/editor/achievement/edit.coffee +++ b/app/views/editor/achievement/edit.coffee @@ -1,6 +1,7 @@ View = require 'views/kinds/RootView' template = require 'templates/editor/achievement/edit' Achievement = require 'models/Achievement' +ConfirmModal = require 'views/modal/confirm' module.exports = class AchievementEditView extends View id: "editor-achievement-edit-view" @@ -9,6 +10,7 @@ module.exports = class AchievementEditView extends View events: 'click #save-button': 'saveAchievement' + 'click #recalculate-button': 'confirmRecalculation' subscriptions: 'save-new': 'saveAchievement' @@ -72,3 +74,34 @@ module.exports = class AchievementEditView extends View res.success => url = "/editor/achievement/#{@achievement.get('slug') or @achievement.id}" document.location.href = url + + confirmRecalculation: (e) -> + renderData = + 'confirmTitle': "Are you really sure?" + 'confirmBody': "This will trigger recalculation of the achievement for all users. Are you really sure you want to go down this path?" + 'confirmDecline': "Not really" + 'confirmConfirm': "Definitely" + + confirmModal = new ConfirmModal(renderData) + confirmModal.onConfirm @recalculateAchievement + @openModalView confirmModal + + recalculateAchievement: => + $.ajax + data: JSON.stringify(achievements: [@achievement.get('slug') or @achievement.get('_id')]) + success: (data, status, jqXHR) -> + noty + timeout: 5000 + text: 'Recalculation process started' + type: 'success' + layout: 'topCenter' + error: (jqXHR, status, error) -> + console.error jqXHR + noty + timeout: 5000 + text: "Starting recalculation process failed with error code #{jqXHR.status}" + type: 'error' + layout: 'topCenter' + url: '/admin/earned.achievement/recalculate' + type: 'POST' + contentType: 'application/json' diff --git a/app/views/editor/level/add_thangs_view.coffee b/app/views/editor/level/add_thangs_view.coffee index cf1824201..2a635b2c9 100644 --- a/app/views/editor/level/add_thangs_view.coffee +++ b/app/views/editor/level/add_thangs_view.coffee @@ -4,7 +4,7 @@ ThangType = require 'models/ThangType' CocoCollection = require 'collections/CocoCollection' class ThangTypeSearchCollection extends CocoCollection - url: '/db/thang.type/search?project=true' + url: '/db/thang.type?project=true' model: ThangType addTerm: (term) -> @@ -73,4 +73,4 @@ module.exports = class AddThangsView extends View onEscapePressed: -> @$el.find('input#thang-search').val("") - @runSearch \ No newline at end of file + @runSearch diff --git a/app/views/editor/level/component/edit.coffee b/app/views/editor/level/component/edit.coffee index 6d416e011..3bc526813 100644 --- a/app/views/editor/level/component/edit.coffee +++ b/app/views/editor/level/component/edit.coffee @@ -8,7 +8,7 @@ SaveVersionModal = require 'views/modal/save_version_modal' module.exports = class LevelComponentEditView extends View id: "editor-level-component-edit-view" template: template - editableSettings: ['name', 'description', 'system', 'language', 'dependencies', 'propertyDocumentation', 'i18n'] + editableSettings: ['name', 'description', 'system', 'codeLanguage', 'dependencies', 'propertyDocumentation', 'i18n'] events: 'click #done-editing-component-button': 'endEditing' diff --git a/app/views/editor/level/system/add.coffee b/app/views/editor/level/system/add.coffee index 64caa52b4..fb42a866b 100644 --- a/app/views/editor/level/system/add.coffee +++ b/app/views/editor/level/system/add.coffee @@ -5,7 +5,7 @@ LevelSystem = require 'models/LevelSystem' CocoCollection = require 'collections/CocoCollection' class LevelSystemSearchCollection extends CocoCollection - url: '/db/level_system/search' + url: '/db/level_system' model: LevelSystem module.exports = class LevelSystemAddView extends View diff --git a/app/views/editor/level/system/edit.coffee b/app/views/editor/level/system/edit.coffee index 404ac360b..6560bafa7 100644 --- a/app/views/editor/level/system/edit.coffee +++ b/app/views/editor/level/system/edit.coffee @@ -8,7 +8,7 @@ SaveVersionModal = require 'views/modal/save_version_modal' module.exports = class LevelSystemEditView extends View id: "editor-level-system-edit-view" template: template - editableSettings: ['name', 'description', 'language', 'dependencies', 'propertyDocumentation', 'i18n'] + editableSettings: ['name', 'description', 'codeLanguage', 'dependencies', 'propertyDocumentation', 'i18n'] events: 'click #done-editing-system-button': 'endEditing' diff --git a/app/views/editor/level/thangs_tab_view.coffee b/app/views/editor/level/thangs_tab_view.coffee index 22a504480..fb5443460 100644 --- a/app/views/editor/level/thangs_tab_view.coffee +++ b/app/views/editor/level/thangs_tab_view.coffee @@ -21,7 +21,7 @@ componentOriginals = "physics.Physical" : "524b75ad7fc0f6d519000001" class ThangTypeSearchCollection extends CocoCollection - url: '/db/thang.type/search?project=original,name,version,slug,kind,components' + url: '/db/thang.type?project=original,name,version,slug,kind,components' model: ThangType module.exports = class ThangsTabView extends View diff --git a/app/views/employers_view.coffee b/app/views/employers_view.coffee index f7cddaa4e..8a2c4e4ff 100644 --- a/app/views/employers_view.coffee +++ b/app/views/employers_view.coffee @@ -20,10 +20,7 @@ module.exports = class EmployersView extends View constructor: (options) -> super options @getCandidates() - checkForEmployerSignupHash: => - if window.location.hash is "#employerSignupLoggingIn" and not ("employer" in me.get("permissions")) - @openModalView application.router.getView("modal/employer_signup","_modal") - window.location.hash = "" + afterRender: -> super() @sortTable() if @candidates.models.length @@ -33,13 +30,20 @@ module.exports = class EmployersView extends View _.delay @checkForEmployerSignupHash, 500 getRenderData: -> - c = super() - c.candidates = @candidates.models - userPermissions = me.get('permissions') ? [] + ctx = super() + ctx.isEmployer = @isEmployer() + ctx.candidates = _.sortBy @candidates.models, (c) -> c.get('jobProfile').updated + ctx.activeCandidates = _.filter ctx.candidates, (c) -> c.get('jobProfile').active + ctx.inactiveCandidates = _.reject ctx.candidates, (c) -> c.get('jobProfile').active + ctx.featuredCandidates = _.filter ctx.activeCandidates, (c) -> c.get('jobProfileApproved') + ctx.otherCandidates = _.reject ctx.activeCandidates, (c) -> c.get('jobProfileApproved') + ctx.moment = moment + ctx._ = _ + ctx - c.isEmployer = _.contains userPermissions, "employer" - c.moment = moment - c + isEmployer: -> + userPermissions = me.get('permissions') ? [] + _.contains userPermissions, "employer" getCandidates: -> @candidates = new CandidatesCollection() @@ -48,6 +52,7 @@ module.exports = class EmployersView extends View @listenToOnce @candidates, 'all', @renderCandidatesAndSetupScrolling renderCandidatesAndSetupScrolling: => + @render() $(".nano").nanoScroller() if window.history?.state?.lastViewedCandidateID @@ -55,6 +60,11 @@ module.exports = class EmployersView extends View else if window.location.hash.length is 25 $(".nano").nanoScroller({scrollTo:$(window.location.hash)}) + checkForEmployerSignupHash: => + if window.location.hash is "#employerSignupLoggingIn" and not ("employer" in me.get("permissions")) + @openModalView application.router.getView("modal/employer_signup","_modal") + window.location.hash = "" + sortTable: -> # http://mottie.github.io/tablesorter/docs/example-widget-bootstrap-theme.html $.extend $.tablesorter.themes.bootstrap, @@ -110,7 +120,7 @@ module.exports = class EmployersView extends View n *= -1 days.push n days[0] - days[1] - sortList: [[6, 0]] + sortList: if @isEmployer() or me.isAdmin() then [[6, 0]] else [[0, 1]] # widget code contained in the jquery.tablesorter.widgets.js file # use the zebra stripe widget if you plan on hiding any rows (filter widget) widgets: ["uitheme", "zebra", "filter"] @@ -172,9 +182,6 @@ module.exports = class EmployersView extends View 7: "✓": filterSelectExactMatch "✗": filterSelectExactMatch - 8: - "✓": filterSelectExactMatch - "✗": filterSelectExactMatch onCandidateClicked: (e) -> id = $(e.target).closest('tr').data('candidate-id') diff --git a/app/views/kinds/RootView.coffee b/app/views/kinds/RootView.coffee index fd036784b..2ef78f7f7 100644 --- a/app/views/kinds/RootView.coffee +++ b/app/views/kinds/RootView.coffee @@ -8,6 +8,7 @@ locale = require 'locale/locale' Achievement = require '../../models/Achievement' User = require '../../models/User' +# TODO remove filterKeyboardEvents = (allowedEvents, func) -> return (splat...) -> @@ -25,26 +26,24 @@ module.exports = class RootView extends CocoView subscriptions: 'achievements:new': 'handleNewAchievements' - initialize: -> - $ => - # TODO Ruben remove this. Allows for easy testing right now though - #test = new Achievement(_id:'537ce4855c91b8d1dda7fda8') - #test.fetch(success:@showNewAchievement) - showNewAchievement: (achievement) -> + showNewAchievement: (achievement, earnedAchievement) -> currentLevel = me.level() nextLevel = currentLevel + 1 currentLevelExp = User.expForLevel(currentLevel) nextLevelExp = User.expForLevel(nextLevel) totalExpNeeded = nextLevelExp - currentLevelExp + expFunction = achievement.getExpFunction() currentExp = me.get('points') - worth = achievement.get('worth') - leveledUp = currentExp - worth < currentLevelExp - alreadyAchievedPercentage = 100 * (currentExp - currentLevelExp - worth) / totalExpNeeded - newlyAchievedPercentage = if currentLevelExp is currentExp then 0 else 100 * worth / totalExpNeeded + previousExp = currentExp - achievement.get('worth') + previousExp = expFunction(earnedAchievement.get('previouslyAchievedAmount')) * achievement.get('worth') if achievement.isRepeatable() + achievedExp = currentExp - previousExp + leveledUp = currentExp - achievedExp < currentLevelExp + alreadyAchievedPercentage = 100 * (previousExp - currentLevelExp) / totalExpNeeded + newlyAchievedPercentage = if leveledUp then 100 * (currentExp - currentLevelExp) / totalExpNeeded else 100 * achievedExp / totalExpNeeded console.debug "Current level is #{currentLevel} (#{currentLevelExp} xp), next level is #{nextLevel} (#{nextLevelExp} xp)." - console.debug "Need a total of #{nextLevelExp - currentLevelExp}, already had #{currentExp - currentLevelExp - worth} and just now earned #{worth} totalling on #{currentExp}" + console.debug "Need a total of #{nextLevelExp - currentLevelExp}, already had #{previousExp} and just now earned #{achievedExp} totalling on #{currentExp}" alreadyAchievedBar = $("
") newlyAchievedBar = $("
") @@ -53,7 +52,7 @@ module.exports = class RootView extends CocoView message = if (currentLevel isnt 1) and leveledUp then "Reached level #{currentLevel}!" else null alreadyAchievedBar.tooltip(title: "#{currentExp} XP in total") - newlyAchievedBar.tooltip(title: "#{worth} XP earned") + newlyAchievedBar.tooltip(title: "#{achievedExp} XP earned") emptyBar.tooltip(title: "#{nextLevelExp - currentExp} XP until level #{nextLevel}") # TODO a default should be linked here @@ -63,7 +62,7 @@ module.exports = class RootView extends CocoView image: $("") description: achievement.get('description') progressBar: progressBar - earnedExp: "+ #{worth} XP" + earnedExp: "+ #{achievedExp} XP" message: message options = @@ -77,13 +76,11 @@ module.exports = class RootView extends CocoView $.notify( data, options ) handleNewAchievements: (earnedAchievements) -> - console.debug 'Got new earned achievements' - # TODO performance? _.each(earnedAchievements.models, (earnedAchievement) => achievement = new Achievement(_id: earnedAchievement.get('achievement')) console.log achievement achievement.fetch( - success: @showNewAchievement + success: (achievement) => @showNewAchievement(achievement, earnedAchievement) ) ) @@ -150,7 +147,7 @@ module.exports = class RootView extends CocoView saveLanguage: (newLang) -> me.set('preferredLanguage', newLang) - res = me.save() + res = me.patch() return unless res res.error -> errors = JSON.parse(res.responseText) diff --git a/app/views/kinds/SearchView.coffee b/app/views/kinds/SearchView.coffee index c8f0bc077..8f7a17420 100644 --- a/app/views/kinds/SearchView.coffee +++ b/app/views/kinds/SearchView.coffee @@ -5,7 +5,7 @@ app = require('application') class SearchCollection extends Backbone.Collection initialize: (modelURL, @model, @term, @projection) -> - @url = "#{modelURL}/search?project=" + @url = "#{modelURL}?project=" if @projection? and not (@projection == []) @url += projection[0] @url += ',' + projected for projected in projection[1..] diff --git a/app/views/modal/confirm.coffee b/app/views/modal/confirm.coffee new file mode 100644 index 000000000..18ded9ed6 --- /dev/null +++ b/app/views/modal/confirm.coffee @@ -0,0 +1,30 @@ +ModalView = require '../kinds/ModalView' +template = require 'templates/modal/confirm' + +module.exports = class ConfirmModal extends ModalView + id: "confirm-modal" + template: template + closeButton: true + closeOnConfirm: true + + events: + 'click #decline-button': 'doDecline' + 'click #confirm-button': 'doConfirm' + + constructor: (@renderData={}, options={}) -> + super(options) + + getRenderData: -> + context = super() + context.closeOnConfirm = @closeOnConfirm + _.extend context, @renderData + + setRenderData: (@renderData) -> + + onDecline: (@decline) -> + + onConfirm: (@confirm) -> + + doConfirm: -> @confirm() if @confirm + + doDecline: -> @decline() if @decline diff --git a/app/views/modal/contact_modal.coffee b/app/views/modal/contact_modal.coffee index 2e313d44a..3c4af7a83 100644 --- a/app/views/modal/contact_modal.coffee +++ b/app/views/modal/contact_modal.coffee @@ -33,3 +33,4 @@ module.exports = class ContactView extends View return forms.applyErrorsToForm @$el, res.errors unless res.valid window.tracker?.trackEvent 'Sent Feedback', message: contactMessage sendContactMessage contactMessage, @$el + $.post "/db/user/#{me.id}/track/contact_codecombat" diff --git a/app/views/modal/diplomat_suggestion_modal.coffee b/app/views/modal/diplomat_suggestion_modal.coffee index 511fe369a..bd735f995 100644 --- a/app/views/modal/diplomat_suggestion_modal.coffee +++ b/app/views/modal/diplomat_suggestion_modal.coffee @@ -12,7 +12,7 @@ module.exports = class DiplomatSuggestionView extends View subscribeAsDiplomat: -> me.setEmailSubscription 'diplomatNews', true - me.save() + me.patch() $("#email_translator").prop("checked", 1) @hide() return diff --git a/app/views/modal/job_profile_contact_modal.coffee b/app/views/modal/job_profile_contact_modal.coffee index 236301454..ff968704f 100644 --- a/app/views/modal/job_profile_contact_modal.coffee +++ b/app/views/modal/job_profile_contact_modal.coffee @@ -39,3 +39,5 @@ module.exports = class JobProfileContactView extends ContactView contactMessage.message += '\n\n\n\n[CodeCombat says: please let us know if you end up accepting this job. Thanks!]' window.tracker?.trackEvent 'Sent Job Profile Message', message: contactMessage sendContactMessage contactMessage, @$el + $.post "/db/user/#{me.id}/track/contact_candidate" + $.post "/db/user/#{@options.recipientID}/track/contacted_by_employer" unless me.isAdmin() diff --git a/app/views/modal/wizard_settings_modal.coffee b/app/views/modal/wizard_settings_modal.coffee index 5715a4c1f..c099a4fba 100644 --- a/app/views/modal/wizard_settings_modal.coffee +++ b/app/views/modal/wizard_settings_modal.coffee @@ -40,7 +40,7 @@ module.exports = class WizardSettingsModal extends View forms.applyErrorsToForm(@$el, res) return - res = me.save() + res = me.patch() return unless res save = $('#save-button', @$el).text($.i18n.t('common.saving', defaultValue: 'Saving...')) .addClass('btn-info').show().removeClass('btn-danger') diff --git a/app/views/play/ladder/ladder_tab.coffee b/app/views/play/ladder/ladder_tab.coffee index 974d03ecd..416cdaf2d 100644 --- a/app/views/play/ladder/ladder_tab.coffee +++ b/app/views/play/ladder/ladder_tab.coffee @@ -10,14 +10,6 @@ ModelModal = require 'views/modal/model_modal' HIGHEST_SCORE = 1000000 -class LevelSessionsCollection extends CocoCollection - url: '' - model: LevelSession - - constructor: (levelID) -> - super() - @url = "/db/level/#{levelID}/all_sessions" - module.exports = class LadderTabView extends CocoView id: 'ladder-tab-view' template: require 'templates/play/ladder/ladder_tab' diff --git a/app/views/play/ladder/ladder_view.coffee b/app/views/play/ladder/ladder_view.coffee index fa087f686..414d1dad4 100644 --- a/app/views/play/ladder/ladder_view.coffee +++ b/app/views/play/ladder/ladder_view.coffee @@ -43,7 +43,7 @@ module.exports = class LadderView extends RootView onLoaded: -> @teams = teamDataFromLevel @level - @render() + super() getRenderData: -> ctx = super() @@ -54,6 +54,7 @@ module.exports = class LadderView extends RootView ctx.levelDescription = marked(@level.get('description')) if @level.get('description') ctx._ = _ ctx.tournamentTimeLeft = moment(new Date(1402444800000)).fromNow() + ctx.winners = require('views/play/ladder/tournament_results')[@levelID] ctx afterRender: -> diff --git a/app/views/play/ladder/tournament_results.coffee b/app/views/play/ladder/tournament_results.coffee new file mode 100644 index 000000000..fc52d8a9f --- /dev/null +++ b/app/views/play/ladder/tournament_results.coffee @@ -0,0 +1,552 @@ +module.exports = results = greed: {} + +results.greed.humans = [ + {team: "humans", rank: 1, sessionID: "5381e3537585483905a829c1", name: "Wizard Dude", playtime: 63184, wins: 363, losses: 0, score: 363} + {team: "humans", rank: 2, sessionID: "537cf76184c54c6e05c05415", name: "Vettax", playtime: 96757, wins: 365, losses: 9, score: 356} + {team: "humans", rank: 3, sessionID: "53836824c85c223a05f4a1dd", name: "HighSea", playtime: 98996, wins: 354, losses: 7, score: 347} + {team: "humans", rank: 4, sessionID: "537c5cf614aaabe80c69fc8d", name: "BubbleDragon", playtime: 136876, wins: 352, losses: 12, score: 340} + {team: "humans", rank: 5, sessionID: "537bad0cb0d477e005347fb5", name: "Bakanio", playtime: 103933, wins: 348, losses: 11, score: 337} + {team: "humans", rank: 6, sessionID: "537cbf86264b3a7d12eb9d55", name: "zero_degrees", playtime: 4236, wins: 346, losses: 17, score: 329} + {team: "humans", rank: 7, sessionID: "538915d2d06c503805fe40d2", name: "Transistor", playtime: 742, wins: 347, losses: 19, score: 328} + {team: "humans", rank: 8, sessionID: "537bbc56831db4ca0526ced3", name: "Chrc", playtime: 2105, wins: 339, losses: 15, score: 324} + {team: "humans", rank: 9, sessionID: "5381d7f87585483905a825fc", name: "nemoyatpeace", playtime: 121202, wins: 344, losses: 20, score: 324} + {team: "humans", rank: 10, sessionID: "53928ff24ca25c6205e290c0", name: "Catalina", playtime: 239, wins: 342, losses: 27, score: 315} + {team: "humans", rank: 11, sessionID: "5383f8162757353805a97454", name: "Zzadded", playtime: 53485, wins: 334, losses: 28, score: 306} + {team: "humans", rank: 12, sessionID: "537d047084c54c6e05c05ab9", name: "Moojo", playtime: 83687, wins: 331, losses: 27, score: 304} + {team: "humans", rank: 13, sessionID: "537bab02f5b6a9d405ec5107", name: "Pentiado", playtime: 70424, wins: 336, losses: 33, score: 303} + {team: "humans", rank: 14, sessionID: "537d11947e1e10b705bbc4a6", name: "Banadux", playtime: 141292, wins: 333, losses: 32, score: 301} + {team: "humans", rank: 15, sessionID: "538181fd7585483905a801e1", name: "chadnickbok", playtime: 922, wins: 334, losses: 33, score: 301} + {team: "humans", rank: 16, sessionID: "537bc4990de0a02c07e8799a", name: "Mepath", playtime: 94421, wins: 315, losses: 28, score: 287} + {team: "humans", rank: 17, sessionID: "53826025c85c223a05f42c5c", name: "Frederick the Great", playtime: 24696, wins: 325, losses: 42, score: 283} + {team: "humans", rank: 18, sessionID: "5387d070b924da39051331a5", name: "NoMan", playtime: 279296, wins: 320, losses: 46, score: 274} + {team: "humans", rank: 19, sessionID: "535e50c1ba35914a07a308c9", name: "Patogeno", playtime: 69601, wins: 318, losses: 44, score: 274} + {team: "humans", rank: 20, sessionID: "537b9b023803a287057583dd", name: "avv", playtime: 85418, wins: 316, losses: 50, score: 266} + {team: "humans", rank: 21, sessionID: "537c89d814aaabe80c6a1139", name: "Foosvald", playtime: 3197, wins: 307, losses: 41, score: 266} + {team: "humans", rank: 22, sessionID: "538350c778171d3d057eb1e4", name: "Ticaj", playtime: 29249, wins: 316, losses: 52, score: 264} + {team: "humans", rank: 23, sessionID: "537b7d9a5bc02238050d1450", name: "DrMonky", playtime: 14815, wins: 315, losses: 55, score: 260} + {team: "humans", rank: 24, sessionID: "537c7d8526529de30cca4310", name: "imkat", playtime: 14870, wins: 292, losses: 38, score: 254} + {team: "humans", rank: 25, sessionID: "5392283b2446a44105387cbb", name: "AKA", playtime: 134724, wins: 312, losses: 58, score: 254} + {team: "humans", rank: 26, sessionID: "537b7ff9e91b4d3b05525db2", name: "UltraNagog", playtime: 113091, wins: 296, losses: 46, score: 250} + {team: "humans", rank: 27, sessionID: "537c1b0e0ff88b2c06288354", name: "Master J", playtime: 32859, wins: 305, losses: 57, score: 248} + {team: "humans", rank: 28, sessionID: "537c551814aaabe80c69f8e9", name: "texastoast", playtime: 75930, wins: 300, losses: 54, score: 246} + {team: "humans", rank: 29, sessionID: "537b9f2551e98aa705b60500", name: "Otsix", playtime: 2270, wins: 302, losses: 58, score: 244} + {team: "humans", rank: 30, sessionID: "537bb4b88698e13805226bb9", name: "asselinpaul", playtime: 22992, wins: 306, losses: 64, score: 242} + {team: "humans", rank: 31, sessionID: "537b87c6e91b4d3b0552600f", name: "tedshot", playtime: 22872, wins: 306, losses: 65, score: 241} + {team: "humans", rank: 32, sessionID: "537bae76ec57a3e805cbf5b3", name: "Aeter", playtime: 18224, wins: 307, losses: 66, score: 241} + {team: "humans", rank: 33, sessionID: "537b9e8a51e98aa705b604ab", name: "bxp", playtime: 17385, wins: 304, losses: 67, score: 237} + {team: "humans", rank: 34, sessionID: "537bdbe4a41b6b3a059befd0", name: "princeben", playtime: 52246, wins: 290, losses: 56, score: 234} + {team: "humans", rank: 35, sessionID: "537bdf1d375835400576a207", name: "Mal Keshar", playtime: 985, wins: 295, losses: 68, score: 227} + {team: "humans", rank: 36, sessionID: "5383f2ea2757353805a972d3", name: "Mergen", playtime: 37792, wins: 297, losses: 71, score: 226} + {team: "humans", rank: 37, sessionID: "537cec6a9cce053a05c04385", name: "Simulatorboy", playtime: 15925, wins: 295, losses: 75, score: 220} + {team: "humans", rank: 38, sessionID: "537b977d556db17605be76a2", name: "shoebane", playtime: 89060, wins: 287, losses: 68, score: 219} + {team: "humans", rank: 39, sessionID: "538757328ca8b1120b8c0bb5", name: "ProvençalLeGaulois", playtime: 183850, wins: 289, losses: 71, score: 218} + {team: "humans", rank: 40, sessionID: "5384b9803e0daa3905188225", name: "Rincewind the Wizard", playtime: 49014, wins: 280, losses: 64, score: 216} + {team: "humans", rank: 41, sessionID: "53805c227585483905a77c38", name: "guuuuuuuuuuu", playtime: 40835, wins: 290, losses: 76, score: 214} + {team: "humans", rank: 42, sessionID: "537bc0060b070d6b0691fd47", name: "toothpaste", playtime: 116438, wins: 289, losses: 78, score: 211} + {team: "humans", rank: 43, sessionID: "538bc7d05572d43b0520ede9", name: "MaxF", playtime: 16012, wins: 271, losses: 62, score: 209} + {team: "humans", rank: 44, sessionID: "537ca06126529de30cca58a1", name: "firemanphil", playtime: 58857, wins: 287, losses: 81, score: 206} + {team: "humans", rank: 45, sessionID: "537b895ee91b4d3b0552606a", name: "Supaku", playtime: 23623, wins: 281, losses: 79, score: 202} + {team: "humans", rank: 46, sessionID: "537ba1d7903fd2b805155298", name: "Greediest", playtime: 32390, wins: 286, losses: 87, score: 199} + {team: "humans", rank: 47, sessionID: "537ba398903fd2b805155376", name: "RapTorS", playtime: 30828, wins: 280, losses: 83, score: 197} + {team: "humans", rank: 48, sessionID: "537bbb371aaa69c405267714", name: "PIptastic", playtime: 14165, wins: 282, losses: 85, score: 197} + {team: "humans", rank: 49, sessionID: "537fd2f62a5a5acd08dbbe90", name: "aldezar", playtime: 55511, wins: 260, losses: 64, score: 196} + {team: "humans", rank: 50, sessionID: "537c0f440ff88b2c06287c31", name: "yes", playtime: 42677, wins: 251, losses: 55, score: 196} + {team: "humans", rank: 51, sessionID: "537fb985c1a6b99109171054", name: "IamTesting", playtime: 242, wins: 280, losses: 90, score: 190} + {team: "humans", rank: 52, sessionID: "537cdd63665bde1b13ffdede", name: "wiggles", playtime: 9574, wins: 278, losses: 94, score: 184} + {team: "humans", rank: 53, sessionID: "537cda44665bde1b13ffdcb2", name: "Nullable", playtime: 21602, wins: 275, losses: 93, score: 182} + {team: "humans", rank: 54, sessionID: "537bf012b72b2bbe053c9173", name: "hyn", playtime: 5688, wins: 278, losses: 97, score: 181} + {team: "humans", rank: 55, sessionID: "537bda813dd7d35105f5fec2", name: "Rinomon", playtime: 19811, wins: 272, losses: 96, score: 176} + {team: "humans", rank: 56, sessionID: "537b9e7251e98aa705b604a2", name: "stuntman_fx", playtime: 12496, wins: 258, losses: 86, score: 172} + {team: "humans", rank: 57, sessionID: "537d1def7e1e10b705bbcbc4", name: "WizBit", playtime: 35790, wins: 270, losses: 98, score: 172} + {team: "humans", rank: 58, sessionID: "537b8a93e91b4d3b055260f3", name: "Korla March", playtime: 13093, wins: 265, losses: 94, score: 171} + {team: "humans", rank: 59, sessionID: "53873bc17d99a7390561afd2", name: "VicksC", playtime: 65582, wins: 262, losses: 93, score: 169} + {team: "humans", rank: 60, sessionID: "537b907d5bc02238050d1b22", name: "Jonanin", playtime: 60888, wins: 272, losses: 103, score: 169} + {team: "humans", rank: 61, sessionID: "537b9345c74f237005ecc475", name: "AdrianoKF", playtime: 8880, wins: 267, losses: 100, score: 167} + {team: "humans", rank: 62, sessionID: "537ba08651e98aa705b605a8", name: "frickinjason", playtime: 311295, wins: 266, losses: 99, score: 167} + {team: "humans", rank: 63, sessionID: "536076104c5ed51d05284aeb", name: "会打电脑的狼", playtime: 180932, wins: 262, losses: 96, score: 166} + {team: "humans", rank: 64, sessionID: "537fb41bc1a6b99109170daf", name: "avatar652", playtime: 23943, wins: 260, losses: 95, score: 165} + {team: "humans", rank: 65, sessionID: "537bb34a8698e13805226ae1", name: "Superice", playtime: 8561, wins: 267, losses: 103, score: 164} + {team: "humans", rank: 66, sessionID: "537b8f285bc02238050d1a91", name: "renner96", playtime: 8962, wins: 265, losses: 103, score: 162} + {team: "humans", rank: 67, sessionID: "537b97ba556db17605be76bc", name: "howsiwei", playtime: 37865, wins: 249, losses: 90, score: 159} + {team: "humans", rank: 68, sessionID: "537d6e997e6f2dba0510649c", name: "Brainoutoforder", playtime: 79551, wins: 263, losses: 104, score: 159} + {team: "humans", rank: 69, sessionID: "537bbe24d9644630065c90f4", name: "cc", playtime: 27244, wins: 247, losses: 94, score: 153} + {team: "humans", rank: 70, sessionID: "538f055b7558763705258211", name: "Klee", playtime: 29269, wins: 264, losses: 111, score: 153} + {team: "humans", rank: 71, sessionID: "537c3a51e8ea6e790a7cceb2", name: "NovaHorizon", playtime: 72951, wins: 254, losses: 103, score: 151} + {team: "humans", rank: 72, sessionID: "537c3e0dd08a96e40a64f01c", name: "Arnfrid", playtime: 13133, wins: 261, losses: 111, score: 150} + {team: "humans", rank: 73, sessionID: "537b7cf65bc02238050d141f", name: "rawpower", playtime: 60808, wins: 256, losses: 110, score: 146} + {team: "humans", rank: 74, sessionID: "537bcc75c57303a5070c2e9a", name: "dhimdis", playtime: 43092, wins: 256, losses: 111, score: 145} + {team: "humans", rank: 75, sessionID: "538bc1a15572d43b0520e8ae", name: "rednek", playtime: 38220, wins: 250, losses: 106, score: 144} + {team: "humans", rank: 76, sessionID: "5380c8ed7585483905a7ad55", name: "Kungfury", playtime: 628, wins: 254, losses: 113, score: 141} + {team: "humans", rank: 77, sessionID: "537c37dff1d9cfe4096ba9c9", name: "Auralien", playtime: 24844, wins: 255, losses: 115, score: 140} + {team: "humans", rank: 78, sessionID: "539234c52446a441053881ad", name: "Szalami", playtime: 45304, wins: 243, losses: 108, score: 135} + {team: "humans", rank: 79, sessionID: "537c15910ff88b2c06287fd5", name: "Ahrimen", playtime: 24541, wins: 253, losses: 120, score: 133} + {team: "humans", rank: 80, sessionID: "537ff34f4cd8023705770b31", name: "ColtYolo", playtime: 18704, wins: 244, losses: 115, score: 129} + {team: "humans", rank: 81, sessionID: "538ceadc56c3613905300df1", name: "Teshynil", playtime: 59986, wins: 233, losses: 105, score: 128} + {team: "humans", rank: 82, sessionID: "538f174df3691038051651f1", name: "Leas", playtime: 11670, wins: 251, losses: 126, score: 125} + {team: "humans", rank: 83, sessionID: "537d8cae13add33a051be70c", name: "Jamar", playtime: 9358, wins: 245, losses: 123, score: 122} + {team: "humans", rank: 84, sessionID: "53877f428ca8b1120b8c2ba8", name: "madcoder", playtime: 58824, wins: 241, losses: 120, score: 121} + {team: "humans", rank: 85, sessionID: "5391f1f282f0bc4705242218", name: "RedRudeBoy", playtime: 13356, wins: 230, losses: 110, score: 120} + {team: "humans", rank: 86, sessionID: "537bdb1c6b018e5505b9d5ae", name: "Ralkarin", playtime: 16137, wins: 245, losses: 128, score: 117} + {team: "humans", rank: 87, sessionID: "537bc9efa968548907de6dc5", name: "scrumlock", playtime: 43716, wins: 238, losses: 122, score: 116} + {team: "humans", rank: 88, sessionID: "53973dc02546283905a3d603", name: "Olivier_A", playtime: 5903, wins: 246, losses: 131, score: 115} + {team: "humans", rank: 89, sessionID: "537d3fffe20e956205f0da0c", name: "Lecky", playtime: 28974, wins: 242, losses: 127, score: 115} + {team: "humans", rank: 90, sessionID: "537b9edc7c89ec9805f4de89", name: "Cemiv", playtime: 5632, wins: 241, losses: 134, score: 107} + {team: "humans", rank: 91, sessionID: "537b8bede91b4d3b05526151", name: "Witchy", playtime: 29809, wins: 214, losses: 110, score: 104} + {team: "humans", rank: 92, sessionID: "535d728c30f061020b8f9893", name: "ZeoNFrosT", playtime: 0, wins: 233, losses: 130, score: 103} + {team: "humans", rank: 93, sessionID: "537b9c4b16613c8405155abd", name: "JD557", playtime: 17456, wins: 236, losses: 134, score: 102} + {team: "humans", rank: 94, sessionID: "537bdb396b018e5505b9d5ba", name: "devast8a", playtime: 89013, wins: 233, losses: 133, score: 100} + {team: "humans", rank: 95, sessionID: "537b7fbc5bc02238050d14ee", name: "jojman272", playtime: 859, wins: 232, losses: 132, score: 100} + {team: "humans", rank: 96, sessionID: "537cc552e4523d0113ba4eb2", name: "Sir Coward", playtime: 26839, wins: 235, losses: 135, score: 100} + {team: "humans", rank: 97, sessionID: "537b8abde91b4d3b05526103", name: "Jef", playtime: 10311, wins: 236, losses: 139, score: 97} + {team: "humans", rank: 98, sessionID: "537ed4db14bb1d38053b5b72", name: "ModernBarbershop", playtime: 33894, wins: 221, losses: 129, score: 92} + {team: "humans", rank: 99, sessionID: "537b8ba6e91b4d3b0552613c", name: "Wiz", playtime: 1655, wins: 227, losses: 137, score: 90} + {team: "humans", rank: 100, sessionID: "537bad3396ee90f605f2b0f9", name: "Rnq", playtime: 14853, wins: 226, losses: 138, score: 88} + {team: "humans", rank: 101, sessionID: "537f9d5ba7d2578f083d69b4", name: "BLACKORP", playtime: 2869, wins: 225, losses: 138, score: 87} + {team: "humans", rank: 102, sessionID: "537ba00e7c89ec9805f4defc", name: "xoko814", playtime: 869, wins: 225, losses: 139, score: 86} + {team: "humans", rank: 103, sessionID: "5391f83082f0bc47052424c7", name: "tarasiu", playtime: 228, wins: 225, losses: 139, score: 86} + {team: "humans", rank: 104, sessionID: "537d26d77e1e10b705bbd0d5", name: "Traitor", playtime: 8900, wins: 222, losses: 137, score: 85} + {team: "humans", rank: 105, sessionID: "537bbb0a1aaa69c4052676f5", name: "TROGDOR BURNINATE", playtime: 6630, wins: 231, losses: 146, score: 85} + {team: "humans", rank: 106, sessionID: "5387a8a3d06c503805fda60d", name: "masanorinyo", playtime: 68753, wins: 226, losses: 142, score: 84} + {team: "humans", rank: 107, sessionID: "538e8593f369103805160d76", name: "Aggar", playtime: 12492, wins: 228, losses: 144, score: 84} + {team: "humans", rank: 108, sessionID: "537e7eac63734c630505be27", name: "TheRealThrall", playtime: 587, wins: 224, losses: 141, score: 83} + {team: "humans", rank: 109, sessionID: "537c8dfa26529de30cca4c43", name: "xeno", playtime: 15394, wins: 222, losses: 142, score: 80} + {team: "humans", rank: 110, sessionID: "537bef0b8c297aa0055d1184", name: "Floogle", playtime: 36828, wins: 224, losses: 145, score: 79} + {team: "humans", rank: 111, sessionID: "537c666826529de30cca38c2", name: "Jerson Otzoy", playtime: 46519, wins: 201, losses: 122, score: 79} + {team: "humans", rank: 112, sessionID: "537cb6b2a3cb63ea103c8000", name: "phisixersai", playtime: 8170, wins: 224, losses: 146, score: 78} + {team: "humans", rank: 113, sessionID: "537b9ba716613c8405155a73", name: "Beerdroid", playtime: 2547, wins: 226, losses: 150, score: 76} + {team: "humans", rank: 114, sessionID: "537c9bcd26529de30cca54e0", name: "Dood", playtime: 1121, wins: 219, losses: 145, score: 74} + {team: "humans", rank: 115, sessionID: "537c5b7c26529de30cca3439", name: "Markoth", playtime: 787, wins: 220, losses: 147, score: 73} + {team: "humans", rank: 116, sessionID: "537d1c297e1e10b705bbca7d", name: "chotic", playtime: 1292, wins: 218, losses: 146, score: 72} + {team: "humans", rank: 117, sessionID: "538484a53e0daa39051863e4", name: "Drago", playtime: 465, wins: 217, losses: 147, score: 70} + {team: "humans", rank: 118, sessionID: "537baaab03ff8dc005b8436c", name: "Petteri", playtime: 15235, wins: 219, losses: 149, score: 70} + {team: "humans", rank: 119, sessionID: "537ca0aa26529de30cca58c8", name: "3ng3l", playtime: 1703, wins: 217, losses: 147, score: 70} + {team: "humans", rank: 120, sessionID: "537b897f5bc02238050d181c", name: "Jremz", playtime: 8386, wins: 221, losses: 152, score: 69} + {team: "humans", rank: 121, sessionID: "537ba85554a7b1d5053bb366", name: "Taters", playtime: 9664, wins: 193, losses: 126, score: 67} + {team: "humans", rank: 122, sessionID: "537b9220e91b4d3b055263ea", name: "Listr", playtime: 34000, wins: 202, losses: 136, score: 66} + {team: "humans", rank: 123, sessionID: "537cadbd2f6e3aee0ed581f0", name: "satefa", playtime: 10106, wins: 220, losses: 156, score: 64} + {team: "humans", rank: 124, sessionID: "537d0a8c84c54c6e05c05db7", name: "redWizzard", playtime: 53180, wins: 183, losses: 120, score: 63} + {team: "humans", rank: 125, sessionID: "536c6d1c68b5258d0c4b7da8", name: "Tober", playtime: 452, wins: 216, losses: 153, score: 63} + {team: "humans", rank: 126, sessionID: "535ee1e023f09c2c0836a2a7", name: "(ノಠ益ಠ)ノ彡┻━┻", playtime: 3229, wins: 218, losses: 158, score: 60} + {team: "humans", rank: 127, sessionID: "5371626351ce9b3a05d95d5c", name: "Perrekus", playtime: 9047, wins: 215, losses: 158, score: 57} + {team: "humans", rank: 128, sessionID: "53968372e1b8fd0c08c693d5", name: "leoTest", playtime: 10739, wins: 215, losses: 158, score: 57} + {team: "humans", rank: 129, sessionID: "537cfcd69cce053a05c04ce1", name: "Soulnai", playtime: 22641, wins: 210, losses: 154, score: 56} + {team: "humans", rank: 130, sessionID: "537bf57795356a43065f7403", name: "Mordecai", playtime: 11262, wins: 215, losses: 159, score: 56} + {team: "humans", rank: 131, sessionID: "5377aabf05e1483905cb99d5", name: "Umaris", playtime: 23418, wins: 211, losses: 156, score: 55} + {team: "humans", rank: 132, sessionID: "537c93ce26529de30cca5029", name: "Willybe", playtime: 4772, wins: 212, losses: 158, score: 54} + {team: "humans", rank: 133, sessionID: "537babf403838cf0050ace06", name: "Brian Humphrey", playtime: 7300, wins: 212, losses: 158, score: 54} + {team: "humans", rank: 134, sessionID: "537e19e30efa6a37059e0b4f", name: "Aqrrc", playtime: 5003, wins: 210, losses: 163, score: 47} + {team: "humans", rank: 135, sessionID: "5395d19622ca0e39054ea5b1", name: "anykey", playtime: 29278, wins: 201, losses: 155, score: 46} + {team: "humans", rank: 136, sessionID: "537b7fd6e91b4d3b05525da3", name: "Gily", playtime: 17673, wins: 205, losses: 163, score: 42} + {team: "humans", rank: 137, sessionID: "537bbbcb3dc5f3c3055c3040", name: "roseaboveit", playtime: 12398, wins: 205, losses: 168, score: 37} + {team: "humans", rank: 138, sessionID: "538757897d99a7390561bb7f", name: "Jeremy", playtime: 6842, wins: 199, losses: 166, score: 33} + {team: "humans", rank: 139, sessionID: "537b8b845bc02238050d18e8", name: "Abel Soares Siqueira", playtime: 4945, wins: 204, losses: 171, score: 33} + {team: "humans", rank: 140, sessionID: "537b8a37e91b4d3b055260c9", name: "mattmatt", playtime: 77241, wins: 198, losses: 166, score: 32} + {team: "humans", rank: 141, sessionID: "537fc26d2a5a5acd08dbb0a5", name: "Melrakal", playtime: 2393, wins: 200, losses: 173, score: 27} + {team: "humans", rank: 142, sessionID: "537bd8a73dd7d35105f5fd6a", name: "drakiac", playtime: 14601, wins: 196, losses: 169, score: 27} + {team: "humans", rank: 143, sessionID: "537cea3d9cce053a05c04226", name: "CNKLC", playtime: 11663, wins: 200, losses: 175, score: 25} + {team: "humans", rank: 144, sessionID: "537bf9c20ff88b2c06286e7c", name: "Jotipalo", playtime: 4124, wins: 199, losses: 176, score: 23} + {team: "humans", rank: 145, sessionID: "53807d9405e52a3305de99f5", name: "Booya", playtime: 118149, wins: 173, losses: 152, score: 21} + {team: "humans", rank: 146, sessionID: "537bf291d3443b2b068c1056", name: "Penguin", playtime: 7688, wins: 196, losses: 176, score: 20} + {team: "humans", rank: 147, sessionID: "5380b82005e52a3305deb679", name: "TheWrightDev", playtime: 26967, wins: 194, losses: 176, score: 18} + {team: "humans", rank: 148, sessionID: "537cf2aa98a1be440545d878", name: "Kipernicus", playtime: 11906, wins: 189, losses: 181, score: 8} + {team: "humans", rank: 149, sessionID: "535e1f1187ab8481075b8d95", name: "freek", playtime: 0, wins: 190, losses: 183, score: 7} + {team: "humans", rank: 150, sessionID: "535f0911d42bda62085e40fc", name: "heated", playtime: 65, wins: 188, losses: 182, score: 6} + {team: "humans", rank: 151, sessionID: "539120d882f0bc470523cd6f", name: "Fitbos", playtime: 1622, wins: 0, losses: 0, score: 0} + {team: "humans", rank: 152, sessionID: "539875130599fa3a05ca7293", name: "jeimmy", playtime: 663, wins: 0, losses: 0, score: 0} + {team: "humans", rank: 153, sessionID: "537b7ccce91b4d3b05525cdc", name: "Bart2121", playtime: 4177, wins: 184, losses: 187, score: -3} + {team: "humans", rank: 154, sessionID: "53866ed58ca8b1120b8b816d", name: "Nikolai", playtime: 26320, wins: 184, losses: 187, score: -3} + {team: "humans", rank: 155, sessionID: "5385610f3e0daa390518e2f6", name: "Stravinsky", playtime: 8870, wins: 182, losses: 185, score: -3} + {team: "humans", rank: 156, sessionID: "537b94956c13497e05de816e", name: "Maix", playtime: 3163, wins: 183, losses: 186, score: -3} + {team: "humans", rank: 157, sessionID: "537187dcce8e453b05c9d21c", name: "NasytToast", playtime: 0, wins: 183, losses: 188, score: -5} + {team: "humans", rank: 158, sessionID: "537bb696dd932f6b05d370fd", name: "HandmadeMercury", playtime: 5539, wins: 169, losses: 175, score: -6} + {team: "humans", rank: 159, sessionID: "537b7f605bc02238050d14c9", name: "Jaden", playtime: 10547, wins: 182, losses: 192, score: -10} + {team: "humans", rank: 160, sessionID: "537c489314aaabe80c69f413", name: "HiddEnigma", playtime: 5850, wins: 181, losses: 191, score: -10} + {team: "humans", rank: 161, sessionID: "537bb7cd1e8dd17a054c1044", name: "Alexander the Grape", playtime: 30398, wins: 178, losses: 189, score: -11} + {team: "humans", rank: 162, sessionID: "538eada37558763705255a0c", name: "Evran", playtime: 22253, wins: 179, losses: 192, score: -13} + {team: "humans", rank: 163, sessionID: "5386aedf7d99a7390561802a", name: "Reclusiarch", playtime: 3281, wins: 178, losses: 192, score: -14} + {team: "humans", rank: 164, sessionID: "538b7202b924da3905157e07", name: "Mlatic", playtime: 723, wins: 176, losses: 190, score: -14} + {team: "humans", rank: 165, sessionID: "537c4411c209bbd60c16a3d8", name: "liorst1", playtime: 14571, wins: 178, losses: 193, score: -15} + {team: "humans", rank: 166, sessionID: "537fcaee2a5a5acd08dbb79c", name: "Booya2nd", playtime: 9598, wins: 153, losses: 168, score: -15} + {team: "humans", rank: 167, sessionID: "538ae84fb924da3905154172", name: "WAAW", playtime: 3668, wins: 177, losses: 194, score: -17} + {team: "humans", rank: 168, sessionID: "538751b78ca8b1120b8c0792", name: "DZC", playtime: 5512, wins: 170, losses: 188, score: -18} + {team: "humans", rank: 169, sessionID: "537c0ab50ff88b2c0628792e", name: "maxily", playtime: 18792, wins: 174, losses: 192, score: -18} + {team: "humans", rank: 170, sessionID: "537ba1347c89ec9805f4df76", name: "qkhhly", playtime: 9518, wins: 176, losses: 196, score: -20} + {team: "humans", rank: 171, sessionID: "538b4bbdd06c503805ff3b8a", name: "iamcodewar", playtime: 10939, wins: 178, losses: 198, score: -20} + {team: "humans", rank: 172, sessionID: "537f14e18fe881f1055df20a", name: "Alanor", playtime: 20437, wins: 150, losses: 174, score: -24} + {team: "humans", rank: 173, sessionID: "537f57ff3282d7a507695039", name: "Navi' Dendi", playtime: 4382, wins: 172, losses: 197, score: -25} + {team: "humans", rank: 174, sessionID: "537c14950ff88b2c06287f22", name: "lolka", playtime: 3820, wins: 172, losses: 200, score: -28} + {team: "humans", rank: 175, sessionID: "53570b7a1bfa9bba14b5e045", name: "Scott", playtime: 6228, wins: 173, losses: 202, score: -29} + {team: "humans", rank: 176, sessionID: "537bbd0f3cd816fa05a48401", name: "DonutBaron", playtime: 9716, wins: 165, losses: 195, score: -30} + {team: "humans", rank: 177, sessionID: "5381726d7585483905a7f978", name: "armlol", playtime: 5149, wins: 171, losses: 204, score: -33} + {team: "humans", rank: 178, sessionID: "538c0bfb56c36139052fa076", name: "Aura Temple Network", playtime: 7489, wins: 169, losses: 205, score: -36} + {team: "humans", rank: 179, sessionID: "538ec77af3691038051629c3", name: "BI", playtime: 3870, wins: 164, losses: 200, score: -36} + {team: "humans", rank: 180, sessionID: "537b889fe91b4d3b0552603e", name: "McDerp", playtime: 3415, wins: 161, losses: 203, score: -42} + {team: "humans", rank: 181, sessionID: "537d169a7e1e10b705bbc736", name: "Nitor", playtime: 7036, wins: 159, losses: 205, score: -46} + {team: "humans", rank: 182, sessionID: "53837eee6f3bea3a05faf5fb", name: "HeadCrusher", playtime: 22806, wins: 133, losses: 182, score: -49} + {team: "humans", rank: 183, sessionID: "537ba4887c89ec9805f4e10d", name: "Geralt", playtime: 2353, wins: 158, losses: 208, score: -50} + {team: "humans", rank: 184, sessionID: "53802aab05e52a3305de797e", name: "micblayo", playtime: 14726, wins: 159, losses: 209, score: -50} + {team: "humans", rank: 185, sessionID: "538b60bdb924da39051576e2", name: "mydoom", playtime: 9270, wins: 159, losses: 209, score: -50} + {team: "humans", rank: 186, sessionID: "5380f3917585483905a7c0b3", name: "Lalaland1125", playtime: 689, wins: 160, losses: 211, score: -51} + {team: "humans", rank: 187, sessionID: "537b8125e91b4d3b05525e1e", name: "Garrett", playtime: 1833, wins: 157, losses: 212, score: -55} + {team: "humans", rank: 188, sessionID: "537b8c60e91b4d3b05526181", name: "Usopp", playtime: 8340, wins: 156, losses: 217, score: -61} + {team: "humans", rank: 189, sessionID: "537b9b4116613c8405155a41", name: "Y0DA", playtime: 5351, wins: 154, losses: 216, score: -62} + {team: "humans", rank: 190, sessionID: "5387f127d06c503805fdcd97", name: "Hexadecimage", playtime: 89476, wins: 151, losses: 222, score: -71} + {team: "humans", rank: 191, sessionID: "539265012446a44105389a8c", name: "odeakihumi", playtime: 23819, wins: 143, losses: 219, score: -76} + {team: "humans", rank: 192, sessionID: "537ba73a980cfcba051f19c5", name: "Torg", playtime: 7045, wins: 145, losses: 221, score: -76} + {team: "humans", rank: 193, sessionID: "5392bbc0e04ba13805ed4c0f", name: "iownspace", playtime: 51659, wins: 146, losses: 223, score: -77} + {team: "humans", rank: 194, sessionID: "537bf14bf8b46fbd0544fc18", name: "danshou", playtime: 22590, wins: 148, losses: 226, score: -78} + {team: "humans", rank: 195, sessionID: "537b212dfe0ec03905fcd712", name: "ThunderClan", playtime: 766, wins: 144, losses: 228, score: -84} + {team: "humans", rank: 196, sessionID: "536540d51415c79e648b1de5", name: "Nick04 Bubonic", playtime: 12, wins: 138, losses: 222, score: -84} + {team: "humans", rank: 197, sessionID: "537fb0792a5a5acd08dba76f", name: "Slackus", playtime: 49736, wins: 141, losses: 226, score: -85} + {team: "humans", rank: 198, sessionID: "537d44911333605305550411", name: "tehowner", playtime: 2969, wins: 138, losses: 223, score: -85} + {team: "humans", rank: 199, sessionID: "537df429933d99860613ab2d", name: "Farafonoff", playtime: 4315, wins: 142, losses: 228, score: -86} + {team: "humans", rank: 200, sessionID: "538501137d99a7390560db54", name: "Joodoc", playtime: 6042, wins: 144, losses: 231, score: -87} + {team: "humans", rank: 201, sessionID: "537b8a58e91b4d3b055260d8", name: "Terebijoke", playtime: 85680, wins: 122, losses: 210, score: -88} + {team: "humans", rank: 202, sessionID: "537c110195356a43065f8450", name: "Chen Yu LIu", playtime: 843, wins: 140, losses: 232, score: -92} + {team: "humans", rank: 203, sessionID: "537dba4613add33a051bf680", name: "Bobbybaby", playtime: 1509, wins: 140, losses: 232, score: -92} + {team: "humans", rank: 204, sessionID: "537b9003e91b4d3b05526329", name: "Coreth", playtime: 11677, wins: 131, losses: 224, score: -93} + {team: "humans", rank: 205, sessionID: "538b618bd06c503805ff4210", name: "robat", playtime: 5776, wins: 138, losses: 234, score: -96} + {team: "humans", rank: 206, sessionID: "5383b7196f3bea3a05fb1472", name: "lilos", playtime: 2464, wins: 135, losses: 232, score: -97} + {team: "humans", rank: 207, sessionID: "537f8672d409ac270861c842", name: "ThatOtherPerson", playtime: 2018, wins: 137, losses: 236, score: -99} + {team: "humans", rank: 208, sessionID: "53809c2c7585483905a795f1", name: "都比", playtime: 28855, wins: 136, losses: 235, score: -99} + {team: "humans", rank: 209, sessionID: "537c866f14aaabe80c6a0f90", name: "Energy", playtime: 1276, wins: 130, losses: 229, score: -99} + {team: "humans", rank: 210, sessionID: "538e4af0755876370525291c", name: "commando Tech", playtime: 3725, wins: 136, losses: 236, score: -100} + {team: "humans", rank: 211, sessionID: "537b9da67c89ec9805f4de0b", name: "tembelu", playtime: 9298, wins: 129, losses: 230, score: -101} + {team: "humans", rank: 212, sessionID: "53972a5f2546283905a3bdc5", name: "holyKoT", playtime: 2171, wins: 131, losses: 241, score: -110} + {team: "humans", rank: 213, sessionID: "537ba6dd980cfcba051f199d", name: "Aaron1011", playtime: 3449, wins: 130, losses: 240, score: -110} + {team: "humans", rank: 214, sessionID: "537c68bf26529de30cca39cb", name: "ESWAT", playtime: 893, wins: 128, losses: 239, score: -111} + {team: "humans", rank: 215, sessionID: "537d5fdb3dcf67c40571fb13", name: "JustTurrable", playtime: 264, wins: 125, losses: 240, score: -115} + {team: "humans", rank: 216, sessionID: "537e4268e1489fe206667e26", name: "EvanK", playtime: 668, wins: 125, losses: 245, score: -120} + {team: "humans", rank: 217, sessionID: "537be834a3b60b390500e0a3", name: "gilxa1226", playtime: 394, wins: 125, losses: 245, score: -120} + {team: "humans", rank: 218, sessionID: "537bbcfd9e1bf5f905926ebf", name: "Qin Shi Huang", playtime: 4691, wins: 125, losses: 246, score: -121} + {team: "humans", rank: 219, sessionID: "537b9bd016613c8405155a8b", name: "WaffleFries", playtime: 2944, wins: 121, losses: 246, score: -125} + {team: "humans", rank: 220, sessionID: "537b8a65e91b4d3b055260e0", name: "Gurra", playtime: 6295, wins: 107, losses: 233, score: -126} + {team: "humans", rank: 221, sessionID: "537e2025933d99860613c614", name: "Encosia", playtime: 2025, wins: 118, losses: 250, score: -132} + {team: "humans", rank: 222, sessionID: "537bb694dd932f6b05d370fc", name: "Kaboomm", playtime: 4333, wins: 116, losses: 253, score: -137} + {team: "humans", rank: 223, sessionID: "538001aebf8ae33a0501b98e", name: "taichi_kunnn", playtime: 3524, wins: 112, losses: 254, score: -142} + {team: "humans", rank: 224, sessionID: "53630b6573bdb4f7045a772c", name: "Lonib", playtime: 0, wins: 113, losses: 257, score: -144} + {team: "humans", rank: 225, sessionID: "5360f83b67c29a0609ddceb4", name: "Readper", playtime: 0, wins: 109, losses: 264, score: -155} + {team: "humans", rank: 226, sessionID: "53717f4551ce9b3a05d96043", name: "cpkenn09y", playtime: 0, wins: 106, losses: 263, score: -157} + {team: "humans", rank: 227, sessionID: "537b9b9416613c8405155a6b", name: "Rubini", playtime: 6763, wins: 103, losses: 271, score: -168} + {team: "humans", rank: 228, sessionID: "53902ad775587637052615d9", name: "Loxk", playtime: 32690, wins: 89, losses: 265, score: -176} + {team: "humans", rank: 229, sessionID: "537b93886c13497e05de80a6", name: "Klomnar", playtime: 405, wins: 98, losses: 274, score: -176} + {team: "humans", rank: 230, sessionID: "537b9c7c16613c8405155ae0", name: "dpen2000", playtime: 4313, wins: 93, losses: 273, score: -180} + {team: "humans", rank: 231, sessionID: "53692c2d84e82a3a0553305e", name: "rnprdk", playtime: 0, wins: 94, losses: 278, score: -184} + {team: "humans", rank: 232, sessionID: "537be99f20a501380564e764", name: "rizend", playtime: 3017, wins: 92, losses: 279, score: -187} + {team: "humans", rank: 233, sessionID: "537ba94d54a7b1d5053bb426", name: "Rokner", playtime: 3911, wins: 90, losses: 282, score: -192} + {team: "humans", rank: 234, sessionID: "537b949e556db17605be74ff", name: "baldeagle", playtime: 7698, wins: 91, losses: 284, score: -193} + {team: "humans", rank: 235, sessionID: "535f0cdfc2e83ad9048faef0", name: "Mobius", playtime: 0, wins: 87, losses: 288, score: -201} + {team: "humans", rank: 236, sessionID: "537bbe446d5f9f2f06e0c674", name: "Lakk", playtime: 12072, wins: 76, losses: 296, score: -220} + {team: "humans", rank: 237, sessionID: "535ac395d83daa1a052494c1", name: "Michael S.", playtime: 304, wins: 68, losses: 304, score: -236} + {team: "humans", rank: 238, sessionID: "53718c6cce8e453b05c9d298", name: "Blitz", playtime: 0, wins: 68, losses: 306, score: -238} + {team: "humans", rank: 239, sessionID: "536310848e22980605aa58c3", name: "sjarvie", playtime: 0, wins: 66, losses: 308, score: -242} + {team: "humans", rank: 240, sessionID: "537ba10e51e98aa705b605df", name: "Deleu", playtime: 7241, wins: 61, losses: 307, score: -246} + {team: "humans", rank: 241, sessionID: "537b92765bc02238050d1bf0", name: "Deneim", playtime: 13791, wins: 62, losses: 312, score: -250} + {team: "humans", rank: 242, sessionID: "538e609af36910380515f8d6", name: "Volgax", playtime: 6774, wins: 59, losses: 310, score: -251} + {team: "humans", rank: 243, sessionID: "537ba62557494fc405a6faac", name: "Zandrasco", playtime: 4193, wins: 60, losses: 315, score: -255} + {team: "humans", rank: 244, sessionID: "537ecf5414bb1d38053b58bb", name: "Sir Mouse", playtime: 16021, wins: 52, losses: 321, score: -269} + {team: "humans", rank: 245, sessionID: "538c8faf5572d43b05214e3b", name: "AceWizard", playtime: 54, wins: 45, losses: 315, score: -270} + {team: "humans", rank: 246, sessionID: "537deb820efa6a37059df268", name: "MadMan30", playtime: 316, wins: 46, losses: 319, score: -273} + {team: "humans", rank: 247, sessionID: "537bf38495356a43065f7291", name: "NicRio", playtime: 130, wins: 43, losses: 316, score: -273} + {team: "humans", rank: 248, sessionID: "537cd876665bde1b13ffdad4", name: "Is_G", playtime: 776, wins: 44, losses: 319, score: -275} + {team: "humans", rank: 249, sessionID: "537bb14b4f81f53805b6366a", name: "jeromeASF", playtime: 335, wins: 41, losses: 318, score: -277} + {team: "humans", rank: 250, sessionID: "5381745005e52a3305df1c23", name: "El Psy Congr00", playtime: 1032, wins: 41, losses: 319, score: -278} + {team: "humans", rank: 251, sessionID: "538451183e0daa39051845a0", name: "loquele", playtime: 549, wins: 43, losses: 322, score: -279} + {team: "humans", rank: 252, sessionID: "537b606a2daeeb3905a9292f", name: "DeathStalker", playtime: 166, wins: 38, losses: 319, score: -281} + {team: "humans", rank: 253, sessionID: "537bd56ab0d1766d05243002", name: "thelion", playtime: 338, wins: 40, losses: 323, score: -283} + {team: "humans", rank: 254, sessionID: "537d15d37e1e10b705bbc6ef", name: "Diegobrp", playtime: 2132, wins: 37, losses: 321, score: -284} + {team: "humans", rank: 255, sessionID: "537caa102f6e3aee0ed57f84", name: "Sakares Saengkaew", playtime: 34, wins: 37, losses: 321, score: -284} + {team: "humans", rank: 256, sessionID: "53627e2fca5c6f3c11ebc9b1", name: "Animex", playtime: 0, wins: 38, losses: 324, score: -286} + {team: "humans", rank: 257, sessionID: "537bc0ad1c8dfa820699fcb9", name: "Black Mage Wizard Guy", playtime: 691, wins: 37, losses: 323, score: -286} + {team: "humans", rank: 258, sessionID: "535f07d39894af8f7fd84e85", name: "Maksym", playtime: 0, wins: 38, losses: 327, score: -289} + {team: "humans", rank: 259, sessionID: "537b98e33803a287057582a8", name: "Angeland", playtime: 1758, wins: 41, losses: 330, score: -289} + {team: "humans", rank: 260, sessionID: "537c63f426529de30cca37ba", name: "Dmitry", playtime: 181, wins: 35, losses: 325, score: -290} + {team: "humans", rank: 261, sessionID: "537ceb1798a1be440545d40e", name: "jaba", playtime: 572, wins: 35, losses: 325, score: -290} + {team: "humans", rank: 262, sessionID: "537c0f3f0ff88b2c06287c2b", name: "Quan Pham", playtime: 4353, wins: 40, losses: 332, score: -292} + {team: "humans", rank: 263, sessionID: "537d60467e6f2dba051060b6", name: "rhall", playtime: 67548, wins: 11, losses: 316, score: -305} + {team: "humans", rank: 264, sessionID: "5384c9de7d99a7390560bfd7", name: "Secathor", playtime: 16404, wins: 28, losses: 345, score: -317} + {team: "humans", rank: 265, sessionID: "538a08bed06c503805feb6b0", name: "Nemoy", playtime: 1643, wins: 0, losses: 319, score: -319} + {team: "humans", rank: 266, sessionID: "537c3806f1d9cfe4096ba9d8", name: "Hannofcart", playtime: 17397, wins: 0, losses: 323, score: -323} + {team: "humans", rank: 267, sessionID: "537c4d5f26529de30cca2e68", name: "Morphumax", playtime: 58395, wins: 0, losses: 323, score: -323} + {team: "humans", rank: 268, sessionID: "53864aa37d99a73905615dda", name: "shoetest", playtime: 14876, wins: 0, losses: 324, score: -324} + {team: "humans", rank: 269, sessionID: "537b97cd556db17605be76c3", name: "domrein", playtime: 12339, wins: 0, losses: 324, score: -324} + {team: "humans", rank: 270, sessionID: "53835f1fc85c223a05f49ddb", name: "hace", playtime: 9175, wins: 0, losses: 325, score: -325} + {team: "humans", rank: 271, sessionID: "53666cc75af8b2c71dc017e8", name: "Bonesdog", playtime: 0, wins: 0, losses: 326, score: -326} + {team: "humans", rank: 272, sessionID: "537be26d20a501380564e359", name: "Cinnamon Scrolls", playtime: 6619, wins: 0, losses: 330, score: -330} + {team: "humans", rank: 273, sessionID: "537b8f3ce91b4d3b055262d7", name: "asdasd", playtime: 242330, wins: 0, losses: 335, score: -335} + {team: "humans", rank: 274, sessionID: "5383551cc85c223a05f49992", name: "marthyi", playtime: 30008, wins: 325, losses: 43, score: 282} + {team: "humans", rank: 275, sessionID: "538f5334f369103805167613", name: "Leshka", playtime: 10409, wins: 93, losses: 282, score: -189} +] + +results.greed.ogres = [ + {team: "ogres", rank: 1, sessionID: "537b958e6c13497e05de81f2", name: "Bellardia", playtime: 189111, wins: 387, losses: 7, score: 380} + {team: "ogres", rank: 2, sessionID: "537c11df95356a43065f84c9", name: "blinkingTore", playtime: 38360, wins: 381, losses: 9, score: 372} + {team: "ogres", rank: 3, sessionID: "537c09ee95356a43065f801c", name: "Eye", playtime: 172960, wins: 370, losses: 18, score: 352} + {team: "ogres", rank: 4, sessionID: "537b7d475bc02238050d1439", name: "Cripi", playtime: 61966, wins: 373, losses: 23, score: 350} + {team: "ogres", rank: 5, sessionID: "537e317450d1673a054b7dd2", name: "Pop-up", playtime: 39939, wins: 369, losses: 26, score: 343} + {team: "ogres", rank: 6, sessionID: "537e364a50d1673a054b8322", name: "Ravenclaw", playtime: 4579, wins: 367, losses: 24, score: 343} + {team: "ogres", rank: 7, sessionID: "537cb51fa0d7a5cc10992b11", name: "JerryP", playtime: 52872, wins: 368, losses: 33, score: 335} + {team: "ogres", rank: 8, sessionID: "537bd37888a86e67053f9d61", name: "Mickydtron", playtime: 38533, wins: 368, losses: 35, score: 333} + {team: "ogres", rank: 9, sessionID: "5384a3f63e0daa390518765d", name: "ProfBoesch", playtime: 10366, wins: 365, losses: 34, score: 331} + {team: "ogres", rank: 10, sessionID: "538064d27585483905a77f9f", name: "KaosWalking", playtime: 88663, wins: 359, losses: 35, score: 324} + {team: "ogres", rank: 11, sessionID: "53892a98d06c503805fe522b", name: "Urdaris", playtime: 87750, wins: 353, losses: 32, score: 321} + {team: "ogres", rank: 12, sessionID: "53668d64c6ab10ef1ea9a10d", name: "no_login_found", playtime: 11313, wins: 355, losses: 46, score: 309} + {team: "ogres", rank: 13, sessionID: "537f68bc14f08dec07950945", name: "Take it easy", playtime: 2839, wins: 348, losses: 47, score: 301} + {team: "ogres", rank: 14, sessionID: "537bac56b0d477e005347f67", name: "gosunero", playtime: 41429, wins: 346, losses: 54, score: 292} + {team: "ogres", rank: 15, sessionID: "536d258f68b5258d0c4b96ed", name: "Jex", playtime: 4910, wins: 337, losses: 48, score: 289} + {team: "ogres", rank: 16, sessionID: "537bcba8a797656f07c93db5", name: "Durbination", playtime: 13465, wins: 337, losses: 49, score: 288} + {team: "ogres", rank: 17, sessionID: "5381e48a7585483905a82a2d", name: "Edoardo Morandi", playtime: 96844, wins: 330, losses: 49, score: 281} + {team: "ogres", rank: 18, sessionID: "538471737d99a73905608fb4", name: "Artraxus", playtime: 61114, wins: 336, losses: 60, score: 276} + {team: "ogres", rank: 19, sessionID: "537ba9a154a7b1d5053bb45a", name: "skeltoac", playtime: 39087, wins: 330, losses: 55, score: 275} + {team: "ogres", rank: 20, sessionID: "537be145a3b60b390500dcc3", name: "ReyO", playtime: 204112, wins: 328, losses: 55, score: 273} + {team: "ogres", rank: 21, sessionID: "537cc939665bde1b13ffd197", name: "Orson Peters", playtime: 24821, wins: 333, losses: 60, score: 273} + {team: "ogres", rank: 22, sessionID: "5395cf0b7362c439054f7c3a", name: "nino48", playtime: 33541, wins: 327, losses: 62, score: 265} + {team: "ogres", rank: 23, sessionID: "537d541e3dcf67c40571f792", name: "pbd", playtime: 4062, wins: 319, losses: 84, score: 235} + {team: "ogres", rank: 24, sessionID: "537e284650d1673a054b7602", name: "Tech", playtime: 13208, wins: 306, losses: 71, score: 235} + {team: "ogres", rank: 25, sessionID: "537d025184c54c6e05c059b7", name: "Storm0x2a", playtime: 1026, wins: 306, losses: 71, score: 235} + {team: "ogres", rank: 26, sessionID: "537bed83f575b482052f03a0", name: "Blindfold", playtime: 45865, wins: 303, losses: 69, score: 234} + {team: "ogres", rank: 27, sessionID: "537bc4750de0a02c07e8797f", name: "Walter Danilo Galante", playtime: 24476, wins: 313, losses: 84, score: 229} + {team: "ogres", rank: 28, sessionID: "538d0a4356c3613905301e50", name: "Fedux", playtime: 1862, wins: 300, losses: 79, score: 221} + {team: "ogres", rank: 29, sessionID: "537b8cbee91b4d3b0552619e", name: "Nazywam", playtime: 6395, wins: 303, losses: 83, score: 220} + {team: "ogres", rank: 30, sessionID: "5382502a7585483905a85e42", name: "vlizard", playtime: 803, wins: 308, losses: 89, score: 219} + {team: "ogres", rank: 31, sessionID: "5385d1cf3e0daa3905191e57", name: "olu", playtime: 65635, wins: 301, losses: 86, score: 215} + {team: "ogres", rank: 32, sessionID: "537babd003838cf0050acdf2", name: "Pentar", playtime: 36006, wins: 309, losses: 95, score: 214} + {team: "ogres", rank: 33, sessionID: "537c31112b08c344082e22de", name: "Agathanar", playtime: 41371, wins: 308, losses: 94, score: 214} + {team: "ogres", rank: 34, sessionID: "537cd98ae4523d0113ba5ac7", name: "Tehvudgaw", playtime: 81681, wins: 298, losses: 94, score: 204} + {team: "ogres", rank: 35, sessionID: "537b803ae91b4d3b05525dc7", name: "Forsaken", playtime: 50237, wins: 294, losses: 91, score: 203} + {team: "ogres", rank: 36, sessionID: "537b7e44e91b4d3b05525d38", name: "COGSMITH", playtime: 284674, wins: 300, losses: 98, score: 202} + {team: "ogres", rank: 37, sessionID: "537ba3e0903fd2b80515539d", name: "Statik", playtime: 64062, wins: 294, losses: 98, score: 196} + {team: "ogres", rank: 38, sessionID: "537b85e15bc02238050d16d4", name: "Cuef", playtime: 81666, wins: 286, losses: 90, score: 196} + {team: "ogres", rank: 39, sessionID: "5392f7b1304ab93805efa3d2", name: "Cracker", playtime: 2576, wins: 282, losses: 90, score: 192} + {team: "ogres", rank: 40, sessionID: "537b9c42ffeaa29e051b2657", name: "JamesJNadeau.com", playtime: 28164, wins: 295, losses: 110, score: 185} + {team: "ogres", rank: 41, sessionID: "537ce37be9c4e97c05e3ea66", name: "Quasar", playtime: 21773, wins: 287, losses: 104, score: 183} + {team: "ogres", rank: 42, sessionID: "537baa2403ff8dc005b8433e", name: "Almatia", playtime: 15387, wins: 291, losses: 109, score: 182} + {team: "ogres", rank: 43, sessionID: "537b87f55bc02238050d1786", name: "mordonne", playtime: 71830, wins: 277, losses: 103, score: 174} + {team: "ogres", rank: 44, sessionID: "537e56129008f87b0541cd03", name: "O'Connor", playtime: 55869, wins: 281, losses: 109, score: 172} + {team: "ogres", rank: 45, sessionID: "537ba9e57b9ffadb05476784", name: "timmox", playtime: 13280, wins: 256, losses: 86, score: 170} + {team: "ogres", rank: 46, sessionID: "537b86e75bc02238050d1721", name: "Tomas", playtime: 21845, wins: 275, losses: 106, score: 169} + {team: "ogres", rank: 47, sessionID: "537bebbea3b60b390500e280", name: "Buge", playtime: 3506, wins: 281, losses: 118, score: 163} + {team: "ogres", rank: 48, sessionID: "53854c0b7d99a7390560f9f4", name: "MatBot", playtime: 15049, wins: 281, losses: 119, score: 162} + {team: "ogres", rank: 49, sessionID: "5380a9bb7585483905a79dff", name: "salesman", playtime: 60290, wins: 277, losses: 118, score: 159} + {team: "ogres", rank: 50, sessionID: "53949f635009fe5b075bb584", name: "Dematerial", playtime: 4661, wins: 273, losses: 116, score: 157} + {team: "ogres", rank: 51, sessionID: "53832a3ec85c223a05f48736", name: "Popey", playtime: 37593, wins: 256, losses: 104, score: 152} + {team: "ogres", rank: 52, sessionID: "537b89a2e91b4d3b05526082", name: "Ryemane", playtime: 20671, wins: 276, losses: 125, score: 151} + {team: "ogres", rank: 53, sessionID: "537cd542e4523d0113ba5782", name: "schups", playtime: 20886, wins: 277, losses: 127, score: 150} + {team: "ogres", rank: 54, sessionID: "537be1cca3b60b390500dd24", name: "ohsnap", playtime: 3574, wins: 273, losses: 127, score: 146} + {team: "ogres", rank: 55, sessionID: "538bef7356c36139052f8c80", name: "Hephaestian", playtime: 9626, wins: 272, losses: 131, score: 141} + {team: "ogres", rank: 56, sessionID: "537c527026529de30cca302f", name: "Jinrai", playtime: 23726, wins: 269, losses: 129, score: 140} + {team: "ogres", rank: 57, sessionID: "538471087d99a73905608f84", name: "Basque", playtime: 37986, wins: 269, losses: 130, score: 139} + {team: "ogres", rank: 58, sessionID: "537cd3c3665bde1b13ffd74c", name: "Stormaggedon Dark Lord of All", playtime: 27471, wins: 261, losses: 123, score: 138} + {team: "ogres", rank: 59, sessionID: "537b984f3803a2870575824f", name: "Cody", playtime: 85426, wins: 259, losses: 138, score: 121} + {team: "ogres", rank: 60, sessionID: "5383ead92757353805a96ff4", name: "hotdogeater", playtime: 55657, wins: 258, losses: 137, score: 121} + {team: "ogres", rank: 61, sessionID: "537d2a397e1e10b705bbd2c8", name: "PatchworkKnight", playtime: 23663, wins: 257, losses: 138, score: 119} + {team: "ogres", rank: 62, sessionID: "537b8185e91b4d3b05525e3c", name: "FlameFrost", playtime: 29939, wins: 251, losses: 142, score: 109} + {team: "ogres", rank: 63, sessionID: "537bb4168623bc410575f208", name: "NitrousDave", playtime: 29420, wins: 252, losses: 143, score: 109} + {team: "ogres", rank: 64, sessionID: "537f4fa1c3067a7b07ba7786", name: "Zodd", playtime: 8820, wins: 248, losses: 140, score: 108} + {team: "ogres", rank: 65, sessionID: "538358b978171d3d057eb471", name: "Lololala", playtime: 39892, wins: 252, losses: 144, score: 108} + {team: "ogres", rank: 66, sessionID: "537f224c8fe881f1055dfb7e", name: "Artoemius", playtime: 1470, wins: 251, losses: 150, score: 101} + {team: "ogres", rank: 67, sessionID: "537b7b25e91b4d3b05525b61", name: "Makaze", playtime: 21714, wins: 248, losses: 149, score: 99} + {team: "ogres", rank: 68, sessionID: "537cf18c9cce053a05c0467f", name: "CodeBane", playtime: 39749, wins: 233, losses: 137, score: 96} + {team: "ogres", rank: 69, sessionID: "537baac910c8a6e405b9cf92", name: "cooler", playtime: 49558, wins: 229, losses: 134, score: 95} + {team: "ogres", rank: 70, sessionID: "5380d0a405e52a3305dec328", name: "DoctorTacoPhD", playtime: 9723, wins: 249, losses: 155, score: 94} + {team: "ogres", rank: 71, sessionID: "53948cfc5009fe5b075ba92d", name: "zeus1200", playtime: 236, wins: 246, losses: 152, score: 94} + {team: "ogres", rank: 72, sessionID: "537c66d114aaabe80c6a0072", name: "Vratislav", playtime: 21203, wins: 237, losses: 144, score: 93} + {team: "ogres", rank: 73, sessionID: "537b996016613c8405155922", name: "Dafe", playtime: 701, wins: 232, losses: 140, score: 92} + {team: "ogres", rank: 74, sessionID: "537b8271e91b4d3b05525e8e", name: "@billyvg", playtime: 20844, wins: 243, losses: 154, score: 89} + {team: "ogres", rank: 75, sessionID: "537caafc2f6e3aee0ed58007", name: "Sakares", playtime: 1257, wins: 237, losses: 151, score: 86} + {team: "ogres", rank: 76, sessionID: "538142de05e52a3305df037d", name: "Meowth", playtime: 12054, wins: 241, losses: 155, score: 86} + {team: "ogres", rank: 77, sessionID: "537155a1ee35af3905987a87", name: "Lanner", playtime: 3658, wins: 236, losses: 152, score: 84} + {team: "ogres", rank: 78, sessionID: "537b99573803a287057582e3", name: "DavyWong", playtime: 16467, wins: 242, losses: 161, score: 81} + {team: "ogres", rank: 79, sessionID: "537be8fda3b60b390500e105", name: "BeAsT MoDe", playtime: 2097, wins: 234, losses: 153, score: 81} + {team: "ogres", rank: 80, sessionID: "537b96696c13497e05de8271", name: "vkeg", playtime: 1883, wins: 236, losses: 157, score: 79} + {team: "ogres", rank: 81, sessionID: "537ca60cb128a1850e083077", name: "SPAMR", playtime: 19978, wins: 233, losses: 154, score: 79} + {team: "ogres", rank: 82, sessionID: "537b5662fe0ec03905fcdcd4", name: "pages", playtime: 22679, wins: 231, losses: 155, score: 76} + {team: "ogres", rank: 83, sessionID: "537bf90295356a43065f7640", name: "", playtime: 167, wins: 218, losses: 144, score: 74} + {team: "ogres", rank: 84, sessionID: "535de130c969efa7053345ab", name: "Racksickle", playtime: 319, wins: 231, losses: 157, score: 74} + {team: "ogres", rank: 85, sessionID: "537c2facf67b6b1c08af376f", name: "greyworm", playtime: 22755, wins: 234, losses: 160, score: 74} + {team: "ogres", rank: 86, sessionID: "537eef8c8fe881f1055dddb6", name: "huang123", playtime: 2590, wins: 234, losses: 161, score: 73} + {team: "ogres", rank: 87, sessionID: "53934430304ab93805efcf4f", name: "LilDooda", playtime: 16603, wins: 229, losses: 156, score: 73} + {team: "ogres", rank: 88, sessionID: "5389c83fb924da39051485d2", name: "nineties", playtime: 58607, wins: 183, losses: 113, score: 70} + {team: "ogres", rank: 89, sessionID: "537b760f1ed81a3b05c4fbfe", name: "Xavion", playtime: 3593, wins: 229, losses: 159, score: 70} + {team: "ogres", rank: 90, sessionID: "5383ec74087dc139054d8658", name: "cattycat", playtime: 384, wins: 228, losses: 160, score: 68} + {team: "ogres", rank: 91, sessionID: "538375bb78171d3d057ebdb6", name: "Johnwg7", playtime: 65297, wins: 233, losses: 165, score: 68} + {team: "ogres", rank: 92, sessionID: "537b99063803a287057582c1", name: "Simba", playtime: 16847, wins: 230, losses: 163, score: 67} + {team: "ogres", rank: 93, sessionID: "53929b482446a4410538b59d", name: "Buh", playtime: 28766, wins: 230, losses: 165, score: 65} + {team: "ogres", rank: 94, sessionID: "537bf9410ff88b2c06286e43", name: "Ulfberht", playtime: 26046, wins: 232, losses: 171, score: 61} + {team: "ogres", rank: 95, sessionID: "537bc6e24add491607deaaa1", name: "Victor Hugo", playtime: 31094, wins: 221, losses: 163, score: 58} + {team: "ogres", rank: 96, sessionID: "53809a807585483905a79529", name: "stderr", playtime: 20568, wins: 229, losses: 173, score: 56} + {team: "ogres", rank: 97, sessionID: "537b8643e91b4d3b05525f83", name: "Austinh100", playtime: 12405, wins: 225, losses: 176, score: 49} + {team: "ogres", rank: 98, sessionID: "537c7fad26529de30cca440d", name: "efraglebagga", playtime: 22417, wins: 219, losses: 172, score: 47} + {team: "ogres", rank: 99, sessionID: "537dde5d933d99860613a256", name: "Meojifo", playtime: 50047, wins: 222, losses: 177, score: 45} + {team: "ogres", rank: 100, sessionID: "537bc0eb467df184064bb865", name: "Diginaut", playtime: 8029, wins: 225, losses: 180, score: 45} + {team: "ogres", rank: 101, sessionID: "538390336f3bea3a05faff49", name: "Ghostly_Cookie", playtime: 5804, wins: 221, losses: 177, score: 44} + {team: "ogres", rank: 102, sessionID: "537f2cb8fdacc2fa050231bf", name: "crazydiv", playtime: 12170, wins: 212, losses: 170, score: 42} + {team: "ogres", rank: 103, sessionID: "537ef0e78fe881f1055dde46", name: "Æ", playtime: 97439, wins: 204, losses: 163, score: 41} + {team: "ogres", rank: 104, sessionID: "537b96f216613c84051557f1", name: "mda", playtime: 11544, wins: 222, losses: 181, score: 41} + {team: "ogres", rank: 105, sessionID: "537b8231e91b4d3b05525e7e", name: "BobFranz", playtime: 632, wins: 222, losses: 184, score: 38} + {team: "ogres", rank: 106, sessionID: "537b9a1d3803a2870575835f", name: "pvande", playtime: 39177, wins: 216, losses: 179, score: 37} + {team: "ogres", rank: 107, sessionID: "537c23fffa0d80a106b10eb4", name: "Nylan", playtime: 1236, wins: 204, losses: 174, score: 30} + {team: "ogres", rank: 108, sessionID: "537b91795bc02238050d1b80", name: "Orange!", playtime: 22940, wins: 210, losses: 181, score: 29} + {team: "ogres", rank: 109, sessionID: "537b8cfde91b4d3b055261b6", name: "Thumb", playtime: 22271, wins: 212, losses: 185, score: 27} + {team: "ogres", rank: 110, sessionID: "53978487888ab73905184003", name: "Nick2", playtime: 5539, wins: 197, losses: 173, score: 24} + {team: "ogres", rank: 111, sessionID: "5386313f8ca8b1120b8b58a3", name: "fudgy", playtime: 2744, wins: 213, losses: 193, score: 20} + {team: "ogres", rank: 112, sessionID: "537cbcc062dea6b811f109c7", name: "Kirstin", playtime: 23151, wins: 211, losses: 192, score: 19} + {team: "ogres", rank: 113, sessionID: "53814d4605e52a3305df086c", name: "jsut", playtime: 21961, wins: 208, losses: 189, score: 19} + {team: "ogres", rank: 114, sessionID: "5384c51f7d99a7390560bce1", name: "Xhuy", playtime: 4562, wins: 204, losses: 188, score: 16} + {team: "ogres", rank: 115, sessionID: "536259c5fc1acf300b2ebcb7", name: "DarthNato", playtime: 26611, wins: 206, losses: 193, score: 13} + {team: "ogres", rank: 116, sessionID: "538db7f65572d43b0521cf9c", name: "rehashed", playtime: 12008, wins: 201, losses: 188, score: 13} + {team: "ogres", rank: 117, sessionID: "5361946c801ef0010d3a3369", name: "DeathScythe", playtime: 0, wins: 209, losses: 197, score: 12} + {team: "ogres", rank: 118, sessionID: "537f5d073282d7a5076952e6", name: "kraxor", playtime: 14420, wins: 191, losses: 179, score: 12} + {team: "ogres", rank: 119, sessionID: "537bc98ba968548907de6dad", name: "Huragok", playtime: 325, wins: 207, losses: 197, score: 10} + {team: "ogres", rank: 120, sessionID: "53718545d0d9b1370555448b", name: "SuchNoob", playtime: 0, wins: 196, losses: 188, score: 8} + {team: "ogres", rank: 121, sessionID: "538a1888b924da390514c2d6", name: "Lomidrevo", playtime: 9470, wins: 207, losses: 199, score: 8} + {team: "ogres", rank: 122, sessionID: "537b92265bc02238050d1bc3", name: "Drew", playtime: 21829, wins: 199, losses: 192, score: 7} + {team: "ogres", rank: 123, sessionID: "537b976e556db17605be769e", name: "Drak", playtime: 9554, wins: 202, losses: 197, score: 5} + {team: "ogres", rank: 124, sessionID: "5380b07205e52a3305deb2ea", name: "LinaLin", playtime: 1852, wins: 204, losses: 199, score: 5} + {team: "ogres", rank: 125, sessionID: "537b84de5bc02238050d1699", name: "WoLfulus", playtime: 9762, wins: 204, losses: 200, score: 4} + {team: "ogres", rank: 126, sessionID: "537b7f945bc02238050d14d8", name: "Shack", playtime: 16108, wins: 199, losses: 197, score: 2} + {team: "ogres", rank: 127, sessionID: "537badf8ec57a3e805cbf577", name: "borreltijd", playtime: 29749, wins: 202, losses: 203, score: -1} + {team: "ogres", rank: 128, sessionID: "537f8306ead05fd3075d1d39", name: "icoderz", playtime: 5382, wins: 202, losses: 204, score: -2} + {team: "ogres", rank: 129, sessionID: "537b92b55bc02238050d1c18", name: "Bullcity", playtime: 7478, wins: 197, losses: 204, score: -7} + {team: "ogres", rank: 130, sessionID: "537ba0937c89ec9805f4df2f", name: "Pavlov", playtime: 15761, wins: 198, losses: 205, score: -7} + {team: "ogres", rank: 131, sessionID: "537b85055bc02238050d16a6", name: "Drunk McLovin", playtime: 10225, wins: 194, losses: 204, score: -10} + {team: "ogres", rank: 132, sessionID: "537cbb38a0d7a5cc10992ee9", name: "Aion Crane", playtime: 9616, wins: 195, losses: 206, score: -11} + {team: "ogres", rank: 133, sessionID: "537be31fa3b60b390500ddc5", name: "antkim003", playtime: 19059, wins: 195, losses: 206, score: -11} + {team: "ogres", rank: 134, sessionID: "537d20d97e1e10b705bbcd58", name: "Tain", playtime: 28374, wins: 173, losses: 189, score: -16} + {team: "ogres", rank: 135, sessionID: "537bb6d6386abb6e05a3facf", name: "RTN", playtime: 4108, wins: 194, losses: 211, score: -17} + {team: "ogres", rank: 136, sessionID: "537b86b95bc02238050d1706", name: "Luogbelnu", playtime: 24526, wins: 191, losses: 208, score: -17} + {team: "ogres", rank: 137, sessionID: "537338f73108bc3905f7528d", name: "chess", playtime: 321, wins: 192, losses: 213, score: -21} + {team: "ogres", rank: 138, sessionID: "537df0950efa6a37059df463", name: "Rury", playtime: 14371, wins: 187, losses: 215, score: -28} + {team: "ogres", rank: 139, sessionID: "537c11290ff88b2c06287d29", name: "Crustopher", playtime: 15401, wins: 167, losses: 195, score: -28} + {team: "ogres", rank: 140, sessionID: "537df96a0efa6a37059df853", name: "HuangSY", playtime: 34229, wins: 178, losses: 209, score: -31} + {team: "ogres", rank: 141, sessionID: "537b909f5bc02238050d1b32", name: "ArthurDent", playtime: 63903, wins: 173, losses: 204, score: -31} + {team: "ogres", rank: 142, sessionID: "537e901b9008f87b0541fc83", name: "Mawox", playtime: 15961, wins: 186, losses: 218, score: -32} + {team: "ogres", rank: 143, sessionID: "537fa33bf56c5874086896cc", name: "Urg", playtime: 1760, wins: 182, losses: 218, score: -36} + {team: "ogres", rank: 144, sessionID: "53854e7d7d99a7390560fadd", name: "Dodrithard", playtime: 4087, wins: 160, losses: 196, score: -36} + {team: "ogres", rank: 145, sessionID: "537bfa3395356a43065f76bd", name: "Cicir", playtime: 7369, wins: 183, losses: 221, score: -38} + {team: "ogres", rank: 146, sessionID: "537ba4f3d60bb0b405d852b2", name: "Dubastot", playtime: 17312, wins: 160, losses: 201, score: -41} + {team: "ogres", rank: 147, sessionID: "537b8dbbe91b4d3b05526226", name: "Raetsel", playtime: 11005, wins: 180, losses: 223, score: -43} + {team: "ogres", rank: 148, sessionID: "537bc0b31c8dfa820699fcc2", name: "Source Error", playtime: 1731, wins: 171, losses: 214, score: -43} + {team: "ogres", rank: 149, sessionID: "537b92dde91b4d3b05526442", name: "Script Pimpkin", playtime: 18711, wins: 180, losses: 225, score: -45} + {team: "ogres", rank: 150, sessionID: "538f33d0755876370525988f", name: "Rigamortis", playtime: 23232, wins: 176, losses: 221, score: -45} + {team: "ogres", rank: 151, sessionID: "537c8bb826529de30cca4b02", name: "Static", playtime: 673, wins: 167, losses: 216, score: -49} + {team: "ogres", rank: 152, sessionID: "537bdd5ed3f3bb3305bd997a", name: "Tuna", playtime: 4854, wins: 177, losses: 226, score: -49} + {team: "ogres", rank: 153, sessionID: "537b945d556db17605be74d0", name: "Dino", playtime: 5413, wins: 177, losses: 227, score: -50} + {team: "ogres", rank: 154, sessionID: "537bcda2c57303a5070c2f3a", name: "LaughingMan", playtime: 52476, wins: 170, losses: 222, score: -52} + {team: "ogres", rank: 155, sessionID: "53825ebf78171d3d057e4e48", name: "Lord Azrael", playtime: 25968, wins: 175, losses: 227, score: -52} + {team: "ogres", rank: 156, sessionID: "537d19307e1e10b705bbc877", name: "Naoki", playtime: 7851, wins: 172, losses: 226, score: -54} + {team: "ogres", rank: 157, sessionID: "537b95526c13497e05de81d2", name: "Gromliqk", playtime: 15566, wins: 174, losses: 230, score: -56} + {team: "ogres", rank: 158, sessionID: "537bacbc03838cf0050ace6f", name: "cameltoe", playtime: 53128, wins: 171, losses: 230, score: -59} + {team: "ogres", rank: 159, sessionID: "537b82d75bc02238050d160d", name: "Kithid", playtime: 14756, wins: 169, losses: 229, score: -60} + {team: "ogres", rank: 160, sessionID: "537d4e5d3dcf67c40571f5c3", name: "bonnnie", playtime: 25945, wins: 173, losses: 233, score: -60} + {team: "ogres", rank: 161, sessionID: "537e4d7a63734c6305059374", name: "kotowlos", playtime: 6498, wins: 172, losses: 233, score: -61} + {team: "ogres", rank: 162, sessionID: "537b80425bc02238050d152c", name: "mdz", playtime: 12720, wins: 167, losses: 229, score: -62} + {team: "ogres", rank: 163, sessionID: "537bb50218bd7f4705b9d459", name: "Claros", playtime: 1250, wins: 170, losses: 236, score: -66} + {team: "ogres", rank: 164, sessionID: "537c8a5626529de30cca4a2f", name: "nulo", playtime: 5264, wins: 168, losses: 236, score: -68} + {team: "ogres", rank: 165, sessionID: "537bb4598698e13805226b77", name: "JojoTheGreat", playtime: 1733, wins: 162, losses: 235, score: -73} + {team: "ogres", rank: 166, sessionID: "537bab9b89a8aeea05970d0b", name: "gameloop.io", playtime: 2823, wins: 163, losses: 237, score: -74} + {team: "ogres", rank: 167, sessionID: "5382411905e52a3305df893b", name: "Richard Adleta", playtime: 3272, wins: 162, losses: 240, score: -78} + {team: "ogres", rank: 168, sessionID: "537c4408a56c78cb0cb4f353", name: "lichens", playtime: 59782, wins: 163, losses: 241, score: -78} + {team: "ogres", rank: 169, sessionID: "537e1ab60efa6a37059e0bf0", name: "Scnoobi", playtime: 3430, wins: 159, losses: 239, score: -80} + {team: "ogres", rank: 170, sessionID: "537f6adc14f08dec07950ab2", name: "Ralphy282", playtime: 42208, wins: 155, losses: 235, score: -80} + {team: "ogres", rank: 171, sessionID: "537be8d820a501380564e6e7", name: "gandalfStormcrow", playtime: 435, wins: 162, losses: 242, score: -80} + {team: "ogres", rank: 172, sessionID: "537bc4b10de0a02c07e879a8", name: "", playtime: 872, wins: 161, losses: 242, score: -81} + {team: "ogres", rank: 173, sessionID: "537e581863734c6305059a53", name: "taz", playtime: 24420, wins: 160, losses: 242, score: -82} + {team: "ogres", rank: 174, sessionID: "537ce331e9c4e97c05e3ea34", name: "bb", playtime: 391, wins: 159, losses: 245, score: -86} + {team: "ogres", rank: 175, sessionID: "537c5f4f14aaabe80c69fd70", name: "@ESWAT", playtime: 13389, wins: 152, losses: 239, score: -87} + {team: "ogres", rank: 176, sessionID: "537b8a515bc02238050d1873", name: "joshuacarley", playtime: 23663, wins: 156, losses: 244, score: -88} + {team: "ogres", rank: 177, sessionID: "537d550d3dcf67c40571f7dc", name: "J'Son", playtime: 5290, wins: 156, losses: 246, score: -90} + {team: "ogres", rank: 178, sessionID: "537c05ef95356a43065f7e1c", name: "Cerbi", playtime: 9095, wins: 151, losses: 250, score: -99} + {team: "ogres", rank: 179, sessionID: "537c532126529de30cca3067", name: "warrned", playtime: 3870, wins: 149, losses: 254, score: -105} + {team: "ogres", rank: 180, sessionID: "537fc780c1a6b99109171791", name: "OWalerys", playtime: 18624, wins: 146, losses: 251, score: -105} + {team: "ogres", rank: 181, sessionID: "537efa7e8fe881f1055de25e", name: "Smaug", playtime: 14526, wins: 146, losses: 253, score: -107} + {team: "ogres", rank: 182, sessionID: "537bf37d95356a43065f7286", name: "cokaroach", playtime: 47117, wins: 124, losses: 237, score: -113} + {team: "ogres", rank: 183, sessionID: "537e304e64659f3a0577248a", name: "Plak87", playtime: 5459, wins: 144, losses: 259, score: -115} + {team: "ogres", rank: 184, sessionID: "537cfe4d9cce053a05c04dc2", name: "xzores", playtime: 9686, wins: 145, losses: 260, score: -115} + {team: "ogres", rank: 185, sessionID: "537bb0ad2323353a05bd57ef", name: "Wicked", playtime: 4520, wins: 140, losses: 258, score: -118} + {team: "ogres", rank: 186, sessionID: "537cd1c4665bde1b13ffd635", name: "Delforas", playtime: 3970, wins: 138, losses: 258, score: -120} + {team: "ogres", rank: 187, sessionID: "53806a8405e52a3305de92fa", name: "Pabelo", playtime: 4629, wins: 141, losses: 262, score: -121} + {team: "ogres", rank: 188, sessionID: "537fcc57c1a6b99109171ced", name: "jhoie", playtime: 1045, wins: 137, losses: 259, score: -122} + {team: "ogres", rank: 189, sessionID: "5395d1ba7362c439054f7e6e", name: "JenJen", playtime: 151, wins: 137, losses: 259, score: -122} + {team: "ogres", rank: 190, sessionID: "537b9dc4ffeaa29e051b2738", name: "Kevin", playtime: 9970, wins: 136, losses: 263, score: -127} + {team: "ogres", rank: 191, sessionID: "537b9c09ffeaa29e051b2637", name: "Tuefekci", playtime: 10790, wins: 131, losses: 259, score: -128} + {team: "ogres", rank: 192, sessionID: "537c5b3526529de30cca340c", name: "patchnotes", playtime: 31167, wins: 119, losses: 252, score: -133} + {team: "ogres", rank: 193, sessionID: "5380fdb47585483905a7c753", name: "null0pointer", playtime: 9524, wins: 131, losses: 266, score: -135} + {team: "ogres", rank: 194, sessionID: "537b9f4751e98aa705b60515", name: "Sawyer", playtime: 9450, wins: 136, losses: 271, score: -135} + {team: "ogres", rank: 195, sessionID: "5380bd747585483905a7a707", name: "Lobo", playtime: 5280, wins: 132, losses: 269, score: -137} + {team: "ogres", rank: 196, sessionID: "5382593bc85c223a05f429d9", name: "squidlarkin", playtime: 2036, wins: 130, losses: 267, score: -137} + {team: "ogres", rank: 197, sessionID: "537cd5fee4523d0113ba57ea", name: "Borlak", playtime: 14470, wins: 133, losses: 271, score: -138} + {team: "ogres", rank: 198, sessionID: "537bbb201aaa69c4052676fd", name: "jMerliN", playtime: 3292, wins: 129, losses: 270, score: -141} + {team: "ogres", rank: 199, sessionID: "537be3c7a3b60b390500de0e", name: "Dragon DM", playtime: 17848, wins: 131, losses: 274, score: -143} + {team: "ogres", rank: 200, sessionID: "537b90295bc02238050d1af5", name: "octopushugs", playtime: 706, wins: 125, losses: 270, score: -145} + {team: "ogres", rank: 201, sessionID: "5380d5be7585483905a7b1e1", name: "Quaritch", playtime: 12711, wins: 128, losses: 274, score: -146} + {team: "ogres", rank: 202, sessionID: "5397732542cf7d3905ed7233", name: "BloodJohn", playtime: 5178, wins: 127, losses: 276, score: -149} + {team: "ogres", rank: 203, sessionID: "537cab3d0e905d000fbfad57", name: "Xor", playtime: 19480, wins: 126, losses: 278, score: -152} + {team: "ogres", rank: 204, sessionID: "537b88f85bc02238050d17e2", name: "Jojas", playtime: 9328, wins: 126, losses: 278, score: -152} + {team: "ogres", rank: 205, sessionID: "539436745009fe5b075b70a7", name: "Vhr", playtime: 3065, wins: 122, losses: 274, score: -152} + {team: "ogres", rank: 206, sessionID: "535f0c4b11c0ebd2049d1664", name: "Jebso", playtime: 0, wins: 125, losses: 278, score: -153} + {team: "ogres", rank: 207, sessionID: "537b9d3cffeaa29e051b26eb", name: "", playtime: 784, wins: 125, losses: 278, score: -153} + {team: "ogres", rank: 208, sessionID: "537c3bdb54bc23770a21e166", name: "seaclair", playtime: 385, wins: 122, losses: 279, score: -157} + {team: "ogres", rank: 209, sessionID: "537bf147f8b46fbd0544fc15", name: "", playtime: 16376, wins: 109, losses: 269, score: -160} + {team: "ogres", rank: 210, sessionID: "537bd2e0be90aa5605fb2ebd", name: "educavalcanti", playtime: 50086, wins: 121, losses: 282, score: -161} + {team: "ogres", rank: 211, sessionID: "537cd0e0665bde1b13ffd5ca", name: "itsbth", playtime: 1299, wins: 104, losses: 265, score: -161} + {team: "ogres", rank: 212, sessionID: "537b8e1ee91b4d3b05526274", name: "Jorge", playtime: 4728, wins: 118, losses: 280, score: -162} + {team: "ogres", rank: 213, sessionID: "537b88f6e91b4d3b05526048", name: "CzonI", playtime: 3033, wins: 120, losses: 283, score: -163} + {team: "ogres", rank: 214, sessionID: "537b7c26e91b4d3b05525c6a", name: "TTSS", playtime: 1132, wins: 120, losses: 283, score: -163} + {team: "ogres", rank: 215, sessionID: "5389d73bb924da3905149015", name: "oli-f", playtime: 10824, wins: 101, losses: 266, score: -165} + {team: "ogres", rank: 216, sessionID: "537b8c21e91b4d3b0552616d", name: "Luke Young", playtime: 10713, wins: 117, losses: 285, score: -168} + {team: "ogres", rank: 217, sessionID: "537b84e15bc02238050d169c", name: "", playtime: 359, wins: 116, losses: 284, score: -168} + {team: "ogres", rank: 218, sessionID: "537b845fe91b4d3b05525f03", name: "walesmd", playtime: 1630, wins: 116, losses: 285, score: -169} + {team: "ogres", rank: 219, sessionID: "538515a73e0daa390518ba45", name: "Azerroth", playtime: 19862, wins: 114, losses: 285, score: -171} + {team: "ogres", rank: 220, sessionID: "537bd660142ee637052420e3", name: "tkl", playtime: 8276, wins: 102, losses: 278, score: -176} + {team: "ogres", rank: 221, sessionID: "538a9449b924da3905151926", name: "cdaaar", playtime: 14955, wins: 45, losses: 222, score: -177} + {team: "ogres", rank: 222, sessionID: "538345b078171d3d057eae61", name: "Ikrus", playtime: 13687, wins: 111, losses: 295, score: -184} + {team: "ogres", rank: 223, sessionID: "537b8e26e91b4d3b05526277", name: "Zairja", playtime: 10077, wins: 101, losses: 293, score: -192} + {team: "ogres", rank: 224, sessionID: "537b97f53803a28705758218", name: "kookieblues", playtime: 2400, wins: 104, losses: 303, score: -199} + {team: "ogres", rank: 225, sessionID: "537d06bd7e1e10b705bbbe83", name: "llcossette", playtime: 2648, wins: 95, losses: 302, score: -207} + {team: "ogres", rank: 226, sessionID: "537b800de91b4d3b05525db8", name: "Merlin Gough", playtime: 8942, wins: 90, losses: 303, score: -213} + {team: "ogres", rank: 227, sessionID: "538382db86de19c31c1ce893", name: "Nick05 Testinator", playtime: 1454, wins: 90, losses: 311, score: -221} + {team: "ogres", rank: 228, sessionID: "537bcd7ec57303a5070c2f1e", name: "hedint", playtime: 8881, wins: 92, losses: 314, score: -222} + {team: "ogres", rank: 229, sessionID: "537cc425665bde1b13ffce89", name: "dotnetjohn", playtime: 1304, wins: 87, losses: 315, score: -228} + {team: "ogres", rank: 230, sessionID: "537c3c1454bc23770a21e187", name: "Prelude", playtime: 2351, wins: 85, losses: 313, score: -228} + {team: "ogres", rank: 231, sessionID: "538925b4d06c503805fe4ff7", name: "Perenolder", playtime: 21235, wins: 79, losses: 322, score: -243} + {team: "ogres", rank: 232, sessionID: "538a26cdd06c503805fec706", name: "the-k-man", playtime: 10321, wins: 78, losses: 327, score: -249} + {team: "ogres", rank: 233, sessionID: "537c531d26529de30cca3065", name: "masashi", playtime: 17871, wins: 75, losses: 330, score: -255} + {team: "ogres", rank: 234, sessionID: "5371861fd4491339051c3b11", name: "asdlk;fj asd fas", playtime: 0, wins: 65, losses: 333, score: -268} + {team: "ogres", rank: 235, sessionID: "537b8429e91b4d3b05525ef5", name: "Missblit", playtime: 28639, wins: 59, losses: 346, score: -287} + {team: "ogres", rank: 236, sessionID: "538b1322d06c503805ff299f", name: "judar1o", playtime: 5891, wins: 59, losses: 347, score: -288} + {team: "ogres", rank: 237, sessionID: "53583ab57962157d05b398f8", name: "makertech81", playtime: 170, wins: 55, losses: 344, score: -289} + {team: "ogres", rank: 238, sessionID: "539723388376eb3805dc2d4e", name: "immersion", playtime: 6471, wins: 54, losses: 348, score: -294} + {team: "ogres", rank: 239, sessionID: "537c99aa14aaabe80c6a1a44", name: "werewolf", playtime: 167, wins: 46, losses: 343, score: -297} + {team: "ogres", rank: 240, sessionID: "539085aaf369103805172236", name: "Kabbi", playtime: 6873, wins: 43, losses: 344, score: -301} + {team: "ogres", rank: 241, sessionID: "537bd587b0d1766d05243016", name: "Casey", playtime: 1229, wins: 44, losses: 346, score: -302} + {team: "ogres", rank: 242, sessionID: "537e2bd864659f3a05772075", name: "", playtime: 322, wins: 42, losses: 349, score: -307} + {team: "ogres", rank: 243, sessionID: "5385c7503e0daa39051919db", name: "Zack", playtime: 66, wins: 40, losses: 348, score: -308} + {team: "ogres", rank: 244, sessionID: "537ba0a551e98aa705b605b2", name: "", playtime: 232, wins: 39, losses: 349, score: -310} + {team: "ogres", rank: 245, sessionID: "535fdebe5c7324a80702cb3b", name: "mich55", playtime: 0, wins: 38, losses: 349, score: -311} + {team: "ogres", rank: 246, sessionID: "5390823e7558763705263cc8", name: "Duchess", playtime: 460, wins: 37, losses: 349, score: -312} + {team: "ogres", rank: 247, sessionID: "5360a43d9859fefb0673aefa", name: "daruba", playtime: 0, wins: 34, losses: 351, score: -317} + {team: "ogres", rank: 248, sessionID: "537bab4f6e54cbda057ee565", name: "Pajamas", playtime: 27043, wins: 14, losses: 338, score: -324} + {team: "ogres", rank: 249, sessionID: "537bf053f8b46fbd0544fb8c", name: "Fusspawn", playtime: 9524, wins: 37, losses: 368, score: -331} + {team: "ogres", rank: 250, sessionID: "5382f069c85c223a05f47200", name: "ferrak", playtime: 7015, wins: 33, losses: 373, score: -340} + {team: "ogres", rank: 251, sessionID: "5380a3737585483905a799e1", name: "Dark Pikachu", playtime: 1373, wins: 29, losses: 377, score: -348} + {team: "ogres", rank: 252, sessionID: "538711898ca8b1120b8bdb3f", name: "A", playtime: 4272, wins: 24, losses: 380, score: -356} + {team: "ogres", rank: 253, sessionID: "5384af823e0daa3905187cb1", name: "Macguffin", playtime: 837, wins: 18, losses: 384, score: -366} + {team: "ogres", rank: 254, sessionID: "537db76b13add33a051bf575", name: "Luyalve", playtime: 1404, wins: 3, losses: 372, score: -369} + {team: "ogres", rank: 255, sessionID: "537b9bac16613c8405155a79", name: "solarian", playtime: 24889, wins: 0, losses: 371, score: -371} + {team: "ogres", rank: 256, sessionID: "537ba09951e98aa705b605ae", name: "Potato", playtime: 61777, wins: 0, losses: 371, score: -371} + {team: "ogres", rank: 257, sessionID: "537d24db84c54c6e05c06ac4", name: "terriblesarcasm", playtime: 39553, wins: 0, losses: 371, score: -371} + {team: "ogres", rank: 258, sessionID: "537c2abc224747bb0761634f", name: "chicones", playtime: 9831, wins: 0, losses: 371, score: -371} + {team: "ogres", rank: 259, sessionID: "538381a5c83e563705a06fdb", name: "diddly-dum", playtime: 8077, wins: 0, losses: 372, score: -372} + {team: "ogres", rank: 260, sessionID: "5388a785b924da3905139f46", name: "AWESOMEHACKER", playtime: 564, wins: 0, losses: 372, score: -372} + {team: "ogres", rank: 261, sessionID: "537bb6d2386abb6e05a3facc", name: "chrm", playtime: 213307, wins: 0, losses: 375, score: -375} + {team: "ogres", rank: 262, sessionID: "537bc26e8fe668b406d4c019", name: "Sapid", playtime: 11917, wins: 0, losses: 376, score: -376} + {team: "ogres", rank: 263, sessionID: "5357127a1bfa9bba14b5e048", name: "dcm", playtime: 5380, wins: 0, losses: 376, score: -376} + {team: "ogres", rank: 264, sessionID: "537aee4cfb4c173805cf26f7", name: "basicer", playtime: 10407, wins: 0, losses: 376, score: -376} + {team: "ogres", rank: 265, sessionID: "5382cbb078171d3d057e804f", name: "vvv", playtime: 719, wins: 0, losses: 376, score: -376} + {team: "ogres", rank: 266, sessionID: "537ba40a903fd2b8051553b0", name: "ElderTale", playtime: 13984, wins: 0, losses: 376, score: -376} + {team: "ogres", rank: 267, sessionID: "53839e256f3bea3a05fb09b3", name: "Gondolfs", playtime: 29985, wins: 0, losses: 378, score: -378} + {team: "ogres", rank: 268, sessionID: "537a73a6e488a7380545affd", name: "Galvan", playtime: 514, wins: 0, losses: 378, score: -378} + {team: "ogres", rank: 269, sessionID: "537b96b16c13497e05de82a8", name: "Krris", playtime: 2618, wins: 0, losses: 379, score: -379} + {team: "ogres", rank: 270, sessionID: "537bc1b81db8c8ac063a3b7b", name: "jpiasetz", playtime: 14565, wins: 0, losses: 380, score: -380} +] diff --git a/app/views/play/level/modal/editor_config_modal.coffee b/app/views/play/level/modal/editor_config_modal.coffee index a90afad08..ff72ada2b 100644 --- a/app/views/play/level/modal/editor_config_modal.coffee +++ b/app/views/play/level/modal/editor_config_modal.coffee @@ -79,7 +79,7 @@ module.exports = class EditorConfigModal extends View Backbone.Mediator.publish 'tome:change-config' Backbone.Mediator.publish 'tome:change-language', language: newLanguage unless newLanguage is oldLanguage @session.save() unless newLanguage is oldLanguage - me.save() + me.patch() destroy: -> super() diff --git a/app/views/play/level/modal/victory_modal.coffee b/app/views/play/level/modal/victory_modal.coffee index ecc883426..954ab63ef 100644 --- a/app/views/play/level/modal/victory_modal.coffee +++ b/app/views/play/level/modal/victory_modal.coffee @@ -81,7 +81,7 @@ module.exports = class VictoryModal extends View if enough and not me.get('hourOfCodeComplete') $('body').append($("")) me.set 'hourOfCodeComplete', true - me.save() + me.patch() window.tracker?.trackEvent 'Hour of Code Finish', {} # Show the "I'm done" button if they get to the end, unless it's been over two hours tooMuch = elapsed >= 120 * 60 * 1000 diff --git a/app/views/play/level/playback_view.coffee b/app/views/play/level/playback_view.coffee index 9d377eb52..d3f66d89d 100644 --- a/app/views/play/level/playback_view.coffee +++ b/app/views/play/level/playback_view.coffee @@ -355,7 +355,7 @@ module.exports = class PlaybackView extends View onToggleMusic: (e) -> e?.preventDefault() me.set('music', not me.get('music')) - me.save() + me.patch() $(document.activeElement).blur() destroy: -> diff --git a/app/views/play/level/tome/cast_button_view.coffee b/app/views/play/level/tome/cast_button_view.coffee index 133cfbfcc..5f368a51c 100644 --- a/app/views/play/level/tome/cast_button_view.coffee +++ b/app/views/play/level/tome/cast_button_view.coffee @@ -97,7 +97,7 @@ module.exports = class CastButtonView extends View return unless delay @autocastDelay = delay = parseInt delay me.set('autocastDelay', delay) - me.save() + me.patch() spell.view.setAutocastDelay delay for spellKey, spell of @spells @castOptions.find('a').each -> $(@).toggleClass('selected', parseInt($(@).attr('data-delay')) is delay) diff --git a/app/views/play/level/tome/spell.coffee b/app/views/play/level/tome/spell.coffee index a44dadde4..a3f0de4ce 100644 --- a/app/views/play/level/tome/spell.coffee +++ b/app/views/play/level/tome/spell.coffee @@ -24,7 +24,7 @@ module.exports = class Spell @permissions = read: p.permissions?.read ? [], readwrite: p.permissions?.readwrite ? [] # teams teamSpells = @session.get('teamSpells') team = @session.get('team') ? 'humans' - @useTranspiledCode = @permissions.readwrite.length and ((teamSpells and not _.contains(teamSpells[team], @spellKey)) or (@session.get('creator') isnt me.id) or @spectateView) + @useTranspiledCode = @permissions.readwrite.length and ((teamSpells and not _.contains(teamSpells[team], @spellKey)) or (@session.get('creator') isnt me.id and not (me.isAdmin() or 'employer' in me.get('permissions'))) or @spectateView) #console.log @spellKey, "using transpiled code?", @useTranspiledCode @source = @originalSource = p.source @parameters = p.parameters diff --git a/app/views/play/level_view.coffee b/app/views/play/level_view.coffee index 392198638..411944390 100644 --- a/app/views/play/level_view.coffee +++ b/app/views/play/level_view.coffee @@ -93,7 +93,7 @@ module.exports = class PlayLevelView extends View setUpHourOfCode: -> me.set 'hourOfCode', true - me.save() + me.patch() $('body').append($("")) application.tracker?.trackEvent 'Hour of Code Begin', {} diff --git a/app/views/play_view.coffee b/app/views/play_view.coffee index 8662ecef4..6997dc83c 100644 --- a/app/views/play_view.coffee +++ b/app/views/play_view.coffee @@ -211,14 +211,21 @@ module.exports = class PlayView extends View difficulty: 3 id: 'bubble-sort-bootcamp-battle' image: '/file/db/level/525ef8ef06e1ab0962000003/commanding_followers_icon.png' - description: "Write a bubble sort to organize your soldiers. - by Alexandru" + description: "Write a bubble sort to organize your soldiers. - by Alexandru Caciulescu" } { name: 'Ogres of Hanoi' difficulty: 3 id: 'ogres-of-hanoi' image: '/file/db/level/526fd3043c637ece50001bb2/the_herd_icon.png' - description: "Transfer a stack of ogres while preserving their honor. - by Alexandru" + description: "Transfer a stack of ogres while preserving their honor. - by Alexandru Caciulescu" + } + { + name: 'Danger! Minefield' + difficulty: 3 + id: 'danger-minefield' + image: '/file/db/level/526bda3fe79aefde2a003e36/mobile_artillery_icon.png' + description: "Learn how to find prime numbers while defusing mines! - by Alexandru Caciulescu" } { name: 'Find the Spy' @@ -234,15 +241,6 @@ module.exports = class PlayView extends View image: '/file/db/level/529662dfe0df8f0000000007/grab_the_mushroom_icon.png' description: "Collect a hundred mushrooms in just five lines of code - by Nathan Gossett" } - - #{ - # name: 'Enemy Artillery' - # difficulty: 1 - # id: 'enemy-artillery' - # image: '/file/db/level/526dba94a188322044000a40/mobile_artillery_icon.png' - # description: "Take cover while shells fly, then strike! - by mcdavid1991" - # disabled: true - #} ] context.campaigns = [ diff --git a/app/views/test.coffee b/app/views/test.coffee index 14ef63311..f4667fc44 100644 --- a/app/views/test.coffee +++ b/app/views/test.coffee @@ -107,6 +107,7 @@ module.exports = TestView = class TestView extends CocoView # TODO Stubbify more things # * document.location # * firebase + # * all the services that load in main.html afterEach -> # TODO Clean up more things diff --git a/bin/coco-mongodb b/bin/coco-mongodb index 55b60e90e..df6e3b7ad 100755 --- a/bin/coco-mongodb +++ b/bin/coco-mongodb @@ -88,5 +88,8 @@ mongo_db_path = os.path.abspath(os.path.join(current_directory,os.pardir)) + os. if not os.path.exists(mongo_db_path): os.mkdir(mongo_db_path) mongo_arguments = [mongo_executable + u" --setParameter textSearchEnabled=true --dbpath=" + mongo_db_path] + +if 'fork' in sys.argv: mongo_arguments[0] += " --fork --logpath ./mongodb.log" + call(mongo_arguments,shell=True) diff --git a/config.coffee b/config.coffee index ddf5522b6..56adbd46d 100644 --- a/config.coffee +++ b/config.coffee @@ -3,14 +3,10 @@ startsWith = (string, substring) -> string.lastIndexOf(substring, 0) is 0 exports.config = - server: - path: 'server.coffee' paths: 'public': 'public' conventions: ignored: (path) -> startsWith(sysPath.basename(path), '_') - workers: - enabled: false # turned out to be much, much slower than without workers sourceMaps: true files: javascripts: @@ -55,7 +51,6 @@ exports.config = 'vendor/scripts/movieclip-NEXT.min.js' # Validated Backbone Mediator dependencies 'bower_components/tv4/tv4.js' - # Aether before box2d for some strange Object.defineProperty thing 'bower_components/aether/build/aether.js' 'bower_components/d3/d3.min.js' @@ -77,7 +72,7 @@ exports.config = plugins: autoReload: - delay: 300 # for race conditions, particularly waiting for onCompile to do its thing + delay: 300 coffeelint: pattern: /^app\/.*\.coffee$/ options: diff --git a/scripts/mail.coffee b/scripts/mail.coffee index f1b193fc8..7e5f53a50 100644 --- a/scripts/mail.coffee +++ b/scripts/mail.coffee @@ -7,9 +7,10 @@ async = require 'async' serverSetup = require '../server_setup' sendwithus = require '../server/sendwithus' -User = require '../server/users/User.coffee' -Level = require '../server/levels/Level.coffee' -LevelSession = require '../server/levels/sessions/LevelSession.coffee' +User = require '../server/users/User' +Level = require '../server/levels/Level' +LevelSession = require '../server/levels/sessions/LevelSession' +tournamentResults = require '../app/views/play/ladder/tournament_results' alreadyEmailed = [] @@ -27,7 +28,7 @@ sendInitialRecruitingEmail = -> async.waterfall [ (callback) -> async.map leaderboards, grabSessions, callback (sessionLists, callback) -> async.map collapseSessions(sessionLists), grabUser, callback - (users, callback) -> async.map users, emailUser, callback + (users, callback) -> async.map users, emailUserInitialRecruiting, callback ], (err, results) -> return console.log "Error:", err if err console.log "Looked at sending to #{results.length} users; sent to #{_.filter(results).length}." @@ -77,7 +78,7 @@ grabUser = (session, callback) -> callback null, user totalEmailsSent = 0 -emailUser = (user, callback) -> +emailUserInitialRecruiting = (user, callback) -> #return callback null, false if user.emails?.anyNotes?.enabled is false # TODO: later, uncomment to obey also "anyNotes" when that's untangled return callback null, false if user.emails?.recruitNotes?.enabled is false return callback null, false if user.email in alreadyEmailed @@ -102,5 +103,64 @@ emailUser = (user, callback) -> return callback err if err callback null, user + +sendTournamentResultsEmail = -> + winners = tournamentResults.greed.humans.concat tournamentResults.greed.ogres + async.waterfall [ + (callback) -> async.map winners, grabSession, callback + (winners, callback) -> async.map winners, grabEmail, callback + (winners, callback) -> async.map winners, emailUserTournamentResults, callback + ], (err, results) -> + return console.log "Error:", err if err + console.log "Looked at sending to #{results.length} users; sent to #{_.filter(results).length}." + console.log "Sent to: ['#{(user.email for user in results when user).join('\', \'')}']" + +grabSession = (winner, callback) -> + LevelSession.findOne(_id: winner.sessionID).select('creator').lean().exec (err, session) -> + return callback err if err + winner.userID = session.creator + callback null, winner + +grabEmail = (winner, callback) -> + User.findOne(_id: winner.userID).select('email').lean().exec (err, user) -> + return callback err if err + winner.email = user.email + callback null, winner + +emailUserTournamentResults = (winner, callback) -> + return callback null, false if DEBUGGING and (winner.team is 'humans' or totalEmailsSent > 1) + ++totalEmailsSent + name = winner.name + team = winner.team.substr(0, winner.team.length - 1) + context = + email_id: sendwithus.templates.greed_tournament_rank + recipient: + address: if DEBUGGING then 'nick@codecombat.com' else winner.email + name: name + email_data: + userID: winner.userID + name: name + level_name: "Greed" + wins: winner.wins + ties: {humans: 377, ogres: 407}[winner.team] - winner.wins - winner.losses + losses: winner.losses + rank: winner.rank + team_name: team + ladder_url: "http://codecombat.com/play/ladder/greed#winners" + top3: winner.rank <= 3 + top5: winner.rank <= 5 + top10: winner.rank <= 10 + top40: winner.rank <= 40 + top100: winner.rank <= 100 + sendwithus.api.send context, (err, result) -> + return callback err if err + callback null, winner + + serverSetup.connectToDatabase() -sendInitialRecruitingEmail() + +fn = process.argv[2] +try + eval fn + '()' +catch err + console.log "Error running #{fn}", err diff --git a/server/achievements/Achievement.coffee b/server/achievements/Achievement.coffee index faa625654..83562018e 100644 --- a/server/achievements/Achievement.coffee +++ b/server/achievements/Achievement.coffee @@ -1,7 +1,8 @@ mongoose = require('mongoose') -plugins = require('../plugins/plugins') jsonschema = require('../../app/schemas/models/achievement') log = require 'winston' +util = require '../../app/lib/utils' +plugins = require('../plugins/plugins') # `pre` and `post` are not called for update operations executed directly on the database, # including `Model.update`,`.findByIdAndUpdate`,`.findOneAndUpdate`, `.findOneAndRemove`,and `.findByIdAndRemove`.order @@ -12,16 +13,21 @@ AchievementSchema = new mongoose.Schema({ userField: String }, {strict: false}) -AchievementSchema.methods.objectifyQuery = () -> +AchievementSchema.methods.objectifyQuery = -> try @set('query', JSON.parse(@get('query'))) if typeof @get('query') == "string" catch error log.error "Couldn't convert query string to object because of #{error}" @set('query', {}) -AchievementSchema.methods.stringifyQuery = () -> +AchievementSchema.methods.stringifyQuery = -> @set('query', JSON.stringify(@get('query'))) if typeof @get('query') != "string" + getExpFunction: -> + kind = @get('function')?.kind or jsonschema.function.default.kind + parameters = @get('function')?.parameters or jsonschema.function.default.parameters + return utils.functionCreators[kind](parameters) if kind of utils.functionCreators + AchievementSchema.post('init', (doc) -> doc.objectifyQuery()) AchievementSchema.pre('save', (next) -> @@ -33,3 +39,7 @@ AchievementSchema.plugin(plugins.NamedPlugin) AchievementSchema.plugin(plugins.SearchablePlugin, {searchable: ['name']}) module.exports = Achievement = mongoose.model('Achievement', AchievementSchema) + +# Reload achievements upon save +AchievablePlugin = require '../plugins/achievements' +AchievementSchema.post 'save', (doc) -> AchievablePlugin.loadAchievements() diff --git a/server/achievements/EarnedAchievement.coffee b/server/achievements/EarnedAchievement.coffee index c4f017e38..16738ae78 100644 --- a/server/achievements/EarnedAchievement.coffee +++ b/server/achievements/EarnedAchievement.coffee @@ -13,7 +13,15 @@ EarnedAchievementSchema = new mongoose.Schema({ default: false }, {strict:false}) +EarnedAchievementSchema.pre 'save', (next) -> + @set('changed', Date.now()) + next() + EarnedAchievementSchema.index({user: 1, achievement: 1}, {unique: true, name: 'earned achievement index'}) EarnedAchievementSchema.index({user: 1, changed: -1}, {name: 'latest '}) -module.exports = EarnedAchievement = mongoose.model('EarnedAchievement', EarnedAchievementSchema) \ No newline at end of file + +module.exports = EarnedAchievement = mongoose.model('EarnedAchievement', EarnedAchievementSchema) + + + diff --git a/server/achievements/achievement_handler.coffee b/server/achievements/achievement_handler.coffee index 317a6f1b8..998c7694c 100644 --- a/server/achievements/achievement_handler.coffee +++ b/server/achievements/achievement_handler.coffee @@ -5,17 +5,11 @@ class AchievementHandler extends Handler modelClass: Achievement # Used to determine which properties requests may edit - editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon'] + editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon', 'function'] jsonSchema = require '../../app/schemas/models/achievement.coffee' hasAccess: (req) -> req.method is 'GET' or req.user?.isAdmin() - get: (req, res) -> - query = @modelClass.find({}) - query.exec (err, documents) => - return @sendDatabaseError(res, err) if err - documents = (@formatEntity(req, doc) for doc in documents) - @sendSuccess(res, documents) module.exports = new AchievementHandler() diff --git a/server/achievements/earned_achievement_handler.coffee b/server/achievements/earned_achievement_handler.coffee index ebab1e45d..24615054d 100644 --- a/server/achievements/earned_achievement_handler.coffee +++ b/server/achievements/earned_achievement_handler.coffee @@ -1,12 +1,115 @@ -mongoose = require('mongoose') +log = require 'winston' +mongoose = require 'mongoose' +async = require 'async' +Achievement = require './Achievement' EarnedAchievement = require './EarnedAchievement' +User = require '../users/User' Handler = require '../commons/Handler' +LocalMongo = require '../../app/lib/LocalMongo' class EarnedAchievementHandler extends Handler modelClass: EarnedAchievement # Don't allow POSTs or anything yet hasAccess: (req) -> - req.method is 'GET' + req.method is 'GET' # or req.user.isAdmin() + + recalculate: (req, res) -> + onSuccess = (data) => log.debug "Finished recalculating achievements" + if 'achievements' of req.body # Support both slugs and IDs separated by commas + achievementSlugsOrIDs = req.body.achievements + EarnedAchievementHandler.recalculate achievementSlugsOrIDs, onSuccess + else + EarnedAchievementHandler.recalculate onSuccess + @sendSuccess res, {} + + # Returns success: boolean + # TODO call onFinished + @recalculate: (callbackOrSlugsOrIDs, onFinished) -> + if _.isArray callbackOrSlugsOrIDs + achievementSlugs = (thing for thing in callbackOrSlugsOrIDs when not Handler.isID(thing)) + achievementIDs = (thing for thing in callbackOrSlugsOrIDs when Handler.isID(thing)) + else + onFinished = callbackOrSlugsOrIDs + + filter = {} + filter.$or = [ + {_id: $in: achievementIDs}, + {slug: $in: achievementSlugs} + ] if achievementSlugs? or achievementIDs? + + # Fetch all relevant achievements + Achievement.find filter, (err, achievements) -> + return log.error err if err? + + # Fetch every single user + User.find {}, (err, users) -> + _.each users, (user) -> + # Keep track of a user's already achieved in order to set the notified values correctly + userID = user.get('_id').toHexString() + + # Fetch all of a user's earned achievements + EarnedAchievement.find {user: userID}, (err, alreadyEarned) -> + alreadyEarnedIDs = [] + previousPoints = 0 + _.each alreadyEarned, (earned) -> + if (_.find achievements, (single) -> earned.get('achievement') is single.get('_id').toHexString()) + alreadyEarnedIDs.push earned.get('achievement') + previousPoints += earned.get 'earnedPoints' + + # TODO maybe also delete earned? Make sure you don't delete too many + + newTotalPoints = 0 + + earnedAchievementSaverGenerator = (achievement) -> (callback) -> + isRepeatable = achievement.get('proportionalTo')? + model = mongoose.model(achievement.get('collection')) + if not model? + log.error "Model #{achievement.get 'collection'} doesn't even exist." + return callback() + + model.findOne achievement.query, (err, something) -> + return callback() unless something + + log.debug "Matched an achievement: #{achievement.get 'name'}" + + earned = + user: userID + achievement: achievement._id.toHexString() + achievementName: achievement.get 'name' + notified: achievement._id in alreadyEarnedIDs + + if isRepeatable + earned.achievedAmount = something.get(achievement.get 'proportionalTo') + earned.previouslyAchievedAmount = 0 + + expFunction = achievement.getExpFunction() + newPoints = expFunction(earned.achievedAmount) * achievement.get('worth') + else + newPoints = achievement.get 'worth' + + earned.earnedPoints = newPoints + newTotalPoints += newPoints + + EarnedAchievement.update {achievement:earned.achievement, user:earned.user}, earned, {upsert: true}, (err) -> + log.error err if err? + callback() + + saveUserPoints = (callback) -> + # In principle it is enough to deduct the old amount of points and add the new amount, + # but just to be entirely safe let's start from 0 in case we're updating all of a user's achievements + log.debug "Matched a total of #{newTotalPoints} new points" + if _.isEmpty filter # Completely clean + User.update {_id: userID}, {$set: points: newTotalPoints}, {}, (err) -> log.error err if err? + else + log.debug "Incrementing score for these achievements with #{newTotalPoints - previousPoints}" + User.update {_id: userID}, {$inc: points: newTotalPoints - previousPoints}, {}, (err) -> log.error err if err? + + earnedAchievementSavers = (earnedAchievementSaverGenerator(achievement) for achievement in achievements) + earnedAchievementSavers.push saveUserPoints + + # We need to have all these database updates chained so we know the final score + async.series earnedAchievementSavers + module.exports = new EarnedAchievementHandler() diff --git a/server/commons/Handler.coffee b/server/commons/Handler.coffee index e8fdbd668..158f40223 100644 --- a/server/commons/Handler.coffee +++ b/server/commons/Handler.coffee @@ -17,6 +17,7 @@ module.exports = class Handler postEditableProperties: [] jsonSchema: {} waterfallFunctions: [] + allowedMethods: ['GET', 'POST', 'PUT', 'PATCH'] # subclasses should override these methods hasAccess: (req) -> true @@ -63,26 +64,72 @@ module.exports = class Handler # generic handlers get: (req, res) -> - # by default, ordinary users never get unfettered access to the database - return @sendUnauthorizedError(res) unless req.user?.isAdmin() + @sendUnauthorizedError(res) if not @hasAccess(req) - # admins can send any sort of query down the wire, though - conditions = JSON.parse(req.query.conditions || '[]') - query = @modelClass.find() + specialParameters = ['term', 'project', 'conditions'] - try - for condition in conditions - name = condition[0] - f = query[name] - args = condition[1..] - query = query[name](args...) - catch e - return @sendError(res, 422, 'Badly formed conditions.') + # If the model uses coco search it's probably a text search + if @modelClass.schema.uses_coco_search + term = req.query.term + matchedObjects = [] + filters = if @modelClass.schema.uses_coco_versions or @modelClass.schema.uses_coco_permissions then [filter: {index: true}] else [filter: {}] + if @modelClass.schema.uses_coco_permissions and req.user + filters.push {filter: {index: req.user.get('id')}} + projection = null + if req.query.project is 'true' + projection = PROJECT + else if req.query.project + if @modelClass.className is 'User' + projection = PROJECT + log.warn "Whoa, we haven't yet thought about public properties for User projection yet." + else + projection = {} + projection[field] = 1 for field in req.query.project.split(',') + for filter in filters + callback = (err, results) => + return @sendDatabaseError(res, err) if err + for r in results.results ? results + obj = r.obj ? r + continue if obj in matchedObjects # TODO: probably need a better equality check + matchedObjects.push obj + filters.pop() # doesn't matter which one + unless filters.length + res.send matchedObjects + res.end() + if term + filter.project = projection + @modelClass.textSearch term, filter, callback + else + args = [filter.filter] + args.push projection if projection + @modelClass.find(args...).limit(FETCH_LIMIT).exec callback + # if it's not a text search but the user is an admin, let him try stuff anyway + else if req.user?.isAdmin() + # admins can send any sort of query down the wire + filter = {} + filter[key] = (val for own key, val of req.query.filter when key not in specialParameters) if 'filter' of req.query + + query = @modelClass.find(filter) + + # Conditions are chained query functions, for example: query.find().limit(20).sort('-dateCreated') + conditions = JSON.parse(req.query.conditions || '[]') + try + for condition in conditions + name = condition[0] + f = query[name] + args = condition[1..] + query = query[name](args...) + catch e + return @sendError(res, 422, 'Badly formed conditions.') + + query.exec (err, documents) => + return @sendDatabaseError(res, err) if err + documents = (@formatEntity(req, doc) for doc in documents) + @sendSuccess(res, documents) + # regular users are only allowed text searches for now, without any additional filters or sorting + else + return @sendUnauthorizedError(res) - query.exec (err, documents) => - return @sendDatabaseError(res, err) if err - documents = (@formatEntity(req, doc) for doc in documents) - @sendSuccess(res, documents) getById: (req, res, id) -> # return @sendNotFoundError(res) # for testing @@ -153,44 +200,6 @@ module.exports = class Handler return @sendDatabaseError(res, err) if err @sendSuccess(res, @formatEntity(req, document)) - # project=true or project=name,description,slug for example - search: (req, res) -> - unless @modelClass.schema.uses_coco_search - return @sendNotFoundError(res) - term = req.query.term - matchedObjects = [] - filters = if @modelClass.schema.uses_coco_versions or @modelClass.schema.uses_coco_permissions then [filter: {index: true}] else [filter: {}] - if @modelClass.schema.uses_coco_permissions and req.user - filters.push {filter: {index: req.user.get('id')}} - projection = null - if req.query.project is 'true' - projection = PROJECT - else if req.query.project - if @modelClass.className is 'User' - projection = PROJECT - log.warn "Whoa, we haven't yet thought about public properties for User projection yet." - else - projection = {} - projection[field] = 1 for field in req.query.project.split(',') - for filter in filters - callback = (err, results) => - return @sendDatabaseError(res, err) if err - for r in results.results ? results - obj = r.obj ? r - continue if obj in matchedObjects # TODO: probably need a better equality check - matchedObjects.push obj - filters.pop() # doesn't matter which one - unless filters.length - res.send matchedObjects - res.end() - if term - filter.project = projection - @modelClass.textSearch term, filter, callback - else - args = [filter.filter] - args.push projection if projection - @modelClass.find(args...).limit(FETCH_LIMIT).exec callback - versions: (req, res, id) -> # TODO: a flexible system for doing GAE-like cursors for these sort of paginating queries # Keeping it simple for now and just allowing access to the first FETCH_LIMIT results. @@ -420,3 +429,7 @@ module.exports = class Handler dict[document.id] = document res.send dict res.end() + + delete: (req, res) -> @sendMethodNotAllowed res, @allowedMethods, "DELETE not allowed." + + head: (req, res) -> @sendMethodNotAllowed res, @allowedMethods, "HEAD not allowed." diff --git a/server/commons/errors.coffee b/server/commons/errors.coffee index 8af347126..3f60ef852 100644 --- a/server/commons/errors.coffee +++ b/server/commons/errors.coffee @@ -17,8 +17,9 @@ module.exports.notFound = (res, message='Not found.') -> res.send 404, message res.end() -module.exports.badMethod = (res, message='Method Not Allowed') -> - # TODO: The response MUST include an Allow header containing a list of valid methods for the requested resource +module.exports.badMethod = (res, allowed=['GET', 'POST', 'PUT', 'PATCH'], message='Method Not Allowed') -> + allowHeader = _.reduce allowed, ((str, current) -> str += ', ' + current) + res.set 'Allow', allowHeader # TODO not sure if these are always the case res.send 405, message res.end() @@ -40,4 +41,4 @@ module.exports.gatewayTimeoutError = (res, message="Gateway timeout") -> module.exports.clientTimeout = (res, message="The server did not recieve the client response in a timely manner") -> res.send 408, message - res.end() \ No newline at end of file + res.end() diff --git a/server/commons/mapping.coffee b/server/commons/mapping.coffee index 98efbad6a..4b859632c 100644 --- a/server/commons/mapping.coffee +++ b/server/commons/mapping.coffee @@ -14,6 +14,7 @@ module.exports.handlers = module.exports.routes = [ + 'routes/admin' 'routes/auth' 'routes/contact' 'routes/db' diff --git a/server/levels/components/level_component_handler.coffee b/server/levels/components/level_component_handler.coffee index 3bcc572d0..98844ddbc 100644 --- a/server/levels/components/level_component_handler.coffee +++ b/server/levels/components/level_component_handler.coffee @@ -9,7 +9,7 @@ LevelComponentHandler = class LevelComponentHandler extends Handler 'description' 'code' 'js' - 'language' + 'codeLanguage' 'dependencies' 'propertyDocumentation' 'configSchema' @@ -25,4 +25,4 @@ LevelComponentHandler = class LevelComponentHandler extends Handler req.method is 'GET' or req.user?.isAdmin() -module.exports = new LevelComponentHandler() \ No newline at end of file +module.exports = new LevelComponentHandler() diff --git a/server/levels/sessions/level_session_handler.coffee b/server/levels/sessions/level_session_handler.coffee index 1c03a17f6..7a89cebdf 100644 --- a/server/levels/sessions/level_session_handler.coffee +++ b/server/levels/sessions/level_session_handler.coffee @@ -14,14 +14,14 @@ class LevelSessionHandler extends Handler getByRelationship: (req, res, args...) -> return @getActiveSessions req, res if args.length is 2 and args[1] is 'active' super(arguments...) - + formatEntity: (req, document) -> documentObject = super(req, document) - if req.user.isAdmin() or req.user.id is document.creator + if req.user.isAdmin() or req.user.id is document.creator or ('employer' in req.user.get('permissions')) return documentObject else return _.omit documentObject, ['submittedCode','code'] - + getActiveSessions: (req, res) -> return @sendUnauthorizedError(res) unless req.user.isAdmin() start = new Date() @@ -34,6 +34,7 @@ class LevelSessionHandler extends Handler hasAccessToDocument: (req, document, method=null) -> return true if req.method is 'GET' and document.get('totalScore') + return true if ('employer' in req.user.get('permissions')) and (method ? req.method).toLowerCase() is 'get' super(arguments...) module.exports = new LevelSessionHandler() diff --git a/server/levels/systems/level_system_handler.coffee b/server/levels/systems/level_system_handler.coffee index bf1bb39d5..e19dd43bd 100644 --- a/server/levels/systems/level_system_handler.coffee +++ b/server/levels/systems/level_system_handler.coffee @@ -7,7 +7,7 @@ LevelSystemHandler = class LevelSystemHandler extends Handler 'description' 'code' 'js' - 'language' + 'codeLanguage' 'dependencies' 'propertyDocumentation' 'configSchema' diff --git a/server/plugins/achievements.coffee b/server/plugins/achievements.coffee index 9fd6a9c94..a3f0096af 100644 --- a/server/plugins/achievements.coffee +++ b/server/plugins/achievements.coffee @@ -1,24 +1,15 @@ mongoose = require('mongoose') -Achievement = require('../achievements/Achievement') EarnedAchievement = require '../achievements/EarnedAchievement' -User = require '../users/User' LocalMongo = require '../../app/lib/LocalMongo' util = require '../../app/lib/utils' log = require 'winston' achievements = {} -loadAchievements = -> - achievements = {} - query = Achievement.find({}) - query.exec (err, docs) -> - _.each docs, (achievement) -> - category = achievement.get 'collection' - achievements[category] = [] unless category of achievements - achievements[category].push achievement -loadAchievements() - module.exports = AchievablePlugin = (schema, options) -> + User = require '../users/User' # Avoid mutual inclusion cycles + Achievement = require('../achievements/Achievement') + checkForAchievement = (doc) -> collectionName = doc.constructor.modelName @@ -53,6 +44,8 @@ module.exports = AchievablePlugin = (schema, options) -> achievement: achievement._id.toHexString() achievementName: achievement.get 'name' } + + worth = achievement.get('worth') earnedPoints = 0 wrapUp = -> # Update user's experience points @@ -67,23 +60,37 @@ module.exports = AchievablePlugin = (schema, options) -> newAmount = docObj[proportionalTo] if originalAmount isnt newAmount + expFunction = achievement.getExpFunction() earned.notified = false earned.achievedAmount = newAmount - earned.changed = Date.now() - EarnedAchievement.findOneAndUpdate({achievement:earned.achievement, user:earned.user}, earned, upsert:true, (err, docs) -> - return log.debug err if err? - ) + earned.earnedPoints = (expFunction(newAmount) - expFunction(originalAmount)) * worth + earned.previouslyAchievedAmount = originalAmount + EarnedAchievement.update {achievement:earned.achievement, user:earned.user}, earned, {upsert: true}, (err) -> + return log.debug err if err? - earnedPoints = achievement.get('worth') * (newAmount - originalAmount) + earnedPoints = earned.earnedPoints + log.debug earnedPoints wrapUp() else # not alreadyAchieved log.debug 'Creating a new earned achievement called \'' + (achievement.get 'name') + '\' for ' + userID + earned.earnedPoints = worth (new EarnedAchievement(earned)).save (err, doc) -> return log.debug err if err? - - earnedPoints = achievement.get('worth') + earnedPoints = worth wrapUp() delete before[doc.id] unless isNew # This assumes everything we patch has a _id return + +module.exports.loadAchievements = -> + achievements = {} + Achievement = require('../achievements/Achievement') + query = Achievement.find({}) + query.exec (err, docs) -> + _.each docs, (achievement) -> + category = achievement.get 'collection' + achievements[category] = [] unless category of achievements + achievements[category].push achievement + +AchievablePlugin.loadAchievements() diff --git a/server/plugins/plugins.coffee b/server/plugins/plugins.coffee index 4b189967e..25674f688 100644 --- a/server/plugins/plugins.coffee +++ b/server/plugins/plugins.coffee @@ -1,12 +1,29 @@ mongoose = require('mongoose') -User = require('../users/User') textSearch = require('mongoose-text-search') +module.exports.MigrationPlugin = (schema, migrations) -> + # Property name migrations made EZ + # This is for just when you want one property to be named differently + + # 1. Change the schema and the client/server logic to use the new name + # 2. Add this plugin to the target models, passing in a dictionary of old/new names. + # 3. Check that tests still run, deploy to production. + # 4. Run db..update({}, { $rename: {'':''} }, { multi: true }) on the server + # 5. Remove the names you added to the migrations dictionaries for the next deploy + + schema.post 'init', -> + for oldKey in _.keys migrations + val = @get oldKey + @set oldKey, undefined + continue if val is undefined + newKey = migrations[oldKey] + @set newKey, val + module.exports.PatchablePlugin = (schema) -> schema.is_patchable = true schema.index({'target.original':1, 'status':'1', 'created':-1}) -RESERVED_NAMES = ['search', 'names'] +RESERVED_NAMES = ['names'] module.exports.NamedPlugin = (schema) -> schema.uses_coco_names = true diff --git a/server/routes/admin.coffee b/server/routes/admin.coffee new file mode 100644 index 000000000..c2ae7612a --- /dev/null +++ b/server/routes/admin.coffee @@ -0,0 +1,27 @@ +log = require 'winston' +errors = require '../commons/errors' +handlers = require('../commons/mapping').handlers + +mongoose = require('mongoose') + +module.exports.setup = (app) -> + app.post '/admin/*', (req, res) -> + # TODO apparently I can leave this out as long as I use res.send + res.setHeader('Content-Type', 'application/json') + + module = req.path[7..] + parts = module.split('/') + module = parts[0] + + return errors.unauthorized(res, 'Must be admin to access this area.') unless req.user?.isAdmin() + + try + moduleName = module.replace '.', '_' + name = handlers[moduleName] + handler = require('../' + name) + + return handler[parts[1]](req, res, parts[2..]...) if parts[1] of handler + + catch error + log.error("Error trying db method '#{req.route.method}' route '#{parts}' from #{name}: #{error}") + errors.notFound(res, "Route #{req.path} not found.") diff --git a/server/routes/auth.coffee b/server/routes/auth.coffee index eed93dfae..8e99682be 100644 --- a/server/routes/auth.coffee +++ b/server/routes/auth.coffee @@ -61,8 +61,11 @@ module.exports.setup = (app) -> req.logIn(user, (err) -> return next(err) if (err) - res.send(UserHandler.formatEntity(req, req.user)) - return res.end() + activity = req.user.trackActivity 'login', 1 + user.update {activity: activity}, (err) -> + return next(err) if (err) + res.send(UserHandler.formatEntity(req, req.user)) + return res.end() ) )(req, res, next) ) @@ -134,12 +137,12 @@ module.exports.setup = (app) -> emails = _.clone(user.get('emails')) or {} msg = '' - + if req.query.recruitNotes emails.recruitNotes ?= {} emails.recruitNotes.enabled = false msg = "Unsubscribed #{req.query.email} from recruiting emails." - + else msg = "Unsubscribed #{req.query.email} from all CodeCombat emails. Sorry to see you go!" emailSettings.enabled = false for emailSettings in _.values(emails) @@ -147,7 +150,7 @@ module.exports.setup = (app) -> emails.generalNews.enabled = false emails.anyNotes ?= {} emails.anyNotes.enabled = false - + user.update {$set: {emails: emails}}, {}, => return errors.serverError res, 'Database failure.' if err res.send msg + "

Account settings

" @@ -172,7 +175,7 @@ module.exports.makeNewUser = makeNewUser = (req) -> user = new User({anonymous:true}) user.set 'testGroupNumber', Math.floor(Math.random() * 256) # also in app/lib/auth user.set 'preferredLanguage', languages.languageCodeFromAcceptedLanguages req.acceptedLanguages - + createMailOptions = (receiver, password) -> # TODO: use email templates here options = @@ -181,4 +184,3 @@ createMailOptions = (receiver, password) -> replyTo: config.mail.username subject: "[CodeCombat] Password Reset" text: "You can log into your account with: #{password}" - diff --git a/server/routes/db.coffee b/server/routes/db.coffee index f65a56744..cddc70592 100644 --- a/server/routes/db.coffee +++ b/server/routes/db.coffee @@ -34,7 +34,6 @@ module.exports.setup = (app) -> return handler.getLatestVersion(req, res, parts[1], parts[3]) if parts[2] is 'version' return handler.versions(req, res, parts[1]) if parts[2] is 'versions' return handler.files(req, res, parts[1]) if parts[2] is 'files' - return handler.search(req, res) if req.route.method is 'get' and parts[1] is 'search' return handler.getNamesByIDs(req, res) if req.route.method in ['get', 'post'] and parts[1] is 'names' return handler.getByRelationship(req, res, parts[1..]...) if parts.length > 2 return handler.getById(req, res, parts[1]) if req.route.method is 'get' and parts[1]? diff --git a/server/routes/file.coffee b/server/routes/file.coffee index 1cc0f73d7..e96a2453a 100644 --- a/server/routes/file.coffee +++ b/server/routes/file.coffee @@ -8,7 +8,7 @@ module.exports.setup = (app) -> app.all '/file*', (req, res) -> return fileGet(req, res) if req.route.method is 'get' return filePost(req, res) if req.route.method is 'post' - return errors.badMethod(res) + return errors.badMethod(res, ['GET', 'POST']) fileGet = (req, res) -> diff --git a/server/routes/folder.coffee b/server/routes/folder.coffee index 7ac5e187a..d63701605 100644 --- a/server/routes/folder.coffee +++ b/server/routes/folder.coffee @@ -4,7 +4,7 @@ errors = require '../commons/errors' module.exports.setup = (app) -> app.all '/folder*', (req, res) -> return folderGet(req, res) if req.route.method is 'get' - return errors.badMethod(res) + return errors.badMethod(res, ['GET']) folderGet = (req, res) -> folder = req.path[7..] @@ -15,4 +15,4 @@ folderGet = (req, res) -> mongoose.connection.db.collection 'media.files', (errors, collection) -> collection.find({'metadata.path': folder}).toArray (err, results) -> res.send(results) - res.end() \ No newline at end of file + res.end() diff --git a/server/routes/languages.coffee b/server/routes/languages.coffee index a959e823d..b29cc8fdb 100644 --- a/server/routes/languages.coffee +++ b/server/routes/languages.coffee @@ -11,7 +11,7 @@ module.exports.setup = (app) -> app.all '/languages', (req, res) -> # Now that these are in the client, not sure when we would use this, but hey - return errors.badMethod(res) if req.route.method isnt 'get' + return errors.badMethod(res, ['GET']) if req.route.method isnt 'get' res.send(languages) return res.end() diff --git a/server/routes/queue.coffee b/server/routes/queue.coffee index 388bce4e0..e69ae720e 100644 --- a/server/routes/queue.coffee +++ b/server/routes/queue.coffee @@ -28,7 +28,7 @@ module.exports.setup = (app) -> app.all '/queue/*', (req, res) -> setResponseHeaderToJSONContentType res - + queueName = getQueueNameFromPath req.path try handler = loadQueueHandler queueName @@ -64,7 +64,7 @@ isHTTPMethodPost = (req) -> return req.route.method is 'post' isHTTPMethodPut = (req) -> return req.route.method is 'put' -sendMethodNotSupportedError = (req, res) -> errors.badMethod(res,"Queues do not support the HTTP method used." ) +sendMethodNotSupportedError = (req, res) -> errors.badMethod(res, ['GET', 'POST', 'PUT'], "Queues do not support the HTTP method used." ) sendQueueError = (req,res, error) -> errors.serverError(res, "Route #{req.path} had a problem: #{error}") diff --git a/server/sendwithus.coffee b/server/sendwithus.coffee index 1f8145eb1..427d203fa 100644 --- a/server/sendwithus.coffee +++ b/server/sendwithus.coffee @@ -15,3 +15,4 @@ module.exports.templates = patch_created: 'tem_xhxuNosLALsizTNojBjNcL' change_made_notify_watcher: 'tem_7KVkfmv9SZETb25dtHbUtG' one_time_recruiting_email: 'tem_mdFMgtcczHKYu94Jmq68j8' + greed_tournament_rank: 'tem_c4KYnk2TriEkkZx5NqqGLG' diff --git a/server/users/User.coffee b/server/users/User.coffee index f0cc8e514..40d662913 100644 --- a/server/users/User.coffee +++ b/server/users/User.coffee @@ -29,6 +29,17 @@ UserSchema.methods.isAdmin = -> p = @get('permissions') return p and 'admin' in p +UserSchema.methods.trackActivity = (activityName, increment) -> + now = new Date() + increment ?= parseInt increment or 1 + increment = Math.max increment, 0 + activity = @get('activity') ? {} + activity[activityName] ?= {first: now, count: 0} + activity[activityName].count += increment + activity[activityName].last = now + @set 'activity', activity + activity + emailNameMap = generalNews: 'announcement' adventurerNews: 'tester' diff --git a/server/users/user_handler.coffee b/server/users/user_handler.coffee index 508578785..5ffa68c06 100644 --- a/server/users/user_handler.coffee +++ b/server/users/user_handler.coffee @@ -48,7 +48,7 @@ UserHandler = class UserHandler extends Handler delete obj[prop] for prop in serverProperties includePrivates = req.user and (req.user.isAdmin() or req.user._id.equals(document._id)) delete obj[prop] for prop in privateProperties unless includePrivates - includeCandidate = includePrivates or (obj.jobProfileApproved and req.user and ('employer' in (req.user.get('permissions') ? [])) and @employerCanViewCandidate req.user, obj) + includeCandidate = includePrivates or (obj.jobProfile?.active and req.user and ('employer' in (req.user.get('permissions') ? [])) and @employerCanViewCandidate req.user, obj) delete obj[prop] for prop in candidateProperties unless includeCandidate return obj @@ -189,11 +189,14 @@ UserHandler = class UserHandler extends Handler return @avatar(req, res, args[0]) if args[1] is 'avatar' return @getNamesByIDs(req, res) if args[1] is 'names' return @nameToID(req, res, args[0]) if args[1] is 'nameToID' + return @getLevelSessionsForEmployer(req, res, args[0]) if args[1] is 'level.sessions' and args[2] is 'employer' return @getLevelSessions(req, res, args[0]) if args[1] is 'level.sessions' return @getCandidates(req, res) if args[1] is 'candidates' + return @getEmployers(req, res) if args[1] is 'employers' return @getSimulatorLeaderboard(req, res, args[0]) if args[1] is 'simulatorLeaderboard' return @getMySimulatorLeaderboardRank(req, res, args[0]) if args[1] is 'simulator_leaderboard_rank' return @getEarnedAchievements(req, res, args[0]) if args[1] is 'achievements' + return @trackActivity(req, res, args[0], args[2], args[3]) if args[1] is 'track' and args[2] return @sendNotFoundError(res) super(arguments...) @@ -225,9 +228,18 @@ UserHandler = class UserHandler extends Handler res.redirect photoURL res.end() + getLevelSessionsForEmployer: (req, res, userID) -> + return @sendUnauthorizedError(res) unless req.user._id+'' is userID or req.user.isAdmin() or ('employer' in req.user.get('permissions')) + query = creator: userID, levelID: {$in: ['gridmancer', 'greed', 'dungeon-arena', 'brawlwood', 'gold-rush']} + projection = 'levelName levelID team playtime codeLanguage submitted' # code totalScore + LevelSession.find(query).select(projection).exec (err, documents) => + return @sendDatabaseError(res, err) if err + documents = (LevelSessionHandler.formatEntity(req, doc) for doc in documents) + @sendSuccess(res, documents) + getLevelSessions: (req, res, userID) -> return @sendUnauthorizedError(res) unless req.user._id+'' is userID or req.user.isAdmin() - query = {'creator': userID} + query = creator: userID projection = null if req.query.project projection = {} @@ -249,6 +261,25 @@ UserHandler = class UserHandler extends Handler doc.save() @sendSuccess(res, cleandocs) + trackActivity: (req, res, userID, activityName, increment=1) -> + return @sendMethodNotAllowed res unless req.method is 'POST' + isMe = userID is req.user._id + '' + isAuthorized = isMe or req.user.isAdmin() + isAuthorized ||= ('employer' in req.user.get('permissions')) and (activityName in ['viewed_by_employer', 'messaged_by_employer']) + return @sendUnauthorizedError res unless isAuthorized + updateUser = (user) => + activity = user.trackActivity activityName, increment + user.update {activity: activity}, (err) => + return @sendDatabaseError res, err if err + @sendSuccess res, result: 'success' + if isMe + updateUser(req.user) + else + @getDocumentForIdOrSlug userID, (err, user) => + return @sendDatabaseError res, err if err + return @sendNotFoundError res unless user + updateUser user + agreeToEmployerAgreement: (req, res) -> userIsAnonymous = req.user?.get('anonymous') if userIsAnonymous then return errors.unauthorized(res, "You need to be logged in to agree to the employer agreeement.") @@ -278,13 +309,11 @@ UserHandler = class UserHandler extends Handler getCandidates: (req, res) -> authorized = req.user.isAdmin() or ('employer' in req.user.get('permissions')) since = (new Date((new Date()) - 2 * 30.4 * 86400 * 1000)).toISOString() - #query = {'jobProfile.active': true, 'jobProfile.updated': {$gt: since}} query = {'jobProfile.updated': {$gt: since}} - query.jobProfileApproved = true unless req.user.isAdmin() + #query.jobProfileApproved = true unless req.user.isAdmin() # We split into featured and other now. query['jobProfile.active'] = true unless req.user.isAdmin() - selection = 'jobProfile' + selection = 'jobProfile jobProfileApproved photoURL' selection += ' email' if authorized - selection += ' jobProfileApproved' if req.user.isAdmin() User.find(query).select(selection).exec (err, documents) => return @sendDatabaseError(res, err) if err candidates = (candidate for candidate in documents when @employerCanViewCandidate req.user, candidate.toObject()) @@ -292,7 +321,7 @@ UserHandler = class UserHandler extends Handler @sendSuccess(res, candidates) formatCandidate: (authorized, document) -> - fields = if authorized then ['jobProfile', 'jobProfileApproved', 'photoURL', '_id'] else ['jobProfile'] + fields = if authorized then ['jobProfile', 'jobProfileApproved', 'photoURL', '_id'] else ['jobProfile', 'jobProfileApproved'] obj = _.pick document.toObject(), fields obj.photoURL ||= obj.jobProfile.photoURL if authorized subfields = ['country', 'city', 'lookingFor', 'jobTitle', 'skills', 'experience', 'updated', 'active'] @@ -311,6 +340,14 @@ UserHandler = class UserHandler extends Handler return false if job.employer?.toLowerCase() is employer.get('employerAt')?.toLowerCase() true + getEmployers: (req, res) -> + return @sendUnauthorizedError(res) unless req.user.isAdmin() + query = {employerAt: {$exists: true}} + selection = 'name firstName lastName email activity signedEmployerAgreement photoURL employerAt' + User.find(query).select(selection).lean().exec (err, documents) => + return @sendDatabaseError res, err if err + @sendSuccess res, documents + buildGravatarURL: (user, size, fallback) -> emailHash = @buildEmailHash user fallback ?= "http://codecombat.com/file/db/thang.type/52a00d55cf1818f2be00000b/portrait.png" diff --git a/server_setup.coffee b/server_setup.coffee index 0cf6b6cc3..11454a29e 100644 --- a/server_setup.coffee +++ b/server_setup.coffee @@ -93,7 +93,10 @@ sendMain = (req, res) -> log.error "Error modifying main.html: #{err}" if err # insert the user object directly into the html so the application can have it immediately. Sanitize data = data.replace('"userObjectTag"', JSON.stringify(UserHandler.formatEntity(req, req.user)).replace(/\//g, '\\/')) - res.send data + res.header "Cache-Control", "no-cache, no-store, must-revalidate" + res.header "Pragma", "no-cache" + res.header "Expires", 0 + res.send 200, data setupFacebookCrossDomainCommunicationRoute = (app) -> app.get '/channel.html', (req, res) -> diff --git a/test/app/lib/FacebookHandler.spec.coffee b/test/app/lib/FacebookHandler.spec.coffee new file mode 100644 index 000000000..662475893 --- /dev/null +++ b/test/app/lib/FacebookHandler.spec.coffee @@ -0,0 +1,80 @@ +FacebookHandler = require 'lib/FacebookHandler' + +mockAuthEvent = + response: + authResponse: + accessToken: "aksdhjflkqjrj245234b52k345q344le4j4k5l45j45s4dkljvdaskl" + userID: "4301938" + expiresIn: 5138 + signedRequest: "akjsdhfjkhea.3423nkfkdsejnfkd" + status: "connected" + +# Whatev, it's all public info anyway +mockMe = + id: "4301938" + email: "scott@codecombat.com" + first_name: "Scott" + gender: "male" + last_name: "Erickson" + link: "https://www.facebook.com/scott.erickson.779" + locale: "en_US" + name: "Scott Erickson" + timezone: -7 + updated_time: "2014-05-21T04:58:06+0000" + username: "scott.erickson.779" + verified: true + work: [ + { + employer: + id: "167559910060759" + name: "CodeCombat" + + location: + id: "114952118516947" + name: "San Francisco, California" + + start_date: "2013-02-28" + } + { + end_date: "2013-01-31" + employer: + id: "39198748555" + name: "Skritter" + + location: + id: "106109576086811" + name: "Oberlin, Ohio" + + start_date: "2008-06-01" + } + ] + +window.FB ?= { + api: -> +} + +describe 'lib/FacebookHandler.coffee', -> + it 'on facebook-logged-in, gets data from FB and sends a patch to the server', -> + me.clear({silent:true}) + me.markToRevert() + me.set({_id: '12345'}) + + spyOn FB, 'api' + + new FacebookHandler() + Backbone.Mediator.publish 'facebook-logged-in', mockAuthEvent + + expect(FB.api).toHaveBeenCalled() + apiArgs = FB.api.calls.argsFor(0) + expect(apiArgs[0]).toBe('/me') + apiArgs[1](mockMe) # sending the 'response' + request = jasmine.Ajax.requests.mostRecent() + expect(request).toBeDefined() + params = JSON.parse request.params + expect(params.firstName).toBe(mockMe.first_name) + expect(params.lastName).toBe(mockMe.last_name) + expect(params.gender).toBe(mockMe.gender) + expect(params.email).toBe(mockMe.email) + expect(params.facebookID).toBe(mockMe.id) + expect(request.method).toBe('PATCH') + expect(_.string.startsWith(request.url, '/db/user/12345')).toBeTruthy() diff --git a/test/app/lib/local_mongo.spec.coffee b/test/app/lib/local_mongo.spec.coffee index 6aa22a8f1..f3719021e 100644 --- a/test/app/lib/local_mongo.spec.coffee +++ b/test/app/lib/local_mongo.spec.coffee @@ -2,7 +2,7 @@ describe 'Local Mongo queries', -> LocalMongo = require 'lib/LocalMongo' beforeEach -> - this.fixture1 = + @fixture1 = 'id': 'somestring' 'value': 9000 'levels': [3, 8, 21] @@ -10,68 +10,72 @@ describe 'Local Mongo queries', -> 'type': 'unicorn' 'likes': ['poptarts', 'popsicles', 'popcorn'] - this.fixture2 = this: is: so: 'deep' + @fixture2 = this: is: so: 'deep' it 'regular match of a property', -> - expect(LocalMongo.matchesQuery(this.fixture1, 'gender': 'unicorn')).toBeFalsy() - expect(LocalMongo.matchesQuery(this.fixture1, 'type':'unicorn')).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'type':'zebra')).toBeFalsy() - expect(LocalMongo.matchesQuery(this.fixture1, 'type':'unicorn', 'id':'somestring')).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'gender': 'unicorn')).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'type':'unicorn')).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'type':'zebra')).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'type':'unicorn', 'id':'somestring')).toBeTruthy() it 'array match of a property', -> - expect(LocalMongo.matchesQuery(this.fixture1, 'likes':'poptarts')).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'likes':'walks on the beach')).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'likes':'poptarts')).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'likes':'walks on the beach')).toBeFalsy() it 'nested match', -> - expect(LocalMongo.matchesQuery(this.fixture2, 'this.is.so':'deep')).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture2, 'this.is.so':'deep')).toBeTruthy() it '$gt selector', -> - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gt': 8000)).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gt': [8000, 10000])).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'levels': '$gt': [10, 20, 30])).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gt': 9000)).toBeFalsy() - expect(LocalMongo.matchesQuery(this.fixture1, 'value': {'$gt': 8000}, 'worth': {'$gt': 5})).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gt': 8000)).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gt': [8000, 10000])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'levels': '$gt': [10, 20, 30])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gt': 9000)).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': {'$gt': 8000}, 'worth': {'$gt': 5})).toBeTruthy() it '$gte selector', -> - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gte': 9001)).toBeFalsy() - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gte': 9000)).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gte': [9000, 10000])).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'levels': '$gte': [21, 30])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gte': 9001)).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gte': 9000)).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gte': [9000, 10000])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'levels': '$gte': [21, 30])).toBeTruthy() it '$lt selector', -> - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$lt': 9001)).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$lt': 9000)).toBeFalsy() - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$lt': [9001, 9000])).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'levels': '$lt': [10, 20, 30])).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'value': {'$lt': 9001}, 'worth': {'$lt': 7})).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$lt': 9001)).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$lt': 9000)).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$lt': [9001, 9000])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'levels': '$lt': [10, 20, 30])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': {'$lt': 9001}, 'worth': {'$lt': 7})).toBeTruthy() it '$lte selector', -> - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$lte': 9000)).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$lte': 8000)).toBeFalsy() - expect(LocalMongo.matchesQuery(this.fixture1, 'value': {'$lte': 9000}, 'worth': {'$lte': [6, 5]})).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$lte': 9000)).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$lte': 8000)).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': {'$lte': 9000}, 'worth': {'$lte': [6, 5]})).toBeTruthy() it '$ne selector', -> - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$ne': 9000)).toBeFalsy() - expect(LocalMongo.matchesQuery(this.fixture1, 'id': '$ne': 'otherstring')).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'id': '$ne': ['otherstring', 'somestring'])).toBeFalsy() - expect(LocalMongo.matchesQuery(this.fixture1, 'likes': '$ne': ['popcorn', 'chicken'])).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$ne': 9000)).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'id': '$ne': 'otherstring')).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'id': '$ne': ['otherstring', 'somestring'])).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'likes': '$ne': ['popcorn', 'chicken'])).toBeFalsy() it '$in selector', -> - expect(LocalMongo.matchesQuery(this.fixture1, 'type': '$in': ['unicorn', 'zebra'])).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'type': '$in': ['cats', 'dogs'])).toBeFalsy() - expect(LocalMongo.matchesQuery(this.fixture1, 'likes': '$in': ['popcorn', 'chicken'])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'type': '$in': ['unicorn', 'zebra'])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'type': '$in': ['cats', 'dogs'])).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'likes': '$in': ['popcorn', 'chicken'])).toBeTruthy() it '$nin selector', -> - expect(LocalMongo.matchesQuery(this.fixture1, 'type': '$nin': ['unicorn', 'zebra'])).toBeFalsy() - expect(LocalMongo.matchesQuery(this.fixture1, 'type': '$nin': ['cats', 'dogs'])).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'likes': '$nin': ['popcorn', 'chicken'])).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'type': '$nin': ['unicorn', 'zebra'])).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'type': '$nin': ['cats', 'dogs'])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'likes': '$nin': ['popcorn', 'chicken'])).toBeFalsy() it '$or operator', -> - expect(LocalMongo.matchesQuery(this.fixture1, $or: [{value:9000}, {type:'zebra'}])).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, $or: [{value:9001}, {worth:$lt:10}])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, $or: [{value:9000}, {type:'zebra'}])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, $or: [{value:9001}, {worth:$lt:10}])).toBeTruthy() it '$and operator', -> - expect(LocalMongo.matchesQuery(this.fixture1, $and: [{value:9000}, {type:'zebra'}])).toBeFalsy() - expect(LocalMongo.matchesQuery(this.fixture1, $and: [{value:9000}, {type:'unicorn'}])).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, $and: [{value:$gte:9000}, {worth:$lt:10}])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, $and: [{value:9000}, {type:'zebra'}])).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, $and: [{value:9000}, {type:'unicorn'}])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, $and: [{value:$gte:9000}, {worth:$lt:10}])).toBeTruthy() + + it '$exists operator', -> + expect(LocalMongo.matchesQuery(@fixture1, type: $exists: true)).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, interesting: $exists: false)).toBeTruthy() diff --git a/test/app/models/CocoModel.spec.coffee b/test/app/models/CocoModel.spec.coffee new file mode 100644 index 000000000..f7703b851 --- /dev/null +++ b/test/app/models/CocoModel.spec.coffee @@ -0,0 +1,84 @@ +CocoModel = require 'models/CocoModel' + +class BlandClass extends CocoModel + @className: 'Bland' + @schema: { + type: 'object' + additionalProperties: false + properties: + number: {type: 'number'} + object: {type: 'object'} + string: {type: 'string'} + _id: {type: 'string'} + } + urlRoot: '/db/bland' + +describe 'CocoModel', -> + describe 'save', -> + + it 'saves to db/', -> + b = new BlandClass({}) + res = b.save() + request = jasmine.Ajax.requests.mostRecent() + expect(res).toBeDefined() + expect(request.url).toBe(b.urlRoot) + expect(request.method).toBe('POST') + + it 'does not save if the data is invalid based on the schema', -> + b = new BlandClass({number: 'NaN'}) + res = b.save() + expect(res).toBe(false) + request = jasmine.Ajax.requests.mostRecent() + expect(request).toBeUndefined() + + it 'uses PUT when _id is included', -> + b = new BlandClass({_id: 'test'}) + b.save() + request = jasmine.Ajax.requests.mostRecent() + expect(request.method).toBe('PUT') + + describe 'patch', -> + it 'PATCHes only properties that have changed', -> + b = new BlandClass({_id: 'test', number:1}) + b.loaded = true + b.set('string', 'string') + b.patch() + request = jasmine.Ajax.requests.mostRecent() + params = JSON.parse request.params + expect(params.string).toBeDefined() + expect(params.number).toBeUndefined() + + it 'collates all changes made over several sets', -> + b = new BlandClass({_id: 'test', number:1}) + b.loaded = true + b.set('string', 'string') + b.set('object', {4:5}) + b.patch() + request = jasmine.Ajax.requests.mostRecent() + params = JSON.parse request.params + expect(params.string).toBeDefined() + expect(params.object).toBeDefined() + expect(params.number).toBeUndefined() + + it 'does not include data from previous patches', -> + b = new BlandClass({_id: 'test', number:1}) + b.loaded = true + b.set('object', {1:2}) + b.patch() + request = jasmine.Ajax.requests.mostRecent() + attrs = JSON.stringify(b.attributes) # server responds with all + request.response({status: 200, responseText: attrs}) + + b.set('number', 3) + b.patch() + request = jasmine.Ajax.requests.mostRecent() + params = JSON.parse request.params + expect(params.object).toBeUndefined() + + it 'does nothing when there\'s nothing to patch', -> + b = new BlandClass({_id: 'test', number:1}) + b.loaded = true + b.set('number', 1) + b.patch() + request = jasmine.Ajax.requests.mostRecent() + expect(request).toBeUndefined() diff --git a/test/server/common.coffee b/test/server/common.coffee index dc01d8389..c098650ca 100644 --- a/test/server/common.coffee +++ b/test/server/common.coffee @@ -9,6 +9,10 @@ jasmine.getEnv().addReporter(new jasmine.SpecReporter({ displaySuccessfulSpec: true, displayFailedSpec: true })) + +rep = new jasmine.JsApiReporter() +jasmine.getEnv().addReporter(rep) + GLOBAL._ = require('lodash') _.str = require('underscore.string') _.mixin(_.str.exports()) @@ -26,6 +30,8 @@ models_path = [ '../../server/levels/thangs/LevelThangType' '../../server/users/User' '../../server/patches/Patch' + '../../server/achievements/Achievement' + '../../server/achievements/EarnedAchievement' ] for m in models_path @@ -149,3 +155,13 @@ _drop = (done) -> chunks = mongoose.connection.db.collection('media.chunks') chunks.remove {}, -> done() + +tickInterval = null +tick = -> + # When you want jasmine-node to exit after running the tests, + # you have to close the connection first. + if rep.finished + mongoose.disconnect() + clearTimeout tickInterval + +tickInterval = setInterval tick, 1000 diff --git a/test/server/functional/achievement.spec.coffee b/test/server/functional/achievement.spec.coffee new file mode 100644 index 000000000..793248758 --- /dev/null +++ b/test/server/functional/achievement.spec.coffee @@ -0,0 +1,110 @@ +require '../common' + +unlockable = + name: 'Dungeon Arena Started' + description: 'Started playing Dungeon Arena.' + worth: 3 + collection: 'level.session' + query: "{\"level.original\":\"dungeon-arena\"}" + userField: 'creator' + +repeatable = + name: 'Simulated' + description: 'Simulated Games.' + worth: 1 + collection: 'User' + query: "{\"simulatedBy\":{\"$gt\":\"0\"}}" + userField: '_id' + proportionalTo: 'simulatedBy' + +url = getURL('/db/achievement') + +describe 'Achievement', -> + allowHeader = 'GET, POST, PUT, PATCH' + + it 'preparing test: deleting all Achievements first', (done) -> + clearModels [Achievement, EarnedAchievement, LevelSession, User], (err) -> + expect(err).toBeNull() + done() + + it 'can\'t be created by ordinary users', (done) -> + loginJoe -> + request.post {uri: url, json: unlockable}, (err, res, body) -> + expect(res.statusCode).toBe(403) + done() + + it 'can\'t be updated by ordinary users', (done) -> + loginJoe -> + request.put {uri: url, json:unlockable}, (err, res, body) -> + expect(res.statusCode).toBe(403) + + request {method: 'patch', uri: url, json: unlockable}, (err, res, body) -> + expect(res.statusCode).toBe(403) + done() + + it 'can be created by admins', (done) -> + loginAdmin -> + request.post {uri: url, json: unlockable}, (err, res, body) -> + expect(res.statusCode).toBe(200) + unlockable._id = body._id + + request.post {uri: url, json: repeatable}, (err, res, body) -> + expect(res.statusCode).toBe(200) + repeatable._id = body._id + done() + + it 'can get all for ordinary users', (done) -> + loginJoe -> + request.get {uri: url, json: unlockable}, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.length).toBe(2) + done() + + it 'can be read by ordinary users', (done) -> + loginJoe -> + request.get {uri: url + '/' + unlockable._id, json: unlockable}, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.name).toBe(unlockable.name) + done() + + it 'can\'t be requested with HTTP HEAD method', (done) -> + loginJoe -> + request.head {uri: url + '/' + unlockable._id}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + it 'can\'t be requested with HTTP DEL method', (done) -> + loginJoe -> + request.del {uri: url + '/' + unlockable._id}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + it 'get schema', (done) -> + request.get {uri:url + '/schema'}, (err, res, body) -> + expect(res.statusCode).toBe(200) + body = JSON.parse(body) + expect(body.type).toBeDefined() + done() + + +describe 'Achieving Achievements', -> + + it 'allows users to unlock one-time Achievements', (done) -> + loginJoe (joe) -> + levelSession = + creator: joe._id + level: original: 'dungeon-arena' + + request.post {uri:getURL('/db/level.session'), json:levelSession}, (session) -> + + done() + + + xit 'cleaning up test: deleting all Achievements and relates', (done) -> + clearModels [Achievement, EarnedAchievement, LevelSession], (err) -> + expect(err).toBeNull() + done() + + diff --git a/test/server/functional/article.spec.coffee b/test/server/functional/article.spec.coffee index a3973007b..b96df0f4c 100644 --- a/test/server/functional/article.spec.coffee +++ b/test/server/functional/article.spec.coffee @@ -8,6 +8,8 @@ describe '/db/article', -> done() article = {name: 'Yo', body:'yo ma'} + article2 = {name: 'Original', body:'yo daddy'} + url = getURL('/db/article') articles = {} @@ -27,11 +29,22 @@ describe '/db/article', -> expect(body.original).toBeDefined() expect(body.creator).toBeDefined() articles[0] = body - done() + + # Having two articles allow for testing article search and such + request.post {uri:url, json:article2}, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.slug).toBeDefined() + expect(body.body).toBeDefined() + expect(body.name).toBeDefined() + expect(body.original).toBeDefined() + expect(body.creator).toBeDefined() + articles[0] = body + + done() it 'allows admins to make new minor versions', (done) -> new_article = _.clone(articles[0]) - new_article.body = '...' + new_article.body = 'yo daddy' request.post {uri:url, json:new_article}, (err, res, body) -> expect(res.statusCode).toBe(200) expect(body.version.major).toBe(0) @@ -61,7 +74,6 @@ describe '/db/article', -> expect(res.statusCode).toBe(200) expect(body.body).toBe(articles[0].body) done() - it 'does not allow regular users to make new versions', (done) -> new_article = _.clone(articles[2]) @@ -87,9 +99,41 @@ describe '/db/article', -> it 'does not allow naming an article a reserved word', (done) -> loginAdmin -> - new_article = {name: 'Search', body:'is a reserved word'} + new_article = {name: 'Names', body:'is a reserved word'} request.post {uri:url, json:new_article}, (err, res, body) -> expect(res.statusCode).toBe(422) done() - \ No newline at end of file + it 'allows regular users to get all articles', (done) -> + loginJoe -> + request.get {uri:url, json:{}}, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.length).toBe(2) + done() + + it 'allows regular users to get articles and use projection', (done) -> + loginJoe -> + # default projection + request.get {uri:url + '?project=true', json:{}}, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.length).toBe(2) + expect(body[0].created).toBeUndefined() + expect(body[0].version).toBeDefined() + + # custom projection + request.get {uri:url + '?project=original', json:{}}, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.length).toBe(2) + expect(Object.keys(body[0]).length).toBe(2) + expect(body[0].original).toBeDefined() + done() + + it 'allows regular users to perform a text search', (done) -> + loginJoe -> + request.get {uri:url + '?term="daddy"', json:{}}, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.length).toBe(1) + expect(body[0].name).toBe(article2.name) + expect(body[0].body).toBe(article2.body) + done() + diff --git a/test/server/functional/auth.spec.coffee b/test/server/functional/auth.spec.coffee index 15ef44171..35bd0660f 100644 --- a/test/server/functional/auth.spec.coffee +++ b/test/server/functional/auth.spec.coffee @@ -6,9 +6,8 @@ urlLogin = getURL('/auth/login') urlReset = getURL('/auth/reset') describe '/auth/whoami', -> - http = require 'http' it 'returns 200', (done) -> - http.get(getURL('/auth/whoami'), (response) -> + request.get(getURL('/auth/whoami'), (err, response) -> expect(response).toBeDefined() expect(response.statusCode).toBe(200) done() diff --git a/test/server/functional/file.spec.coffee b/test/server/functional/file.spec.coffee index 0fb58ffd8..eba3d24e4 100644 --- a/test/server/functional/file.spec.coffee +++ b/test/server/functional/file.spec.coffee @@ -1,12 +1,16 @@ require '../common' -describe '/file', -> +# Doesn't work on Travis. Need to figure out why, probably by having the +# url not depend on some external resource. + +xdescribe '/file', -> url = getURL('/file') files = [] options = { uri:url json: { - url: 'http://scotterickson.info/images/where-are-you.jpg' + # url: 'http://scotterickson.info/images/where-are-you.jpg' + url: 'http://fc07.deviantart.net/fs37/f/2008/283/5/1/Chu_Chu_Pikachu_by_angelishi.gif' filename: 'where-are-you.jpg' mimetype: 'image/jpeg' description: 'None!' @@ -20,9 +24,12 @@ describe '/file', -> filename: 'ittybitty.data' mimetype: 'application/octet-stream' description: 'rando-info' - my_buffer_url: 'http://scotterickson.info/images/where-are-you.jpg' + # my_buffer_url: 'http://scotterickson.info/images/where-are-you.jpg' + my_buffer_url: 'http://fc07.deviantart.net/fs37/f/2008/283/5/1/Chu_Chu_Pikachu_by_angelishi.gif' } + allowHeader = 'GET, POST' + it 'preparing test : deletes all the files first', (done) -> dropGridFS -> done() @@ -142,19 +149,28 @@ describe '/file', -> request.post(options, func) + it ' can\'t be requested with HTTP PATCH method', (done) -> + request {method: 'patch', uri:url}, (err, res) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + it ' can\'t be requested with HTTP PUT method', (done) -> request.put {uri:url}, (err, res) -> expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) done() it ' can\'t be requested with HTTP HEAD method', (done) -> request.head {uri:url}, (err, res) -> expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) done() it ' can\'t be requested with HTTP DEL method', (done) -> request.del {uri:url}, (err, res) -> expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) done() # TODO: test server errors, see what they do diff --git a/test/server/functional/folder.spec.coffee b/test/server/functional/folder.spec.coffee new file mode 100644 index 000000000..e79ef6d63 --- /dev/null +++ b/test/server/functional/folder.spec.coffee @@ -0,0 +1,35 @@ +require '../common' + +describe 'folder', -> + url = getURL('/folder') + allowHeader = 'GET' + + it 'can\'t be requested with HTTP POST method', (done) -> + request.post {uri: url}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + it 'can\'t be requested with HTTP PUT method', (done) -> + request.put {uri: url}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + it 'can\'t be requested with HTTP PATCH method', (done) -> + request {method:'patch', uri: url}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + it 'can\'t be requested with HTTP HEAD method', (done) -> + request.head {uri: url}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + it 'can\'t be requested with HTTP DELETE method', (done) -> + request.del {uri: url}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() diff --git a/test/server/functional/languages.spec.coffee b/test/server/functional/languages.spec.coffee new file mode 100644 index 000000000..f8ccba0df --- /dev/null +++ b/test/server/functional/languages.spec.coffee @@ -0,0 +1,35 @@ +require '../common' + +describe 'languages', -> + url = getURL('/languages') + allowHeader = 'GET' + + it 'can\'t be requested with HTTP POST method', (done) -> + request.post {uri: url}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + it 'can\'t be requested with HTTP PUT method', (done) -> + request.put {uri: url}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + it 'can\'t be requested with HTTP PATCH method', (done) -> + request {method:'patch', uri: url}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + it 'can\'t be requested with HTTP HEAD method', (done) -> + request.head {uri: url}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + it 'can\'t be requested with HTTP DELETE method', (done) -> + request.del {uri: url}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() diff --git a/test/server/functional/level_component.spec.coffee b/test/server/functional/level_component.spec.coffee index 9127ccefd..e185657ba 100644 --- a/test/server/functional/level_component.spec.coffee +++ b/test/server/functional/level_component.spec.coffee @@ -6,7 +6,7 @@ describe 'LevelComponent', -> name:'BashesEverything' description:'Makes the unit uncontrollably bash anything bashable, using the bash system.' code: 'bash();' - language: 'coffeescript' + codeLanguage: 'coffeescript' permissions:simplePermissions propertyDocumentation: [] system: 'ai' @@ -35,7 +35,7 @@ describe 'LevelComponent', -> expect(body.name).toBe(component.name) expect(body.description).toBe(component.description) expect(body.code).toBe(component.code) - expect(body.language).toBe(component.language) + expect(body.codeLanguage).toBe(component.codeLanguage) expect(body.__v).toBe(0) expect(body.creator).toBeDefined() expect(body.original).toBeDefined() @@ -69,7 +69,7 @@ describe 'LevelComponent', -> expect(body.slug).toBeDefined() expect(body.description).toBe(components[0].description) expect(body.code).toBe(components[0].code) - expect(body.language).toBe(components[0].language) + expect(body.codeLanguage).toBe(components[0].codeLanguage) expect(body.__v).toBe(0) expect(body.official).toBeDefined() expect(body.creator).toBeDefined() @@ -130,17 +130,17 @@ describe 'LevelComponent', -> xit ' can\'t be requested with HTTP PUT method', (done) -> request.put {uri:url+'/'+components[0]._id}, (err, res) -> - expect(res.statusCode).toBe(404) + expect(res.statusCode).toBe(405) done() it ' can\'t be requested with HTTP HEAD method', (done) -> request.head {uri:url+'/'+components[0]._id}, (err, res) -> - expect(res.statusCode).toBe(404) + expect(res.statusCode).toBe(405) done() it ' can\'t be requested with HTTP DEL method', (done) -> request.del {uri:url+'/'+components[0]._id}, (err, res) -> - expect(res.statusCode).toBe(404) + expect(res.statusCode).toBe(405) done() it 'get schema', (done) -> diff --git a/test/server/functional/level_system.spec.coffee b/test/server/functional/level_system.spec.coffee index 229c3a39d..a84c75308 100644 --- a/test/server/functional/level_system.spec.coffee +++ b/test/server/functional/level_system.spec.coffee @@ -9,7 +9,7 @@ describe 'LevelSystem', -> constructor: (world) -> super world """ - language: 'coffeescript' + codeLanguage: 'coffeescript' permissions:simplePermissions dependencies: [] propertyDocumentation: [] @@ -37,7 +37,7 @@ describe 'LevelSystem', -> expect(body.name).toBe(system.name) expect(body.description).toBe(system.description) expect(body.code).toBe(system.code) - expect(body.language).toBe(system.language) + expect(body.codeLanguage).toBe(system.codeLanguage) expect(body.__v).toBe(0) expect(body.creator).toBeDefined() expect(body.original).toBeDefined() @@ -71,7 +71,7 @@ describe 'LevelSystem', -> expect(body.slug).toBeDefined() expect(body.description).toBe(systems[0].description) expect(body.code).toBe(systems[0].code) - expect(body.language).toBe(systems[0].language) + expect(body.codeLanguage).toBe(systems[0].codeLanguage) expect(body.__v).toBe(0) expect(body.official).toBeDefined() expect(body.creator).toBeDefined() @@ -123,12 +123,12 @@ describe 'LevelSystem', -> it ' can\'t be requested with HTTP HEAD method', (done) -> request.head {uri:url+'/'+systems[0]._id}, (err, res) -> - expect(res.statusCode).toBe(404) + expect(res.statusCode).toBe(405) done() it ' can\'t be requested with HTTP DEL method', (done) -> request.del {uri:url+'/'+systems[0]._id}, (err, res) -> - expect(res.statusCode).toBe(404) + expect(res.statusCode).toBe(405) done() it 'get schema', (done) -> diff --git a/test/server/functional/queue.spec.coffee b/test/server/functional/queue.spec.coffee new file mode 100644 index 000000000..c59ff409b --- /dev/null +++ b/test/server/functional/queue.spec.coffee @@ -0,0 +1,25 @@ +require '../common' + +describe 'queue', -> + someURL = getURL('/queue/') + allowHeader = 'GET, POST, PUT' + + xit 'can\'t be requested with HTTP PATCH method', (done) -> + request {method:'patch', uri: someURL}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + xit 'can\'t be requested with HTTP HEAD method', (done) -> + request.head {uri: someURL}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + xit 'can\'t be requested with HTTP DELETE method', (done) -> + request.del {uri: someURL}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + diff --git a/test/server/integration/models/Level.spec.coffee b/test/server/integration/models/Level.spec.coffee index 7cbc286d7..e74daaccb 100644 --- a/test/server/integration/models/Level.spec.coffee +++ b/test/server/integration/models/Level.spec.coffee @@ -18,13 +18,3 @@ describe 'Level', -> level.save (err) -> throw err if err done() - - it 'loads again after being saved', (done) -> - url = getURL('/db/level/'+level._id) - request.get url, (err, res, body) -> - expect(res.statusCode).toBe(200) - sameLevel = JSON.parse(body) - expect(sameLevel.name).toEqual(level.get 'name') - expect(sameLevel.description).toEqual(level.get 'description') - expect(sameLevel.permissions).toEqual(simplePermissions) - done() diff --git a/test/server/integration/models/LevelComponent.spec.coffee b/test/server/integration/models/LevelComponent.spec.coffee index 90dc5779d..6936fa86f 100644 --- a/test/server/integration/models/LevelComponent.spec.coffee +++ b/test/server/integration/models/LevelComponent.spec.coffee @@ -6,7 +6,7 @@ describe 'LevelComponent', -> name:'Bashes Everything' description:'Makes the unit uncontrollably bash anything bashable, using the bash system.' code: 'bash();' - language: 'javascript' + codeLanguage: 'javascript' official: true permissions:simplePermissions diff --git a/test/server/integration/models/LevelSystem.spec.coffee b/test/server/integration/models/LevelSystem.spec.coffee index 39008f9f7..d61cd98e0 100644 --- a/test/server/integration/models/LevelSystem.spec.coffee +++ b/test/server/integration/models/LevelSystem.spec.coffee @@ -9,7 +9,7 @@ describe 'LevelSystem', -> constructor: (world) -> super world """ - language: 'coffeescript' + codeLanguage: 'coffeescript' official: true permissions:simplePermissions