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

View file

@ -183,9 +183,16 @@ nav.navbar.navbar-default
.request-demo-row.text-center .request-demo-row.text-center
h3 Curious? Request a demo and we'll show you the ropes h3 Curious? Request a demo and we'll show you the ropes
h4 Or create a class and see it for yourself! 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.btn-lg(href="/teachers/freetrial") Request a Demo
a.btn.btn-primary-alt.btn-lg(href="/courses/teachers") Create a Class 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 h3.text-center Computer science courses for all ages
h4.text-center 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 span(data-i18n="play_level.tome_run_button_ran") Ran
if !observing if !view.observing
if mirror if view.mirror
.ladder-submission-view .ladder-submission-view
else 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(data-i18n="play_level.tome_submit_button") Submit
span.spl.secret.submit-again-time span.spl.secret.submit-again-time

View file

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

View file

@ -196,7 +196,7 @@ block content
.well .well
p.text-center.blurb-subtitle Resources for Teachers p.text-center.blurb-subtitle Resources for Teachers
p 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') img(src='/images/Adobe_PDF_file_icon_32x32.png')
span Getting Started Guide span Getting Started Guide
p p

View file

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

View file

@ -12,6 +12,7 @@ module.exports = class AnalyticsView extends RootView
template: template template: template
furthestCourseDayRange: 30 furthestCourseDayRange: 30
lineColors: ['red', 'blue', 'green', 'purple', 'goldenrod', 'brown', 'darkcyan'] lineColors: ['red', 'blue', 'green', 'purple', 'goldenrod', 'brown', 'darkcyan']
minSchoolCount: 20
constructor: (options) -> constructor: (options) ->
super options super options
@ -119,6 +120,18 @@ module.exports = class AnalyticsView extends RootView
@render?() @render?()
}, 0).load() }, 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 = new CocoCollection([], { url: "/db/course", model: Course})
@courses.comparator = "_id" @courses.comparator = "_id"
@listenToOnce @courses, 'sync', @onCoursesSync @listenToOnce @courses, 'sync', @onCoursesSync

View file

@ -32,24 +32,13 @@ module.exports = class CastButtonView extends CocoView
@updateReplayabilityInterval = setInterval @updateReplayability, 1000 @updateReplayabilityInterval = setInterval @updateReplayability, 1000
@observing = options.session.get('creator') isnt me.id @observing = options.session.get('creator') isnt me.id
@loadMirrorSession() if @options.level.get('slug') in ['ace-of-coders', 'elemental-wars'] @loadMirrorSession() if @options.level.get('slug') in ['ace-of-coders', 'elemental-wars']
@mirror = @mirrorSession?
@autoSubmitsToLadder = @options.level.get('slug') in ['wakka-maul'] @autoSubmitsToLadder = @options.level.get('slug') in ['wakka-maul']
destroy: -> destroy: ->
clearInterval @updateReplayabilityInterval clearInterval @updateReplayabilityInterval
super() 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: -> afterRender: ->
super() super()
@castButton = $('.cast-button', @$el) @castButton = $('.cast-button', @$el)
@ -66,6 +55,18 @@ module.exports = class CastButtonView extends CocoView
attachTo: (spellView) -> attachTo: (spellView) ->
@$el.detach().prependTo(spellView.toolbarView.$el).show() @$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) -> onCastButtonClick: (e) ->
Backbone.Mediator.publish 'tome:manual-cast', {} Backbone.Mediator.publish 'tome:manual-cast', {}

View file

