Merge branch 'master' into production

This commit is contained in:
Matt Lott 2016-02-05 10:50:47 -08:00
commit 1cfdee1679
14 changed files with 389 additions and 472 deletions

View file

@ -4,7 +4,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
no_ie: "抱歉Internet Explorer 8 等旧式浏览器无法使用本网站。" # Warning that only shows up in IE8 and older
no_mobile: "CodeCombat不是针对移动设备设计的所以可能无法达到最佳体验" # Warning that shows up on mobile devices
play: "开始游戏" # The big play button that opens up the campaign view.
# play_campaign_version: "Play Campaign Version" # Shows up under big play button if you only play /courses
play_campaign_version: "玩战役模式" # Shows up under big play button if you only play /courses
old_browser: "噢, 您的浏览器版本太旧了, 不能运行CodeCombat。抱歉!" # Warning that shows up on really old Firefox/Chrome/Safari
old_browser_suffix: "您可以继续重试下去,但很可能不起作用,更新浏览器吧亲~"
ipad_browser: "坏消息:CodeCombat无法在iPad的浏览器中运行。好消息我们的iPad应用正在等待苹果公司审核通过。"
@ -17,7 +17,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
nav:
play: "关卡选择" # The top nav bar entry where players choose which levels to play
community: "社区"
# courses: "Courses"
courses: "课程"
editor: "编辑器"
blog: "博客"
forum: "论坛"
@ -52,7 +52,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
play:
play_as: "扮演" # Ladder page
# compete: "Compete!" # Course details page
compete: "开战!" # Course details page
spectate: "旁观他人的游戏" # Ladder page
players: "玩家" # Hover over a level on /play
hours_played: "游戏时长" # Hover over a level on /play
@ -75,7 +75,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
subscription_required: "需订阅"
anonymous: "匿名玩家"
level_difficulty: "难度:"
# play_classroom_version: "Play Classroom Version" # Choose a level in campaign version that you also can play in one of your courses
play_classroom_version: "玩课堂模式" # Choose a level in campaign version that you also can play in one of your courses
campaign_beginner: "新手作战"
awaiting_levels_adventurer_prefix: "我们每周都会开放新关卡"
awaiting_levels_adventurer: "注册成为冒险家"
@ -560,14 +560,14 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
cat_blurb: "气宗"
scott_title: "共同创始人" # {change}
scott_blurb: "理性至上"
# maka_title: "Customer Advocate"
# maka_blurb: "Storyteller"
maka_title: "客户律师"
maka_blurb: "故事作者"
rob_title: "编译器工程师" # {change}
rob_blurb: "编代码之类的"
josh_c_title: "游戏设计师"
josh_c_blurb: "设计游戏"
# robin_title: "UX Design & Research"
# robin_blurb: "Scaffolding"
robin_title: "用户体验设计和研究"
robin_blurb: "基架"
josh_title: "游戏设计师"
josh_blurb: "地面是熔岩"
retrostyle_title: "插画师"
@ -594,15 +594,15 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
being_reviewed_2: "审核。"
approved_1: "您的免费订阅试用申请已被" # {change}
approved_2: "批准。" # {change}
# approved_4: "You can now enroll your students on the"
# approved_5: "courses"
# approved_6: "page."
approved_4: "现在你可以招收学生在"
approved_5: "课程"
approved_6: "页面。"
denied_1: "您的免费订阅试用申请已被" # {change}
denied_2: "拒绝。"
contact_1: "如果你有进一步的问题, 请联系"
contact_2: ""
description_1: "我们可为教师提供用于评估的免费订阅。你可以在我们的"
# description_1b: "You can find more information on our"
description_1b: "你可以找到更多信息在我们的"
description_2: "教师"
description_3: "页面找到更多的信息。"
description_4: "请填写此简单问卷,我们将会向您的电子邮件发送设置说明。"
@ -736,7 +736,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
subs_only: "只限订阅"
create_clan: "创建新的部落"
private_preview: "预览"
# private_clans: "Private Clans"
private_clans: "私人部落"
public_clans: "公开部落"
my_clans: "我的部落"
clan_name: "部落名字"
@ -773,7 +773,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
playtime: "游戏时间"
last_played: "最后玩了"
leagues_explanation: "在部落里与其他成员组成联盟一起参加下面的多人竞技场。"
# track_concepts1: "Track concepts"
track_concepts1: "跟踪概念"
track_concepts2a: "由每位学生学习"
track_concepts2b: "由每位成员学习"
track_concepts3a: "查看每位同学达到的等级"
@ -785,7 +785,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
track_concepts6b: "按姓名或进度排序成员"
track_concepts7: "需要邀请"
track_concepts8: "来加入"
# private_require_sub: "Private clans require a subscription to create or join."
private_require_sub: "创建或加入私人部落时需要具体描述。"
courses:
course: "课程"
@ -818,7 +818,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
play_time: "游戏时间:"
completed: "完成:"
invite_students: "邀请学生加入此班级。"
invite_link_header: "参与班级的縺结"
invite_link_header: "参与班级的链接"
invite_link_p_1: "分享给您想分享的其他人以加入课程。"
invite_link_p_2: "或让我们代你直接发送电邮:"
capacity_used: "课程插槽已用:"
@ -865,152 +865,152 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
topics: "题目"
hours_content: "内容时间:"
get_free: "取得免费课程!"
# enroll_paid: "Enroll Students in Paid Courses"
# you_have1: "You have"
# you_have2: "unused paid enrollments"
# use_one: "Use 1 paid enrollment for"
# use_multiple: "Use paid enrollments for the following students:"
# already_enrolled: "already enrolled"
# licenses_remaining: "licenses remaining:"
# insufficient_enrollments: "insufficient paid enrollments"
# enroll_students: "Enroll Students"
# get_enrollments: "Get More Enrollments"
# change_language: "Change Course Language"
# keep_using: "Keep Using"
# switch_to: "Switch To"
# greetings: "Greetings!"
# learn_p: "Learn Python"
# learn_j: "Learn JavaScript"
# language_cannot_change: "Language cannot be changed once students join a class."
# back_classrooms: "Back to my classrooms"
# back_courses: "Back to my courses"
# edit_details: "Edit class details"
# enrolled_courses: "enrolled in paid courses:"
# purchase_enrollments: "Purchase Enrollments"
# remove_student: "remove student"
# assign: "Assign"
# to_assign: "to assign paid courses."
# teacher: "Teacher"
# complete: "Complete"
# none: "None"
# save: "Save"
# play_campaign_title: "Play the Campaign"
# play_campaign_description: "Youre ready to take the next step! Explore hundreds of challenging levels, learn advanced programming skills, and compete in multiplayer arenas!"
# create_account_title: "Create an Account"
# create_account_description: "Sign up for a FREE CodeCombat account and gain access to more levels, more programming skills, and more fun!"
# preview_campaign_title: "Preview Campaign"
# preview_campaign_description: "Take a sneak peek at all that CodeCombat has to offer before signing up for your FREE account."
# arena: "Arena"
# arena_soon_title: "Arena Coming Soon"
# arena_soon_description: "We are working on a multiplayer arena for classrooms at the end of"
# not_enrolled1: "Not enrolled"
# not_enrolled2: "Ask your teacher to enroll you in the next course."
# next_course: "Next Course"
# coming_soon1: "Coming soon"
# coming_soon2: "We are hard at work making more courses for you!"
# available_levels: "Available Levels"
# welcome_to_courses: "Adventurers, welcome to Courses!"
# ready_to_play: "Ready to play?"
# start_new_game: "Start New Game"
# play_now_learn_header: "Play now to learn"
# play_now_learn_1: "basic syntax to control your character"
# play_now_learn_2: "while loops to solve pesky puzzles"
# play_now_learn_3: "strings & variables to customize actions"
# play_now_learn_4: "how to defeat an ogre (important life skills!)"
# welcome_to_page: "Welcome to your Courses page!"
# completed_hoc: "Amazing! You've completed the Hour of Code course!"
# ready_for_more_header: "Ready for more? Play the campaign mode!"
# ready_for_more_1: "Use gems to unlock new items!"
# ready_for_more_2: "Play through brand new worlds and challenges"
# ready_for_more_3: "Learn even more programming!"
# saved_games: "Saved Games"
# hoc: "Hour of Code"
# my_classes: "My Classes"
# class_added: "Class successfully added!"
# view_class: "view class"
# view_levels: "view levels"
# join_class: "Join A Class"
# ask_teacher_for_code: "Ask your teacher if you have a CodeCombat class code! If so, enter it below:"
# enter_c_code: "<Enter Class Code>"
# join: "Join"
# joining: "Joining class"
# course_complete: "Course Complete"
# play_arena: "Play Arena"
# start: "Start"
# last_level: "Last Level"
# welcome_to_hoc: "Adventurers, welcome to our Hour of Code!"
# logged_in_as: "Logged in as:"
# not_you: "Not you?"
# welcome_back: "Hi adventurer, welcome back!"
# continue_playing: "Continue Playing"
# more_options: "More options:"
# option1_header: "Option 1: Invite students via email"
# option1_body: "Students will automatically be sent an invitation to join this class, and will need to create an account with a username and password."
# option2_header: "Option 2: Send URL to your students"
# option2_body: "Students will be asked to enter an email address, username and password to create an account."
# option3_header: "Option 3: Direct students to codecombat.com/courses"
# option3_body: "Give students the following passcode to enter along with an email address, username and password when they create an account."
# thank_you_pref: "Thank you for your purchase! You can now assign"
# thank_you_suff: "more students to paid courses."
# return_to_class: "Return to classroom"
# return_to_course_man: "Return to course management."
# students_not_enrolled: "students not enrolled"
# total_all_classes: "Total Across All Classes"
# how_many_enrollments: "How many additional paid enrollments do you need?"
# each_student_access: "Each student in a class will get access to Courses 2-4 once they are enrolled in paid courses. You may assign each course to each student individually."
# purchase_now: "Purchase Now"
# enrollments: "enrollments"
# remove_student1: "Remove Student"
# are_you_sure: "Are you sure you want to remove this student from this class?"
# remove_description1: "Student will lose access to this classroom and assigned classes. Progress and gameplay is NOT lost, and the student can be added back to the classroom at any time."
# remove_description2: "The activated paid license will not be returned."
# keep_student: "Keep Student"
# removing_user: "Removing user"
# to_join_ask: "To join a class, ask your teacher for an unlock code."
# join_this_class: "Join Class"
# enter_here: "<enter unlock code here>"
# successfully_joined: "Successfully joined"
# click_to_start: "Click here to start taking"
# my_courses: "My Courses"
# classroom: "Classroom"
# use_school_email: "use your school email if you have one"
# unique_name: "a unique name no one has chosen"
# pick_something: "pick something you can remember"
# class_code: "Class Code"
# optional_ask: "optional - ask your teacher to give you one!"
# optional_school: "optional - what school do you go to?"
# start_playing: "Start Playing"
# skip_this: "Skip this, I'll create an account later!"
# welcome: "Welcome"
# getting_started: "Getting Started with Courses"
# download_getting_started: "Download Getting Started Guide [PDF]"
# getting_started_1: "Create a new class by clicking the green 'Create New Class' button below."
# getting_started_2: "Once you've created a class, click the blue 'Add Students' button."
# getting_started_3: "You'll see student's progress below as they sign up and join your class."
# additional_resources: "Additional Resources"
# additional_resources_1_pref: "Download/print our"
# additional_resources_1_mid: "Course 1 Teacher's Guide"
# additional_resources_1_suff: "explanations and solutions to each level."
# additional_resources_2_pref: "Complete our"
# additional_resources_2_suff: "to get two free enrollments for the rest of our paid courses."
# additional_resources_3_pref: "Visit our"
# additional_resources_3_mid: "Teacher Forums"
# additional_resources_3_suff: "to connect to fellow educators who are using CodeCombat."
# additional_resources_4_pref: "Check out our"
# additional_resources_4_mid: "Schools Page"
# additional_resources_4_suff: "to learn more about CodeCombat's classroom offerings."
# your_classes: "Your Classes"
# no_classes: "No classes yet!"
# create_new_class1: "create new class"
# available_courses: "Available Courses"
# unused_enrollments: "Unused enrollments available:"
# students_access: "All students get access to Introduction to Computer Science for free. One enrollment per student is required to assign them to paid CodeCombat courses. A single student does not need multiple enrollments to access all paid courses."
# active_courses: "active courses"
# no_students: "No students yet!"
# add_students1: "add students"
# view_edit: "view/edit"
# students_enrolled: "students enrolled"
# length: "Length:"
enroll_paid: "招收学生到已付费课程"
you_have1: "你有"
you_have2: "未使用的已付费课程。"
use_one: "使用一个付费名额于"
use_multiple: "为下列学生使用付费名额:"
already_enrolled: "已注册"
licenses_remaining: "证书剩余:"
insufficient_enrollments: "付费名额不足"
enroll_students: "招收学生"
get_enrollments: "获取更多招收名额"
change_language: "修改课程预言"
keep_using: "继续使用"
switch_to: "切换到"
greetings: "欢迎!"
learn_p: "学习Python"
learn_j: "学习JavaScript"
language_cannot_change: "一旦学生加入课程后则不可修改课程语言。"
back_classrooms: "回到我的教室"
back_courses: "回到我的课程"
edit_details: "编辑课程详情"
enrolled_courses: "报名付费课程:"
purchase_enrollments: "购买名额"
remove_student: "删除学生"
assign: "分派"
to_assign: "去分派付费课程。"
teacher: "老师"
complete: "完成"
none: ""
save: "保存"
play_campaign_title: "玩战役模式"
play_campaign_description: "你已经准备开始下一步了!探索数以百计的挑战关卡、学习高级编程技巧,以及在多人竞技场中战斗!"
create_account_title: "创建账号"
create_account_description: "注册免费的CodeCombat账号以获取更多关卡、更多的编程技巧和更多乐趣"
preview_campaign_title: "试玩战役"
preview_campaign_description: "在注册你的免费账号前简单看一下CodeCombat将要提供的所有内容。"
arena: "竞技场"
arena_soon_title: "竞技场马上就来"
arena_soon_description: "我们正在开发一个多人竞技场"
not_enrolled1: "未报名"
not_enrolled2: "通知你的老师把你加入下一课程。"
next_course: "下个课程"
coming_soon1: "马上就来"
coming_soon2: "我们正在努力地为您准备更多课程!"
available_levels: "可用关卡"
welcome_to_courses: "探险者,欢迎来到课程中!"
ready_to_play: "准备玩了吗?"
start_new_game: "开始新游戏"
play_now_learn_header: "开始学习"
play_now_learn_1: "控制你角色的基本语法"
play_now_learn_2: "解决麻烦谜题的while循环"
play_now_learn_3: "自定义动作的字符串和变量"
play_now_learn_4: "如何打败食人魔(重要生存技巧!)"
welcome_to_page: "欢迎来到你的课程页面!"
completed_hoc: "太棒了!你已经完成了编码之时课程!"
ready_for_more_header: "准备玩更多东西了?玩竞技场模式!"
ready_for_more_1: "使用宝石解锁更多物品!"
ready_for_more_2: "玩品牌新世界和挑战"
ready_for_more_3: "学习更多编程!"
saved_games: "已保存游戏"
hoc: "编码之时"
my_classes: "我的课程"
class_added: "成功添加课程!"
view_class: "浏览课程"
view_levels: "浏览关卡"
join_class: "加入课程"
ask_teacher_for_code: "问你的老师如果你有CodeCombat课程码如果是的话在下方输入"
enter_c_code: "<输入课程码>"
join: "加入"
joining: "加入课程中"
course_complete: "课程完成"
play_arena: "玩竞技场"
start: "开始"
last_level: "上一关卡"
welcome_to_hoc: "探险者,欢迎来到我们的编码之时!"
logged_in_as: "登录为:"
not_you: "不是你?"
welcome_back: "探险者,欢迎回来!"
continue_playing: "继续玩"
more_options: "更多选项:"
option1_header: "选项1通过电子邮件邀请学生"
option1_body: "学生会被自动发送一个加入该课程的邀请,他们需要提供用户名和密码以创建账号。"
option2_header: "选项2发送链接给你的学生"
option2_body: "学生会被要求输入一个电子邮箱地址、用户名和密码以创建账号。"
option3_header: "选项3通知学生到codecombat.com/courses"
option3_body: "给学生下列密码以便他们在使用电子邮件地址、用户名和密码创建账号时输入。"
thank_you_pref: "感谢你的购买!现在你可以分派"
thank_you_suff: "更多学生到付费课程中。"
return_to_class: "回到教室"
return_to_course_man: "回到课程管理。"
students_not_enrolled: "未注册的学生"
total_all_classes: "所有课程总数"
how_many_enrollments: "你需要多少额外的付费名额?"
each_student_access: "课程中的学生可以访问课程2-4一旦他们注册了付费课程。你可以为每个学生单独分配任何课程。"
purchase_now: "现在购买"
enrollments: "名额"
remove_student1: "删除学生"
are_you_sure: "你确定要从该课程中删除该学生吗?"
remove_description1: "学生将失去访问该课堂和被分派课程的权利。游戏进度不会丢失,该学生可以在任何时间被添加回该教室。"
remove_description2: "被激活的付费证书将无法返还。"
keep_student: "保留学生"
removing_user: "删除用户"
to_join_ask: "为了加入课程,问你的老师得到一个解锁码。"
join_this_class: "加入课程"
enter_here: "<在这里输入解锁码>"
successfully_joined: "成功加入"
click_to_start: "点击这里开始"
my_courses: "我的课程"
classroom: "教室"
use_school_email: "用你学校的电子邮箱如果你有"
unique_name: "没人使用的唯一名称"
pick_something: "选用你能记住的某些东西"
class_code: "课程码"
optional_ask: "可选 - 让你的老师给你一个!"
optional_school: "可选 - 你想去什么学校?"
start_playing: "开始玩"
skip_this: "跳过,以后我会创建一个账号!"
welcome: "欢迎"
getting_started: "开始课程"
download_getting_started: "下载新手教程[PDF]"
getting_started_1: "点击下面绿色的'创建新课程'按钮来创建新课程。"
getting_started_2: "当你已经创建好了课程,点击蓝色的'添加学生'按钮。"
getting_started_3: "你将会在下面看到学生的进度,当他们注册和加入你的课程。"
additional_resources: "额外资源"
additional_resources_1_pref: "下载/打印我们的"
additional_resources_1_mid: "课程1老师的讲义"
additional_resources_1_suff: "到各个关卡的解释和攻略。"
additional_resources_2_pref: "完成我们的"
additional_resources_2_suff: "以获得两个额外的我们后续付费课程的免费名额。"
additional_resources_3_pref: "访问我们的"
additional_resources_3_mid: "教室论坛"
additional_resources_3_suff: "以连接那些使用CodeCombat的伙伴教育者。"
additional_resources_4_pref: "检出我们的"
additional_resources_4_mid: "学校页面"
additional_resources_4_suff: "以学得更多关于CodeCombat的教室供应。"
your_classes: "你的课程"
no_classes: "还没有任何课程!"
create_new_class1: "创建新课程"
available_courses: "可用课程"
unused_enrollments: "未使用的可用名额:"
students_access: "所有的学生都可以免费访问Introduction to Computer Science。每个学生需要一个名额以被分派到付费的CodeCombat课程。每个学生并不需要多个名额来访问所有的付费课程。"
active_courses: "已激活课程"
no_students: "还没有学生!"
add_students1: "添加学生"
view_edit: "查看/编辑"
students_enrolled: "学生已注册"
length: "长度:"
classes:
archmage_title: "大法师"
@ -1322,11 +1322,11 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
account_prepaid:
purchase_code: "购买订阅码"
# purchase_code1: "Subscription Codes can be redeemed to add premium subscription time to one or more CodeCombat accounts."
# purchase_code2: "Each CodeCombat account can only redeem a particular Subscription Code once."
# purchase_code3: "Subscription Code months will be added to the end of any existing subscription on the account."
purchase_code1: "订阅码可以为一个或多个CodeCombat账号兑换额外的订阅时间。"
purchase_code2: "每个CodeCombat账号每次只能兑换一个订阅码。"
purchase_code3: "订阅码时间会在账号现有的订阅时间基础上延长。"
users: "玩家"
# months: "Months"
months: "月份"
purchase_total: "总共"
purchase_button: "提交购买"
your_codes: "你的订阅码:" # {change}
@ -1354,7 +1354,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
bad_input: "错误输入。"
server_error: "服务器错误。"
unknown: "未知错误。"
# error: "ERROR"
error: "错误"
resources:
sessions: "session"
@ -1406,16 +1406,16 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
campaigns: "任务"
poll: "调查"
user_polls_record: "投票结果"
# course: "Course"
# courses: "Courses"
# course_instance: "Course Instance"
# course_instances: "Course Instances"
# classroom: "Classroom"
# classrooms: "Classrooms"
# clan: "Clan"
# clans: "Clans"
# members: "Members"
# users: "Users"
course: "课程"
courses: "课程"
course_instance: "课程示例"
course_instances: "课程示例"
classroom: "教室"
classrooms: "教室"
clan: "部落"
clans: "部落"
members: "成员"
users: "用户"
concepts:
advanced_strings: "高级字符串"
@ -1481,7 +1481,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
email_settings_url: "您的电子邮件设置"
email_description_suffix: "或者我们发送的邮件中的链接,您可以随时更改您的偏好设置或者随时取消订阅。"
cost_title: "花费"
# cost_description: "CodeCombat is free to play for all of its core levels, with a ${{price}} USD/mo subscription for access to extra level branches and {{gems}} bonus gems per month. You can cancel with a click, and we offer a 100% money-back guarantee."
cost_description: "CodeCombat的所有核心关卡都是免费的,需要${{price}}美元/月的订阅费来访问额外关卡分支并获得{{gems}}宝石奖励每月。你可以通过勾选来取消我们提供100%退款保证。"
copyrights_title: "版权与许可"
contributor_title: "贡献者许可协议"
contributor_description_prefix: "所有对本网站或是GitHub代码库的贡献都依照我们的"
@ -1516,8 +1516,8 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
nutshell_title: "简而言之"
nutshell_description: "我们在关卡编辑器里公开的任何资源您都可以在制作关卡时随意使用但我们保留限制在CodeCombat.com 之上创建的关卡本身传播的权利,因为我们以后可能决定为它们收费。"
canonical: "这篇说明的英文版本是权威版本。如果各个翻译版本之间有任何冲突,请以英文版为准。"
# third_party_title: "Third Party Services"
# third_party_description: "CodeCombat uses the following third party services (among others):"
third_party_title: "第三方服务"
third_party_description: "CodeCombat使用了下列第三方服务(除了别的以外):"
ladder_prizes:
title: "竞标赛奖项" # This section was for an old tournament and doesn't need new translations now.

View file

@ -27,6 +27,12 @@ block content
.kpi-chart.line-chart-container
h3 Active Classes 90 days
.small Active class: 12+ students in a classroom, with 6+ who played in last 30 days.
.small Paid student: user.coursePrepaidID set and prepaid.properties.trialRequestID NOT set
.small Trial student: user.coursePrepaidID set and prepaid.properties.trialRequestID set
.small Paid class: at least one paid student in the classroom
.small Trial class: not paid, at least one trial student in classroom
.small Free class: not paid, not trial
.active-classes-chart.line-chart-container
h3 Recurring Revenue 90 days
@ -56,6 +62,22 @@ block content
else
div Loading ...
h3 School Counts
.small Only including schools with #{view.minSchoolCount}+ counts
if view.schoolCounts
table.table.table-striped.table-condensed
tr
th
th School Name
th User Count
each val, i in view.schoolCounts
tr
td= i + 1
td= val.schoolName
td= val.count
else
div Loading ...
h1 Active Classes
table.table.table-striped.table-condensed
tr

View file

@ -183,9 +183,16 @@ nav.navbar.navbar-default
.request-demo-row.text-center
h3 Curious? Request a demo and we'll show you the ropes
h4 Or create a class and see it for yourself!
p
div
a.btn.btn-primary.btn-lg(href="/teachers/freetrial") Request a Demo
a.btn.btn-primary-alt.btn-lg(href="/courses/teachers") Create a Class
div
if me.isAnonymous()
span.spr Already have an account?
a.login-button Login
else
span.spr You are currently logged in as #{me.broadName()}.
a#logout-button Logout?
h3.text-center Computer science courses for all ages
h4.text-center