@ -36,8 +36,14 @@ module.exports = class ProblemAlertView extends CocoView
$(window).off 'resize', @onWindowResize $(window).off 'resize', @onWindowResize
super() super()
getRenderData: (context={}) -> afterRender: ->
context = super context 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? if @problem?
format = (s) -> marked(s.replace(/</g, '&lt;').replace(/>/g, '&gt;')) if s? format = (s) -> marked(s.replace(/</g, '&lt;').replace(/>/g, '&gt;')) if s?
message = @problem.aetherProblem.message message = @problem.aetherProblem.message
@ -50,16 +56,8 @@ module.exports = class ProblemAlertView extends CocoView
message = message.replace /^(Line \d+)/, "$1, time #{age.toFixed(1)}" message = message.replace /^(Line \d+)/, "$1, time #{age.toFixed(1)}"
else else
message = "Time #{age.toFixed(1)}: #{message}" message = "Time #{age.toFixed(1)}: #{message}"
context.message = format message @message = format message
context.hint = format @problem.aetherProblem.hint @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'
onShowProblemAlert: (data) -> onShowProblemAlert: (data) ->
return unless $('#code-area').is(":visible") return unless $('#code-area').is(":visible")
@ -72,6 +70,7 @@ module.exports = class ProblemAlertView extends CocoView
@lineOffsetPx = data.lineOffsetPx or 0 @lineOffsetPx = data.lineOffsetPx or 0
@$el.show() @$el.show()
@onWindowResize() @onWindowResize()
@setProblemMessage()
@render() @render()
@onJiggleProblemAlert() @onJiggleProblemAlert()
application.tracker?.trackEvent 'Show problem alert', {levelID: @level.get('slug'), ls: @session?.get('_id')} 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 // Insert per-day active class counts into analytics.perdays collection
// Usage: // Usage:
// mongo <address>:<port>/<database> <script file> -u <username> -p <password> // mongo <address>:<port>/<database> <script file> -u <username> -p <password>
try { try {
logDB = new Mongo("localhost").getDB("analytics") var logDB = new Mongo("localhost").getDB("analytics")
var scriptStartTime = new Date(); var scriptStartTime = new Date();
var analyticsStringCache = {}; 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 numDays = 40;
var daysInMonth = 30; var daysInMonth = 30;
var startDay = new Date(); var startDay = new Date();
today = startDay.toISOString().substr(0, 10); var today = startDay.toISOString().substr(0, 10);
startDay.setUTCDate(startDay.getUTCDate() - numDays); startDay.setUTCDate(startDay.getUTCDate() - numDays);
startDay = startDay.toISOString().substr(0, 10); startDay = startDay.toISOString().substr(0, 10);
log("Today is " + today); log("Today is " + today);
log("Start day is " + startDay); log("Start day is " + startDay);
log("Getting active class counts..."); log("Getting active class counts..");
var activeClassCounts = getActiveClassCounts(startDay); var activeClassCounts = getActiveClassCounts(startDay);
// printjson(activeClassCounts); // printjson(activeClassCounts);
log("Inserting active class counts..."); log("Inserting active class counts..");
for (var event in activeClassCounts) { for (var event in activeClassCounts) {
for (var day in activeClassCounts[event]) { for (var day in activeClassCounts[event]) {
if (today === day) continue; // Never save data for today because it's incomplete 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]); insertEventCount(event, day, activeClassCounts[event][day]);
} }
} }
@ -38,130 +49,73 @@ catch(err) {
} }
function getActiveClassCounts(startDay) { 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 // TODO: does not handle class membership changes
if (!startDay) return {}; if (!startDay) return {};
var minGroupSize = 12; var cursor, doc;
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: {}
});
}
// Classrooms // Classrooms
var classroomCourseInstancesMap = {}; // paid: at least one paid member
cursor = db.course.instances.find( // trial: not paid, at least one trial member
{$where: 'this.members && this.members.length >= ' + minGroupSize}, // free: not paid, not free trial
{classroomID: 1, courseID: 1, members: 1, ownerID: 1} // 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()) { while (cursor.hasNext()) {
var doc = cursor.next(); doc = cursor.next();
var owner = doc.ownerID.valueOf(); if (doc.members) {
var classroom = doc.classroomID ? doc.classroomID.valueOf() : doc._id.valueOf(); var classroomID = doc._id.valueOf();
var members = []; for (var i = 0; i < doc.members.length; i++) {
for (var i = 0 ; i < doc.members.length; i++) { if (doc.members.length < minClassSize) continue;
userPlayedMap[doc.members[i].valueOf()] = []; var userID = doc.members[i].valueOf();
members.push(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"); var startDate = ISODate(startDay + "T00:00:00.000Z");
startDate.setUTCDate(startDate.getUTCDate() - daysInMonth); startDate.setUTCDate(startDate.getUTCDate() - daysInMonth);
var endDate = ISODate(startDay + "T00:00:00.000Z"); var endDate = ISODate(startDay + "T00:00:00.000Z");
@ -169,162 +123,72 @@ function getActiveClassCounts(startDay) {
var startObj = objectIdWithTimestamp(startDate); var startObj = objectIdWithTimestamp(startDate);
var queryParams = {$and: [ var queryParams = {$and: [
{_id: {$gte: startObj}}, {_id: {$gte: startObj}},
{user: {$in: Object.keys(userPlayedMap)}}, {user: {$in: classroomUserIDs}},
{event: 'Started Level'} {event: 'Started Level'}
]}; ]};
cursor = logDB['log'].find(queryParams, {user: 1}); cursor = logDB['log'].find(queryParams, {user: 1});
while (cursor.hasNext()) { while (cursor.hasNext()) {
var doc = cursor.next(); doc = cursor.next();
if (!userPlayedMap[doc.user]) userPlayedMap[doc.user] = [];
userPlayedMap[doc.user].push(doc._id.getTimestamp()); userPlayedMap[doc.user].push(doc._id.getTimestamp());
} }
// printjson(userPlayedMap); // 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 log("Calculate number of active members per classroom per day per event type..");
for (var event in classes) { var classDayTypeMap = {};
for (var i = 0; i < classes[event].length; i++) { for (var classroom in classroomUsersMap) {
if (classroomUsersMap[classroom].length < minClassSize) continue;
// 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);
// For each each day in our target date range
classDayTypeMap[classroom] = {};
startDate = ISODate(startDay + "T00:00:00.000Z"); startDate = ISODate(startDay + "T00:00:00.000Z");
startDate.setUTCDate(startDate.getUTCDate() - daysInMonth); startDate.setUTCDate(startDate.getUTCDate() - daysInMonth);
endDate = ISODate(startDay + "T00:00:00.000Z"); endDate = ISODate(startDay + "T00:00:00.000Z");
while (endDate < todayDate) { while (endDate < todayDate) {
var endDay = endDate.toISOString().substring(0, 10); 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 // Count active users of each type for current day
var paidActiveMemberCount = 0; for (var j = 0; j < classroomUsersMap[classroom].length; j++) {
for (var j = 0; j < paidClass.members.length; j++) { var member = classroomUsersMap[classroom][j];
var member = paidClass.members[j];
// Was member active during current timeframe? // Was member active during current timeframe?
if (userPlayedMap[member]) { if (userPlayedMap[member]) {
for (var k = 0; k < userPlayedMap[member].length; k++) { for (var k = 0; k < userPlayedMap[member].length; k++) {
if (userPlayedMap[member][k] > startDate && userPlayedMap[member][k] <= endDate) { if (userPlayedMap[member][k] > startDate && userPlayedMap[member][k] <= endDate) {
paidActiveMemberCount++; classDayTypeMap[classroom][endDay][userEventMap[member]]++;
break; 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); startDate.setUTCDate(startDate.getUTCDate() + 1);
endDate.setUTCDate(endDate.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 = {}; var activeClassCounts = {};
for (var event in classes) { for (var classroom in classDayTypeMap) {
if (!activeClassCounts[event]) activeClassCounts[event] = {}; for (var endDay in classDayTypeMap[classroom]) {
for (var i = 0; i < classes[event].length; i++) { var activeStudents = 0;
for (var endDay in classes[event][i].activeDayMap) { var classEvent = eventNameFree;
if (!activeClassCounts[event][endDay]) activeClassCounts[event][endDay] = 0; for (var event in classDayTypeMap[classroom][endDay]) {
activeClassCounts[event][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 *** // *** 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) { function log(str) {
print(new Date().toISOString() + " " + str); print(new Date().toISOString() + " " + str);
} }

View file

@ -26,12 +26,9 @@ class AnalyticsPerDayHandler extends Handler
getActiveClasses: (req, res) -> getActiveClasses: (req, res) ->
events = [ events = [
'Active classes private clan', 'Active classes paid',
'Active classes managed subscription', 'Active classes trial',
'Active classes bulk subscription', 'Active classes free'
'Active classes prepaid',
'Active classes course free',
'Active classes course paid'
] ]
AnalyticsString.find({v: {$in: events}}).exec (err, documents) => 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 @getByIDs(req, res) if args[1] is 'users'
return @getNamesByIDs(req, res) if args[1] is 'names' return @getNamesByIDs(req, res) if args[1] is 'names'
return @getPrepaidCodes(req, res) if args[1] is 'prepaid_codes' 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 @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 @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 @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) => Prepaid.find({}).or(orQuery).exec (err, documents) =>
@sendSuccess(res, 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) -> agreeToCLA: (req, res) ->
return @sendForbiddenError(res) unless req.user return @sendForbiddenError(res) unless req.user
doc = doc =