View file

@ -1,11 +1,11 @@
button.btn.btn-lg.btn-illustrated.cast-button(title=castVerbose)
button.btn.btn-lg.btn-illustrated.cast-button(title=view.castVerbose())
span(data-i18n="play_level.tome_run_button_ran") Ran
if !observing
if mirror
if !view.observing
if view.mirror
.ladder-submission-view
else
button.btn.btn-lg.btn-illustrated.submit-button(title=castRealTimeVerbose)
button.btn.btn-lg.btn-illustrated.submit-button(title=view.castRealTimeVerbose())
span(data-i18n="play_level.tome_submit_button") Submit
span.spl.secret.submit-again-time

View file

@ -1,8 +1,8 @@
button.close(type="button") &times;
h3.problem-alert-title(data-i18n="play_level.problem_alert_title") Fix Your Code
if hint
span.problem-title!= hint
if view.hint
span.problem-title!= view.hint
br
span.problem-subtitle!= message
span.problem-subtitle!= view.message
else
span.problem-title!= message
span.problem-title!= view.message

View file

@ -196,7 +196,7 @@ block content
.well
p.text-center.blurb-subtitle Resources for Teachers
p
a(href='http://codecombat.com/docs/CodeCombatHourofCodeGettingStartedGuide.pdf')
a(href='http://codecombat.com/docs/CodeCombatCoursesGettingStartedGuide.pdf')
img(src='/images/Adobe_PDF_file_icon_32x32.png')
span Getting Started Guide
p

View file

@ -43,6 +43,8 @@ module.exports = class RequestQuoteView extends RootView
if @trialRequests.size()
@trialRequest = @trialRequests.first()
me.setRole 'teacher'
if @trialRequest and @trialRequest.get('status') isnt 'submitted' and @trialRequest.get('status') isnt 'approved'
window.tracker?.trackEvent 'View Trial Request', category: 'Teachers', label: 'View Trial Request', ['Mixpanel']
super()
onSubmitForm: (e) ->
@ -80,6 +82,7 @@ module.exports = class RequestQuoteView extends RootView
onTrialRequestSubmit: ->
@$('form, #form-submit-success').toggleClass('hide')
window.tracker?.trackEvent 'Submit Trial Request', category: 'Teachers', label: 'Trial Request', ['Mixpanel']
onClickLoginButton: ->
modal = new AuthModal({

View file

@ -12,6 +12,7 @@ module.exports = class AnalyticsView extends RootView
template: template
furthestCourseDayRange: 30
lineColors: ['red', 'blue', 'green', 'purple', 'goldenrod', 'brown', 'darkcyan']
minSchoolCount: 20
constructor: (options) ->
super options
@ -119,6 +120,18 @@ module.exports = class AnalyticsView extends RootView
@render?()
}, 0).load()
@supermodel.addRequestResource('school_counts', {
url: '/db/user/-/school_counts'
method: 'POST'
data: {minCount: @minSchoolCount}
success: (@schoolCounts) =>
@schoolCounts?.sort (a, b) ->
return -1 if a.count > b.count
return 0 if a.count is b.count
1
@render?()
}, 0).load()
@courses = new CocoCollection([], { url: "/db/course", model: Course})
@courses.comparator = "_id"
@listenToOnce @courses, 'sync', @onCoursesSync

View file

@ -32,24 +32,13 @@ module.exports = class CastButtonView extends CocoView
@updateReplayabilityInterval = setInterval @updateReplayability, 1000
@observing = options.session.get('creator') isnt me.id
@loadMirrorSession() if @options.level.get('slug') in ['ace-of-coders', 'elemental-wars']
@mirror = @mirrorSession?
@autoSubmitsToLadder = @options.level.get('slug') in ['wakka-maul']
destroy: ->
clearInterval @updateReplayabilityInterval
super()
getRenderData: (context={}) ->
context = super context
shift = $.i18n.t 'keyboard_shortcuts.shift'
enter = $.i18n.t 'keyboard_shortcuts.enter'
castShortcutVerbose = "#{shift}+#{enter}"
castRealTimeShortcutVerbose = (if @isMac() then 'Cmd' else 'Ctrl') + '+' + castShortcutVerbose
context.castVerbose = castShortcutVerbose + ': ' + $.i18n.t('keyboard_shortcuts.run_code')
context.castRealTimeVerbose = castRealTimeShortcutVerbose + ': ' + $.i18n.t('keyboard_shortcuts.run_real_time')
context.observing = @observing
context.mirror = @mirrorSession?
context
afterRender: ->
super()
@castButton = $('.cast-button', @$el)
@ -66,6 +55,18 @@ module.exports = class CastButtonView extends CocoView
attachTo: (spellView) ->
@$el.detach().prependTo(spellView.toolbarView.$el).show()
castShortcutVerbose: ->
shift = $.i18n.t 'keyboard_shortcuts.shift'
enter = $.i18n.t 'keyboard_shortcuts.enter'
"#{shift}+#{enter}"
castVerbose: ->
@castShortcutVerbose() + ': ' + $.i18n.t('keyboard_shortcuts.run_code')
castRealTimeVerbose: ->
castRealTimeShortcutVerbose = (if @isMac() then 'Cmd' else 'Ctrl') + '+' + @castShortcutVerbose()
castRealTimeShortcutVerbose + ': ' + $.i18n.t('keyboard_shortcuts.run_real_time')
onCastButtonClick: (e) ->
Backbone.Mediator.publish 'tome:manual-cast', {}

View file

@ -36,8 +36,14 @@ module.exports = class ProblemAlertView extends CocoView
$(window).off 'resize', @onWindowResize
super()
getRenderData: (context={}) ->
context = super context
afterRender: ->
super()
if @problem?
@$el.addClass('alert').addClass("alert-#{@problem.aetherProblem.level}").hide().fadeIn('slow')
@$el.addClass('no-hint') unless @problem.aetherProblem.hint
@playSound 'error_appear'
setProblemMessage: ->
if @problem?
format = (s) -> marked(s.replace(/</g, '&lt;').replace(/>/g, '&gt;')) if s?
message = @problem.aetherProblem.message
@ -50,16 +56,8 @@ module.exports = class ProblemAlertView extends CocoView
message = message.replace /^(Line \d+)/, "$1, time #{age.toFixed(1)}"
else
message = "Time #{age.toFixed(1)}: #{message}"
context.message = format message
context.hint = format @problem.aetherProblem.hint
context
afterRender: ->
super()
if @problem?
@$el.addClass('alert').addClass("alert-#{@problem.aetherProblem.level}").hide().fadeIn('slow')
@$el.addClass('no-hint') unless @problem.aetherProblem.hint
@playSound 'error_appear'
@message = format message
@hint = format @problem.aetherProblem.hint
onShowProblemAlert: (data) ->
return unless $('#code-area').is(":visible")
@ -72,6 +70,7 @@ module.exports = class ProblemAlertView extends CocoView
@lineOffsetPx = data.lineOffsetPx or 0
@$el.show()
@onWindowResize()
@setProblemMessage()
@render()
@onJiggleProblemAlert()
application.tracker?.trackEvent 'Show problem alert', {levelID: @level.get('slug'), ls: @session?.get('_id')}

View file

@ -1,31 +1,42 @@
/* global db */
/* global Mongo */
/* global ISODate */
// Insert per-day active class counts into analytics.perdays collection
// Usage:
// mongo <address>:<port>/<database> <script file> -u <username> -p <password>
try {
logDB = new Mongo("localhost").getDB("analytics")
var logDB = new Mongo("localhost").getDB("analytics")
var scriptStartTime = new Date();
var analyticsStringCache = {};
var minClassSize = 12;
var minActiveCount = 6;
var eventNamePaid = 'Active classes paid';
var eventNameTrial = 'Active classes trial';
var eventNameFree = 'Active classes free';
var numDays = 40;
var daysInMonth = 30;
var startDay = new Date();
today = startDay.toISOString().substr(0, 10);
var today = startDay.toISOString().substr(0, 10);
startDay.setUTCDate(startDay.getUTCDate() - numDays);
startDay = startDay.toISOString().substr(0, 10);
log("Today is " + today);
log("Start day is " + startDay);
log("Getting active class counts...");
log("Getting active class counts..");
var activeClassCounts = getActiveClassCounts(startDay);
// printjson(activeClassCounts);
log("Inserting active class counts...");
log("Inserting active class counts..");
for (var event in activeClassCounts) {
for (var day in activeClassCounts[event]) {
if (today === day) continue; // Never save data for today because it's incomplete
// print(event, day, activeClassCounts[event][day]);
insertEventCount(event, day, activeClassCounts[event][day]);
}
}
@ -38,130 +49,73 @@ catch(err) {
}
function getActiveClassCounts(startDay) {
// Tally active classes per day
// Tally active classes per day, for paid, trial, and free
// TODO: does not handle class membership changes
if (!startDay) return {};
var minGroupSize = 12;
var classes = {
'Active classes private clan': [],
'Active classes managed subscription': [],
'Active classes bulk subscription': [],
'Active classes prepaid': [],
'Active classes course free': [],
'Active classes course paid': []
};
var userPlayedMap = {};
// Private clans
// TODO: does not handle clan membership changes over time
var cursor = db.clans.find({$and: [{type: 'private'}, {$where: 'this.members.length >= ' + minGroupSize}]});
while (cursor.hasNext()) {
var doc = cursor.next();
var members = doc.members.map(function(a) {
userPlayedMap[a.valueOf()] = [];
return a.valueOf();
});
classes['Active classes private clan'].push({
owner: doc.ownerID.valueOf(),
members: members,
activeDayMap: {}
});
}
// Managed subscriptions
// TODO: does not handle former recipients playing after sponsorship ends
var bulkSubGroups = {};
cursor = db.payments.find({$and: [{service: 'stripe'}, {$where: '!this.purchaser.equals(this.recipient)'}]});
while (cursor.hasNext()) {
var doc = cursor.next();
var purchaser = doc.purchaser.valueOf();
if (!bulkSubGroups[purchaser]) bulkSubGroups[purchaser] = {};
bulkSubGroups[purchaser][doc.recipient.valueOf()] = true;
}
for (var purchaser in bulkSubGroups) {
if (Object.keys(bulkSubGroups[purchaser]).length >= minGroupSize) {
for (var member in bulkSubGroups[purchaser]) {
userPlayedMap[member] = [];
}
classes['Active classes managed subscription'].push({
owner: purchaser,
members: Object.keys(bulkSubGroups[purchaser]),
activeDayMap: {}
});
}
}
// Bulk subscriptions
bulkSubGroups = {};
cursor = db.payments.find({$and: [{service: 'external'}, {$where: '!this.purchaser.equals(this.recipient)'}]});
while (cursor.hasNext()) {
var doc = cursor.next();
var purchaser = doc.purchaser.valueOf();
if (!bulkSubGroups[purchaser]) bulkSubGroups[purchaser] = {};
bulkSubGroups[purchaser][doc.recipient.valueOf()] = true;
}
for (var purchaser in bulkSubGroups) {
if (Object.keys(bulkSubGroups[purchaser]).length >= minGroupSize) {
for (var member in bulkSubGroups[purchaser]) {
userPlayedMap[member] = [];
}
classes['Active classes bulk subscription'].push({
owner: purchaser,
members: Object.keys(bulkSubGroups[purchaser]),
activeDayMap: {}
});
}
}
// Prepaids terminal_subscription
bulkSubGroups = {};
cursor = db.prepaids.find(
{$and: [{type: 'terminal_subscription'}, {$where: 'this.redeemers && this.redeemers.length >= ' + minGroupSize}]},
{creator: 1, type: 1, redeemers: 1}
);
while (cursor.hasNext()) {
var doc = cursor.next();
var owner = doc.creator.valueOf();
var members = [];
for (var i = 0 ; i < doc.redeemers.length; i++) {
userPlayedMap[doc.redeemers[i].userID.valueOf()] = [];
members.push(doc.redeemers[i].userID.valueOf());
}
classes['Active classes prepaid'].push({
owner: owner,
members: members,
activeDayMap: {}
});
}
var cursor, doc;
// Classrooms
var classroomCourseInstancesMap = {};
cursor = db.course.instances.find(
{$where: 'this.members && this.members.length >= ' + minGroupSize},
{classroomID: 1, courseID: 1, members: 1, ownerID: 1}
);
// paid: at least one paid member
// trial: not paid, at least one trial member
// free: not paid, not free trial
// user.coursePrepaidID set means access to paid courses
// prepaid.properties.trialRequestID means access was via trial
// Find classroom users
log("Finding classrooms..");
var userClassroomsMap = {};
var classroomUsersMap = {};
var classroomUserIDs = [];
var classroomUserObjectIds = [];
cursor = db.classrooms.find({}, {members: 1});
while (cursor.hasNext()) {
var doc = cursor.next();
var owner = doc.ownerID.valueOf();
var classroom = doc.classroomID ? doc.classroomID.valueOf() : doc._id.valueOf();
var members = [];
doc = cursor.next();
if (doc.members) {
var classroomID = doc._id.valueOf();
for (var i = 0; i < doc.members.length; i++) {
userPlayedMap[doc.members[i].valueOf()] = [];
members.push(doc.members[i].valueOf());
if (doc.members.length < minClassSize) continue;
var userID = doc.members[i].valueOf();
if (!userClassroomsMap[userID]) userClassroomsMap[userID] = [];
userClassroomsMap[userID].push(classroomID);
if (!classroomUsersMap[classroomID]) classroomUsersMap[classroomID] = [];
classroomUsersMap[classroomID].push(userID)
classroomUserIDs.push(doc.members[i].valueOf());
classroomUserObjectIds.push(doc.members[i]);
}
}
if (!classroomCourseInstancesMap[classroom]) classroomCourseInstancesMap[classroom] = [];
classroomCourseInstancesMap[classroom].push({
course: doc.courseID.valueOf(),
owner: owner,
members: members,
});
}
// printjson(classroomCourseInstancesMap);
log("Find user types..");
var userEventMap = {};
var prepaidUsersMap = {};
var prepaidIDs = [];
cursor = db.users.find({_id: {$in: classroomUserObjectIds}}, {coursePrepaidID: 1});
while (cursor.hasNext()) {
doc = cursor.next();
if (doc.coursePrepaidID) {
userEventMap[doc._id.valueOf()] = eventNamePaid;
if (!prepaidUsersMap[doc.coursePrepaidID.valueOf()]) prepaidUsersMap[doc.coursePrepaidID.valueOf()] = [];
prepaidUsersMap[doc.coursePrepaidID.valueOf()].push(doc._id.valueOf());
prepaidIDs.push(doc.coursePrepaidID);
}
else {
userEventMap[doc._id.valueOf()] = eventNameFree;
}
}
cursor = db.prepaids.find({_id: {$in: prepaidIDs}}, {properties: 1});
while (cursor.hasNext()) {
doc = cursor.next();
if (doc.properties && doc.properties.trialRequestID) {
for (var i = 0; i < prepaidUsersMap[doc._id.valueOf()].length; i++) {
userEventMap[prepaidUsersMap[doc._id.valueOf()][i]] = eventNameTrial;
}
}
}
// Find all the started level events for our class members, for startDay - daysInMonth
log("Find Started Level log events for all classroom members for last " + (numDays + daysInMonth) + " days..");
var userPlayedMap = {};
var startDate = ISODate(startDay + "T00:00:00.000Z");
startDate.setUTCDate(startDate.getUTCDate() - daysInMonth);
var endDate = ISODate(startDay + "T00:00:00.000Z");
@ -169,162 +123,72 @@ function getActiveClassCounts(startDay) {
var startObj = objectIdWithTimestamp(startDate);
var queryParams = {$and: [
{_id: {$gte: startObj}},
{user: {$in: Object.keys(userPlayedMap)}},
{user: {$in: classroomUserIDs}},
{event: 'Started Level'}
]};
cursor = logDB['log'].find(queryParams, {user: 1});
while (cursor.hasNext()) {
var doc = cursor.next();
doc = cursor.next();
if (!userPlayedMap[doc.user]) userPlayedMap[doc.user] = [];
userPlayedMap[doc.user].push(doc._id.getTimestamp());
}
// printjson(userPlayedMap);
// print(startDate, endDate, todayDate);
// Now we have a set of classes, and when users played
// For a given day, walk classes and find out how many members were active during the previous daysInMonth
while (endDate < todayDate) {
var endDay = endDate.toISOString().substring(0, 10);
// For each class
for (var event in classes) {
for (var i = 0; i < classes[event].length; i++) {
// For each member of current class
var activeMemberCount = 0;
for (var j = 0; j < classes[event][i].members.length; j++) {
var member = classes[event][i].members[j];
// Was member active during current timeframe?
if (userPlayedMap[member]) {
for (var k = 0; k < userPlayedMap[member].length; k++) {
if (userPlayedMap[member][k] > startDate && userPlayedMap[member][k] <= endDate) {
activeMemberCount++;
break;
}
}
}
}
// Classes active for a given day if has minGroupSize members, and at least 1/2 played in last daysInMonth days
if (activeMemberCount >= Math.round(classes[event][i].members.length / 2)) {
classes[event][i].activeDayMap[endDay] = true;
}
}
}
startDate.setUTCDate(startDate.getUTCDate() + 1);
endDate.setUTCDate(endDate.getUTCDate() + 1);
}
// Classrooms are processed differently because they could be free or paid active classes
var courseNameMap = {};
cursor = db.courses.find({}, {name: 1});
while (cursor.hasNext()) {
var doc = cursor.next();
courseNameMap[doc._id.valueOf()] = doc.name;
}
// For each classroom, check free and paid members separately
for (var classroom in classroomCourseInstancesMap) {
var freeMembers = {};
var paidMembers = {};
var owner = null;
for (var i = 0; i < classroomCourseInstancesMap[classroom].length; i++) {
var courseInstance = classroomCourseInstancesMap[classroom][i];
if (!owner) owner = courseInstance.owner;
for (var j = 0; j < courseInstance.members.length; j++) {
if (courseNameMap[courseInstance.course] === 'Introduction to Computer Science') {
freeMembers[courseInstance.members[j]] = true;
}
else {
paidMembers[courseInstance.members[j]] = true;
}
}
}
var freeClass = {
owner: owner,
members: Object.keys(freeMembers),
activeDayMap: {}
};
var paidClass = {
owner: owner,
members: Object.keys(paidMembers),
activeDayMap: {}
};
// print('Processing classroom', classroom, freeClass.members.length, paidClass.members.length);
log("Calculate number of active members per classroom per day per event type..");
var classDayTypeMap = {};
for (var classroom in classroomUsersMap) {
if (classroomUsersMap[classroom].length < minClassSize) continue;
// For each each day in our target date range
classDayTypeMap[classroom] = {};
startDate = ISODate(startDay + "T00:00:00.000Z");
startDate.setUTCDate(startDate.getUTCDate() - daysInMonth);
endDate = ISODate(startDay + "T00:00:00.000Z");
while (endDate < todayDate) {
var endDay = endDate.toISOString().substring(0, 10);
classDayTypeMap[classroom][endDay] = {};
classDayTypeMap[classroom][endDay][eventNamePaid] = 0;
classDayTypeMap[classroom][endDay][eventNameTrial] = 0;
classDayTypeMap[classroom][endDay][eventNameFree] = 0;
// For each paid member of current class
var paidActiveMemberCount = 0;
for (var j = 0; j < paidClass.members.length; j++) {
var member = paidClass.members[j];
// Count active users of each type for current day
for (var j = 0; j < classroomUsersMap[classroom].length; j++) {
var member = classroomUsersMap[classroom][j];
// Was member active during current timeframe?
if (userPlayedMap[member]) {
for (var k = 0; k < userPlayedMap[member].length; k++) {
if (userPlayedMap[member][k] > startDate && userPlayedMap[member][k] <= endDate) {
paidActiveMemberCount++;
classDayTypeMap[classroom][endDay][userEventMap[member]]++;
break;
}
}
}
}
// Classes active for a given day if has minGroupSize members, and at least 1/2 played in last daysInMonth days
if (paidClass.members.length > minGroupSize && paidActiveMemberCount >= Math.round(paidClass.members.length / 2)) {
// print('paid classroom', classroom, endDay);
paidClass.activeDayMap[endDay] = true;
}
else {
// For each free member of current class
var freeActiveMemberCount = 0;
for (var j = 0; j < freeClass.members.length; j++) {
var member = freeClass.members[j];
// Was member active during current timeframe?
if (userPlayedMap[member]) {
for (var k = 0; k < userPlayedMap[member].length; k++) {
if (userPlayedMap[member][k] > startDate && userPlayedMap[member][k] <= endDate) {
freeActiveMemberCount++;
break;
}
}
}
}
if (freeClass.members.length > minGroupSize && freeActiveMemberCount >= Math.round(freeClass.members.length / 2)) {
// print('free classroom', classroom, endDay);
freeClass.activeDayMap[endDay] = true;
}
}
startDate.setUTCDate(startDate.getUTCDate() + 1);
endDate.setUTCDate(endDate.getUTCDate() + 1);
}
// printjson(freeClass);
// printjson(paidClass);
classes['Active classes course free'].push(freeClass);
classes['Active classes course paid'].push(paidClass);
}
// printjson(classes['Active classes course paid']);
log("Aggregate class counts by day and type..");
var activeClassCounts = {};
for (var event in classes) {
if (!activeClassCounts[event]) activeClassCounts[event] = {};
for (var i = 0; i < classes[event].length; i++) {
for (var endDay in classes[event][i].activeDayMap) {
if (!activeClassCounts[event][endDay]) activeClassCounts[event][endDay] = 0;
activeClassCounts[event][endDay]++;
for (var classroom in classDayTypeMap) {
for (var endDay in classDayTypeMap[classroom]) {
var activeStudents = 0;
var classEvent = eventNameFree;
for (var event in classDayTypeMap[classroom][endDay]) {
if (classDayTypeMap[classroom][endDay][event] > 1) {
activeStudents += classDayTypeMap[classroom][endDay][event];
if (event === eventNamePaid) classEvent = event;
if (classEvent !== eventNamePaid && event === eventNameTrial) classEvent = event;
}
}
if (activeStudents >= minActiveCount) {
if (!activeClassCounts[classEvent]) activeClassCounts[classEvent] = {};
if (!activeClassCounts[classEvent][endDay]) activeClassCounts[classEvent][endDay] = 0;
activeClassCounts[classEvent][endDay]++;
}
}
}
@ -334,17 +198,6 @@ function getActiveClassCounts(startDay) {
// *** Helper functions ***
function slugify(text)
// https://gist.github.com/mathewbyrne/1280286
{
return text.toString().toLowerCase()
.replace(/\s+/g, '-') // Replace spaces with -
.replace(/[^\w\-]+/g, '') // Remove all non-word chars
.replace(/\-\-+/g, '-') // Replace multiple - with single -
.replace(/^-+/, '') // Trim - from start of text
.replace(/-+$/, ''); // Trim - from end of text
}
function log(str) {
print(new Date().toISOString() + " " + str);
}

View file

@ -26,12 +26,9 @@ class AnalyticsPerDayHandler extends Handler
getActiveClasses: (req, res) ->
events = [
'Active classes private clan',
'Active classes managed subscription',
'Active classes bulk subscription',
'Active classes prepaid',
'Active classes course free',
'Active classes course paid'
'Active classes paid',
'Active classes trial',
'Active classes free'
]
AnalyticsString.find({v: {$in: events}}).exec (err, documents) =>

View file

@ -309,6 +309,7 @@ UserHandler = class UserHandler extends Handler
return @getByIDs(req, res) if args[1] is 'users'
return @getNamesByIDs(req, res) if args[1] is 'names'
return @getPrepaidCodes(req, res) if args[1] is 'prepaid_codes'
return @getSchoolCounts(req, res) if args[1] is 'school_counts'
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'
@ -464,6 +465,27 @@ UserHandler = class UserHandler extends Handler
Prepaid.find({}).or(orQuery).exec (err, documents) =>
@sendSuccess(res, documents)
getSchoolCounts: (req, res) ->
return @sendSuccess(res, []) unless req.user?.isAdmin()
minCount = req.body.minCount ? 20
query = {$and: [
{anonymous: false},
{schoolName: {$exists: true}},
{schoolName: {$ne: ''}}
]}
User.find(query, {schoolName: 1}).exec (err, documents) =>
return @sendDatabaseError(res, err) if err
schoolCountMap = {}
for doc in documents
schoolName = doc.get('schoolName')
schoolCountMap[schoolName] ?= 0;
schoolCountMap[schoolName]++;
schoolCounts = []
for schoolName, count of schoolCountMap
continue unless count >= minCount
schoolCounts.push schoolName: schoolName, count: count
@sendSuccess(res, schoolCounts)
agreeToCLA: (req, res) ->
return @sendForbiddenError(res) unless req.user
doc =