update rail copy

update rail copy

--- apps/artwork/client/setup_artwork_rails.coffee
+++ apps/artwork/client/setup_artwork_rails.coffee
@@ -12,19 +12,19 @@
     title: "Similar Artworks from #{artwork.artist?.name}"
   partner_artworks:
     url: "/#{artwork.partner?.default_profile_id}/works"
-    title: "More Works From #{artwork.partner?.name}"
+    title: "More Works by #{artwork.partner?.name}"
   artist_artworks:
     url: "/artist/#{artwork.artist.id}/works"
-    title: "More Works From #{artwork.artist?.name}"
+    title: "More Works by #{artwork.artist?.name}"
   show_artworks:
     url: "/show/#{artwork.shows[0]?.id}"
-    title: "More Works From #{artwork.shows[0]?.name}"
+    title: "More Works from #{artwork.shows[0]?.name}"
   current_auction_artworks:
     url: "/auction/#{artwork.related?.id}"
-    title: "More Works From #{artwork.related?.name}"
+    title: "More Works from #{artwork.related?.name}"
   closed_auction_artworks:
     url: "/auction/#{artwork.related?.id}"
-    title: "More Works From #{artwork.related?.name}"
+    title: "More Works from #{artwork.related?.name}"

 module.exports = (artwork, artist) ->
   new LayeredSearchView
@@ -67,6 +67,7 @@
           view.carouselPromise

         Q.all([carouselPromises]).then ->
-          $('#artwork-rails').attr 'data-state', 'fade-in'
+          _.defer =>
+            $('#artwork-rails').attr 'data-state', 'fade-in'


Edited the schema

Edited the schema

--- migrations/20141105235711_initial.coffee
+++ migrations/20141105235711_initial.coffee
@@ -4,7 +4,7 @@
       t.increments()
       t.dateTime 'started_at'
       t.dateTime 'ended_at'
-      t.timestamps()
+      t.string 'topic'

     knex.schema.createTable 'word_submissions', (t)->
       t.increments()


Update zh-HANS.coffee

Update zh-HANS.coffee

--- app/locale/zh-HANS.coffee
+++ app/locale/zh-HANS.coffee
@@ -4,12 +4,12 @@
     classroom_edition: "教育版："
     learn_to_code: "学习编程："
     play_now: "开始游戏"
-    im_a_teacher: "我是教师"
+    im_a_teacher: "我是老师"
     im_a_student: "我是学生"
     learn_more: "了解更多"
     classroom_in_a_box: "教授计算机科学的一站式课堂。"
     codecombat_is: "CodeCombat是一个让学生通过玩游戏学习计算机科学的平台。"
-    our_courses: "我们的游戏编程课程尤其适用于教室教学的需求。无编程经验的教师也可轻松授课。"
+    our_courses: "我们的游戏编程课程尤其适用于教室教学的需求。无编程经验的老师也可轻松授课。"
     watch_how: "看一看CodeCombat是怎样转变人们学习计算机的方式。"
     top_screenshots_hint: "学生编写代码后可即时查看改动。"
     designed_with: "从教育者角度设计"
@@ -47,7 +47,7 @@
     get_started_subtitle: "配置班级，添加学生，观察他们学习计算机科学的进度。"
     request_demo: "申请课程演示"
     setup_a_class: "配置班级"
-    have_an_account: "已有帐号？"
+    have_an_account: "已有账号？"
     logged_in_as: "您现在的登录身份："
     computer_science: "适合全年龄段的计算机科学课程" # {change}
     show_me_lesson_time: "显示估计课程时间："
@@ -82,7 +82,7 @@
     about: "关于"
     contact: "联系我们"
     twitter_follow: "关注"
-    my_classrooms: "我的教室" # {change}
+    my_classrooms: "我的班级" # {change}
     my_courses: "我的课程"
     careers: "招聘"
     facebook: "Facebook"
@@ -230,7 +230,7 @@

   signup:
     create_student_header: "创建学生账号"
-    create_teacher_header: "创建教师账号"
+    create_teacher_header: "创建老师账号"
     create_individual_header: "创建独立账号"
     email_announcements: "通过邮件接收新关卡和功能通知！"
 #    teacher_email_announcements: "Keep me updated on new teacher resources, curriculum, and courses!"
@@ -248,10 +248,10 @@
     connected_facebook_header: "成功连接 Facebook ！"
     connected_facebook_p: "完成注册后即可使用您的 Facebook 账号登陆。"
     facebook_exists: "此 Facebook 账号已与其他账号绑定！"
-    hey_students: "同学，请输入教师提供的班级码。"
+    hey_students: "同学，请输入老师提供的班级码。"
     birthday: "生日"
     parent_email_blurb: "我知道你坐不住准备学编程啦！— 我们也很兴奋！你的家长会收到一封邮件，上面写着为你注册账号要注意的事情。如果有问题就发邮件到 {{email_link}} 吧。"
-    classroom_not_found: "班级代码不正确。请检查是否打错或者寻求教师的帮助。"
+    classroom_not_found: "班级代码不正确。请检查是否打错或者寻求老师的帮助。"
     checking: "检查中..."
     account_exists: "这个邮箱已被使用："
     sign_in: "登陆"
@@ -262,17 +262,17 @@
     choose_type: "选择账号类型："
     teacher_type_1: "用 CodeCombat 教编程！"
     teacher_type_2: "建立班级"
-    teacher_type_3: "取得课程教案"
+    teacher_type_3: "取得课程指导"
     teacher_type_4: "查看学生进度"
-    signup_as_teacher: "注册教师账号"
-    student_type_1: "玩好玩游戏，同时学编程！"
-    student_type_2: "和班级同学一起玩"
-    student_type_3: "与同学一决高下"
+    signup_as_teacher: "注册成为老师"
+    student_type_1: "玩游戏的同时学习编程！"
+    student_type_2: "和班上同学一起玩！"
+    student_type_3: "在竞技场中互相对抗"
     student_type_4: "自行选择英雄！"
-    student_type_5: "要准备班级码哦！"
-    signup_as_student: "注册学生账号"
-    individuals_or_parents: "独立者和家长"
-    individual_type: "为课堂外学习编程设计。家长可在此注册账号。"
+    student_type_5: "要准备班级代码哦！"
+    signup_as_student: "注册成为学生"
+    individuals_or_parents: "独立账号＆家长"
+    individual_type: "对于想要在课外学习编程的学生，家长应该从这个地方注册账号。"
     signup_as_individual: "注册独立账号"
     enter_class_code: "输入班级码："
     enter_birthdate: "请选择生日："
@@ -480,7 +480,7 @@
     victory_new_hero: "新的英雄"
     victory_viking_code_school: "这关真的超难！ 如果您想成为一个软件开发人员，您就应该去试一下Viking Code School。在这里您可以把您的知识增长到另一个台阶。只需要14周您就能成为一个专业的网页开发人员。"
     victory_become_a_viking: "成为一个维京人吧"
-    victory_no_progress_for_teachers: "教师不能保存进度。但您可在您的班级为自己添加一个学生账号。"
+    victory_no_progress_for_teachers: "老师不能保存进度。但您可在您的班级为自己添加一个学生账号。"
     tome_cast_button_run: "运行"
     tome_cast_button_running: "正在运行"
     tome_cast_button_ran: "运行过"
@@ -650,7 +650,7 @@
     recovered: "之前购买的宝石已恢复。请刷新页面。"
     price: "x{{gems}} / 月"
 #    buy_premium: "Buy Premium"
-#    purchase: "Purchase"
+    purchase: "购买"
 #    purchased: "Purchased"

 #  earn_gems:
@@ -658,10 +658,10 @@
 #    prompt_body: "Keep playing to earn more!"

   subscribe:
-#    best_deal: "Best Deal!"
+    best_deal: "最佳优惠!"
 #    confirmation: "Congratulations! You now have a CodeCombat Premium Subscription!"
 #    premium_already_subscribed: "You're already subscribed to Premium!"
-    subscribe_modal_title: "CodeCombat Premium"
+    subscribe_modal_title: "CodeCombat 订阅"
     comparison_blurb: "亲，订阅CodeCombat，大力的提升您的技能！" # {change}
 #    premium_pricing_prefix: "Get Premium for just"
 #    premium_pricing_suffix: "and become a master coder."
@@ -789,13 +789,13 @@
     master_coder: "通过订阅成为编程高手！"
 #    paypal_redirect: "You will be redirected to PayPal to complete the subscription process."
     subscribe_now: "现在订阅"
-    hero_blurb_1: "订阅后可使用 __premiumHeroesCount__ 个超级英雄,利用Okar Stompfoot不可阻挡的力量,叶之Naria的致命精度,或Nalfar Cryptor的\"可爱\"骷髅！"
-    hero_blurb_2: "高级战士可解锁惊人的武术技能，如Warcry、Stomp和Hurl Enemy。 或者玩游侠,使用隐形和弓箭、飞刀、陷阱！尝试用你的技能作为一个真正的编码教学，释放一个强大的原力，死灵或元素魔法阵列！"
+    hero_blurb_1: "订阅后可使用 __premiumHeroesCount__ 个超级英雄，利用Okar Stompfoot不可阻挡的力量，叶之Naria的致命精度，或Nalfar Cryptor的\"可爱\"骷髅！"
+    hero_blurb_2: "高级战士可解锁惊人的武术技能，如Warcry、Stomp和Hurl Enemy。 或者玩游侠，使用隐形和弓箭、飞刀、陷阱！尝试用你的技能作为一个真正的编码教学，释放一个强大的原力、死灵或元素魔法阵列！"
     hero_caption: "令人兴奋的新英雄！"
-#    pet_blurb_1: "Pets aren't just adorable companions, they also provide unique functionality and methods. The Baby Griffon can carry units through the air, the Wolf Pup plays catch with enemy arrows, the Cougar is fond of chasing ogres around, and the Mimic attracts coins like a magnet!"
-    pet_blurb_2: "收集所有的宠物,发现它们独特的能力！"
+    pet_blurb_1: "宠物不只是可爱的同伴，它们还提供独特的技能。 宝宝Griffon可以在空中携带单位，宝宝Wolf可以抓住敌人射来的箭，宝宝Cougar喜欢追杀食人魔，而宝宝Mimic可以像磁铁一样吸引硬币!"
+    pet_blurb_2: "收集所有的宠物，发现它们独特的能力！"
     pet_caption: "领养宠物陪伴你的英雄！"
-    game_dev_blurb: "学习游戏脚本并建立新的关卡,与您的朋友分享！ 放置你想要的项目,编写单位逻辑和行为代码,看看你的朋友是否可以击败关卡！"
+    game_dev_blurb: "学习游戏脚本并建立新的关卡，与您的朋友分享！ 放置你想要的项目，编写单位逻辑和行为代码，看看你的朋友是否可以击败关卡！"
     game_dev_caption: "设计自己的游戏来挑战你的朋友！"
     everything_in_premium: "您在CodeCombat高级订阅中获得的内容:"
     list_gems: "获得奖励的宝石购买装备、宠物和英雄"
@@ -803,9 +803,9 @@
     list_heroes: "解锁独家英雄, 包括游侠和精灵类"
     list_game_dev: "与朋友分享游戏"
     list_web_dev: "构建网站和应用程序"
-    list_items: "配备限量专用物品,如宠物"
+    list_items: "配备限量专用物品，如宠物"
     list_support: "获取高级支持，以帮助您调试棘手的代码"
-    list_clans: "创建私人宗门邀请你的朋友加入,并与其它宗门在排行榜上进行竞争"
+    list_clans: "创建私人部落邀请你的朋友加入,并与其它部落在排行榜上进行竞争"

   choose_hero:
     choose_hero: "请选择您的英雄"
@@ -910,32 +910,32 @@
     cat_blurb: "气宗"
     scott_title: "共同创始人，软件工程师"
     scott_blurb: "理性至上"
-    maka_title: "客户律师"
+    maka_title: "客户代言人"
     maka_blurb: "故事作者"
     rob_title: "软件工程师"
     rob_blurb: "编代码之类的"
     josh_c_title: "游戏设计师"
     josh_c_blurb: "设计游戏"
-    robin_title: "用户体验设计和研究" # {change}
+    robin_title: "产品经理" # {change}
     robin_blurb: "基架" # {change}
     josh_title: "游戏设计师"
     josh_blurb: "地面是熔岩"
     phoenix_title: "软件工程师"
     nolan_title: "区域经理"
-    elliot_title: "客户经理"
+    elliot_title: "合作经理"
     elliot_blurb: "读心者"
-    lisa_title: "市场开发代表" # {change}
+    lisa_title: "校园专家" # {change}
     lisa_blurb: "坚韧不拔的人"
-    sean_title: "领土经理"
-    liz_title: "领土经理"
-#    david_title: "Customer Success Manager"
-#    jane_title: "Partnership Associate"
+    sean_title: "区域经理"
+    liz_title: "区域经理"
+    david_title: "客户成功经理"
+    jane_title: "合作伙伴"
     retrostyle_title: "插画师"
     retrostyle_blurb: "复古风格的游戏"
     jose_title: "音乐"
     jose_blurb: "放轻松"
     bryukh_title: "游戏设计师"
-#    bryukh_blurb: "Constructs puzzles"
+    bryukh_blurb: "构建拼图"
     community_title: "...以及我们的开源社区"
     community_subtitle: "超过 450 个志愿者帮助我们建设 CodeCombat，每周都有新成员加入！" # {change}
     community_description_3: "CodeCombat 是一个"
@@ -988,45 +988,45 @@
   teachers:
     licenses_needed: "需要许可"

-#  special_offer:
-#    special_offer: "Special Offer"
-#    project_based_title: "Project-Based Courses"
-#    project_based_description: "Web and Game Development courses feature shareable final projects."
-#    great_for_clubs_title: "Great for clubs and electives"
-#    great_for_clubs_description: "Teachers can purchase up to __maxQuantityStarterLicenses__ Starter Licenses." #
-#    low_price_title: "Just __starterLicensePrice__ per student"
-#    low_price_description: "Starter Licenses are active for __starterLicenseLengthMonths__ months from purchase."
-#    three_great_courses: "Three great courses included in the Starter License:"
-#    license_limit_description: "Teachers can purchase up to __maxQuantityStarterLicenses__ Starter Licenses. You have already purchased __quantityAlreadyPurchased__. If you need more, contact __supportEmail__. Starter Licenses are valid for __starterLicenseLengthMonths__ months."
-#    student_starter_license: "Student Starter License"
-#    purchase_starter_licenses: "Purchase Starter Licenses"
-#    purchase_starter_licenses_to_grant: "Purchase Starter Licenses to grant access to __starterLicenseCourseList__"
-#    starter_licenses_can_be_used: "Starter Licenses can be used to assign additional courses immediately after purchase."
-#    pay_now: "Pay Now"
-#    we_accept_all_major_credit_cards: "We accept all major credit cards."
-#    cs2_description: "builds on the foundation from Introduction to Computer Science, diving into if-statements, functions, events and more."
-#    wd1_description: "introduces the basics of HTML and CSS while teaching skills needed for students to build their first webpage."
-#    gd1_description: "uses syntax students are already familiar with to show them how to build and share their own playable game levels."
-#    see_an_example_project: "see an example project"
-#    get_started_today: "Get started today!"
-#    want_all_the_courses: "Want all the courses? Request information on our Full Licenses."
-#    compare_license_types: "Compare License Types:"
-#    cs: "Computer Science"
-#    wd: "Web Development"
-#    wd1: "Web Development 1"
-#    gd: "Game Development"
-#    gd1: "Game Development 1"
-#    maximum_students: "Maximum # of Students"
-#    unlimited: "Unlimited"
-#    priority_support: "Priority support"
-#    yes: "Yes"
-#    price_per_student: "__price__ per student"
-#    pricing: "Pricing"
-#    free: "Free"
-#    purchase: "Purchase"
-#    courses_prefix: "Courses"
+  special_offer:
+    special_offer: "特别优惠"
+    project_based_title: "基于项目的课程"
+    project_based_description: "Web 和 Game Development 课程可共享最终项目."
+    great_for_clubs_title: "伟大的教育和培训课程"
+    great_for_clubs_description: "老师可以购买多达 __maxQuantityStarterLicenses__ 个学生入门许可证." #
+    low_price_title: "每个学生仅仅只要 __starterLicensePrice__ 元 "
+    low_price_description: "学生入门许可证在购买后的 __starterLicenseLengthMonths__ 个月内有效."
+    three_great_courses: "学生入门许可证中包含三个伟大的课程:"
+    license_limit_description: "老师最多可以购买 __maxQuantityStarterLicenses__ 个许可证. 你已经购买了 __quantityAlreadyPurchased__ 个. 如果你需要更多, 请联系 __supportEmail__. 学生入门许可证有效期为 __starterLicenseLengthMonths__ 个月."
+    student_starter_license: "学生入门许可"
+    purchase_starter_licenses: "购买入门许可证"
+    purchase_starter_licenses_to_grant: "购买入门许可证可被授权访问 __starterLicenseCourseList__"
+    starter_licenses_can_be_used: "入门许可证购买后可以立即分配额外的课程."
+    pay_now: "现在付款"
+    we_accept_all_major_credit_cards: "我们接受所有主要的信用卡."
+    cs2_description: "建立在计算机科学入门之上，深入了解 if 语句，函数(functions)，事件(events)和更多。"
+    wd1_description: "介绍了HTML和CSS的基础知识，同时需要教学技巧来教会学生建立他们的第一个网页。"
+    gd1_description: "对于已经熟悉了语法的学生，向他们展示如何构建和分享自己创建的游戏关卡。"
+    see_an_example_project: "看一个示例项目"
+    get_started_today: "今天就开始!"
+    want_all_the_courses: "想要所有课程? 联系我们获取完整的许可信息。"
+    compare_license_types: "比较许可证类型:"
+    cs: "计算机科学"
+    wd: "网页开发"
+    wd1: "网页开发1"
+    gd: "游戏开发"
+    gd1: "游戏开发1"
+    maximum_students: "最多学生数"
+    unlimited: "无限"
+    priority_support: "优先支持"
+    yes: "是"
+    price_per_student: "__price__ 每个学生"
+    pricing: "定价"
+    free: "免费"
+    purchase: "购买"
+    courses_prefix: "课程"
 #    courses_suffix: ""
-#    course_prefix: "Course"
+    course_prefix: "课程"
 #    course_suffix: ""

   teachers_quote:
@@ -1080,15 +1080,15 @@
     thanks_sub_header: "感谢您为您的学校表达对 CodeCombat 的兴趣。"
     thanks_p: "我们不久会和你联系！如果想和我们取得联系，可以通过以下方式："
     back_to_classes: "返回班级"
-    finish_signup: "即将创建您的教师账号："
-    finish_signup_p: "创建教师账号可以建立班级、添加学生、观察他们学习计算机科学的进度。"
+    finish_signup: "即将创建您的老师账号："
+    finish_signup_p: "创建老师账号可以建立班级、添加学生、观察他们学习计算机科学的进度。"
     signup_with: "通过以下方式创建账号："
     connect_with: "与已有账号连接："
-    conversion_warning: "警告：您的当前账号为 学生账号。一旦提交表格，您的账号将会升级为教师账号。"
-    learn_more_modal: "在 CodeCombat 中，教师账号可以观察学生进度、分配名额以及管理教室。教师账号不能加入班级 - 如果您使用当前账号加入了班级，升级后将不能访问所加入的班级。"
-    create_account: "创建教师账号"
-    create_account_subtitle: "在 CodeCombat 班级中使用教师专用工具。 创建班级添加学生、观察学生进度！"
-    convert_account_title: "升级为教师账号"
+    conversion_warning: "警告：您的当前账号为 学生账号。一旦提交表格，您的账号将会升级为老师账号。"
+    learn_more_modal: "在 CodeCombat 中，老师账号可以观察学生进度、分配名额以及管理教室。老师账号不能加入班级 - 如果您使用当前账号加入了班级，升级后将不能访问所加入的班级。"
+    create_account: "创建老师账号"
+    create_account_subtitle: "在 CodeCombat 班级中使用老师专用工具。 创建班级添加学生、观察学生进度！"
+    convert_account_title: "升级为老师账号"
     not: "不是"

   versions:
@@ -1200,33 +1200,33 @@
     contribute_to_the_project: "为项目做贡献"

   clans:
-    clan: "宗门"
-    clans: "宗门"
-    new_name: "新宗门的名字"
-    new_description: "新宗门的描述"
-    make_private: "设定宗门为私人宗门"
+    clan: "部落"
+    clans: "部落"
+    new_name: "新部落的名字"
+    new_description: "新部落的描述"
+    make_private: "设定部落为私人部落"
     subs_only: "只限订阅"
-    create_clan: "创建新的宗门"
+    create_clan: "创建新的部落"
     private_preview: "预览"
-    private_clans: "私人宗门"
-    public_clans: "公开宗门"
-    my_clans: "我的宗门"
-    clan_name: "宗门名字"
+    private_clans: "私人部落"
+    public_clans: "公开部落"
+    my_clans: "我的部落"
+    clan_name: "部落名字"
     name: "名字"
     chieftain: "首领"
-    edit_clan_name: "编辑宗门的名字"
-    edit_clan_description: "编辑宗门的描述"
+    edit_clan_name: "编辑部落的名字"
+    edit_clan_description: "编辑部落的描述"
     edit_name: "编辑名字"
     edit_description: "编辑描述"
     private: "(私人)"
     summary: "综述"
     average_level: "平均等级"
     average_achievements: "平均成就"
-    delete_clan: "删除宗门"
-    leave_clan: "离开宗门"
-    join_clan: "加入宗门"
+    delete_clan: "删除部落"
+    leave_clan: "离开部落"
+    join_clan: "加入部落"
     invite_1: "邀请："
-    invite_2: "*通过这个链接来邀请玩家加入宗门。"
+    invite_2: "*通过这个链接来邀请玩家加入部落。"
     members: "成员"
     progress: "进度"
     not_started_1: "还没开始"
@@ -1243,7 +1243,7 @@
     latest_achievement: "最新成就"
     playtime: "游戏时间"
     last_played: "最后玩了"
-    leagues_explanation: "在宗门里与其他成员组成联盟一起参加下面的多人竞技场。"
+    leagues_explanation: "在部落里与其他成员组成联盟一起参加下面的多人竞技场。"
     track_concepts1: "跟踪概念"
     track_concepts2a: "由每位学生学习"
     track_concepts2b: "由每位成员学习"
@@ -1256,10 +1256,10 @@
     track_concepts6b: "按姓名或进度排序成员"
     track_concepts7: "需要邀请"
     track_concepts8: "来加入"
-    private_require_sub: "创建或加入私人宗门时需要具体描述。"
+    private_require_sub: "创建或加入私人部落时需要具体描述。"

   courses:
-    create_new_class: "建立新的课堂"
+    create_new_class: "建立新的班级"
     unnamed_class: "班级未命名"
     edit_settings1: "编辑班级设定"
     add_students: "添加学生"
@@ -1284,8 +1284,8 @@
     keep_using: "继续使用"
     switch_to: "切换到"
     greetings: "欢迎！"
-    back_classrooms: "回到我的教室"
-    back_classroom: "返回教室"
+    back_classrooms: "回到我的班级"
+    back_classroom: "返回班级"
     back_courses: "回到我的课程"
     edit_details: "编辑课程详情"
     purchase_enrollments: "购买学生许可"
@@ -1313,10 +1313,10 @@
     class_added: "成功添加课程！"
     view_levels: "浏览关卡" # {change}
     view_project_gallery: "浏览同学的项目"
-    join_class: "加入课程"
+    join_class: "加入班级"
     join_class_2: "加入班级"
-    ask_teacher_for_code: "从你的老师领取CodeCombat课程码！领取后请在下方输入："
-    enter_c_code: "<输入课程码>"
+    ask_teacher_for_code: "从你的老师那里领取CodeCombat班级代码！领取后请在下方输入："
+    enter_c_code: "<输入班级代码>"
     join: "加入"
     joining: "加入课程中"
     course_complete: "课程完成"
@@ -1327,10 +1327,10 @@
     not_you: "不是你？"
     continue_playing: "继续玩"
     option1_header: "选项1：通过电子邮件邀请学生" # {change}
-    option1_body: "学生会被自动发送一个加入该课程的邀请，他们需要提供用户名和密码以创建账号。" # {change}
+    option1_body: "学生会被自动发送一个加入该班级的邀请，他们需要提供用户名和密码以创建账号。" # {change}
     remove_student1: "删除学生"
     are_you_sure: "你确定要从该课程中删除该学生吗？"
-    remove_description1: "学生将失去访问该课堂和被分派课程的权利。游戏进度不会丢失，该学生可以在任何时间被添加回该教室。"
+    remove_description1: "学生将失去访问该课程和被分派课程的权利。游戏进度不会丢失，该学生可以在任何时间被添加回该班级。"
     remove_description2: "被激活的付费许可证将无法返还。"
     keep_student: "保留学生"
     removing_user: "删除用户"
@@ -1359,23 +1359,23 @@
     student_age_range_to: "到"
     create_class: "创建班级"
     class_name: "班级名称"
-    teacher_account_restricted: "你的账号是教师账号，不能访问学生内容。"
+    teacher_account_restricted: "你的账号是老师账号，不能访问学生内容。"
     account_restricted: "此页面需要学生账号访问。"
     update_account_login_title: "登陆并升级账号"
     update_account_title: "请留意您的账号！"
     update_account_blurb: "在访问班级前，请选择您的账号的用途。"
     update_account_current_type: "当前账号类型："
     update_account_account_email: "账号邮箱/用户名："
-    update_account_am_teacher: "我是教师"
+    update_account_am_teacher: "我是老师"
     update_account_keep_access: "保持所创建班级的权限"
-    update_account_teachers_can: "教师账号能："
+    update_account_teachers_can: "老师账号能："
     update_account_teachers_can1: "创建/管理/添加 班级"
     update_account_teachers_can2: "添加学生到课程中"
     update_account_teachers_can3: "解锁所有课程关卡以便测试"
-    update_account_teachers_can4: "推出教师专用功能时获得使用权"
+    update_account_teachers_can4: "推出老师专用功能时获得使用权"
     update_account_teachers_warning: "警告：你将会退出以前加入的每个班级，并且也不能以学生身份游玩。"
-    update_account_remain_teacher: "保持教师身份"
-    update_account_update_teacher: "升级为教师账号"
+    update_account_remain_teacher: "保持老师身份"
+    update_account_update_teacher: "升级为老师账号"
     update_account_am_student: "我是学生"
     update_account_remove_access: "取消所创建班级的权限"
     update_account_students_can: "学生账号能："
@@ -1408,10 +1408,10 @@
     web_dev_language_transition: "在本课中，所有的编程语言都是使用HTML和JavaScript编写。之前使用Python语言学习的班级会有额外的JavaScript编程介绍关卡来过渡。已经选择JavaScript编程语言的班级将会跳过这些介绍关卡。 "
 #    course_membership_required_to_play: "You'll need to join a course to play this level."
 #    license_required_to_play: "Ask your teacher to assign a license to you so you can continue to play CodeCombat!"
-#    update_old_classroom: "New school year, new levels!"
-#    update_old_classroom_detail: "To make sure you're getting the most up-to-date levels, make sure you create a new class for this semester by clicking Create a New Class on your"
-#    teacher_dashboard: "teacher dashboard"
-#    update_old_classroom_detail_2: "and giving students the new Class Code that appears."
+    update_old_classroom: "新学年, 新关卡!"
+    update_old_classroom_detail: "为确保您获得最新关卡，请您尽量在本学期创建新的班级，方法是单击"
+    teacher_dashboard: "老师面板"
+    update_old_classroom_detail_2: "创建新班级，并为学生提供出现的新班级代码。"

   project_gallery:
 #    no_projects_published: "Be the first to publish a project in this course!"
@@ -1424,31 +1424,31 @@
     level_overview_solutions: "关卡概览和解法"
     no_student_assigned: "没有学生被分配到这个课程。"
     paren_new: "(新)"
-    teacher_dashboard: "教师面板" # Navbar
-    my_classes: "我的教室"
+    teacher_dashboard: "老师面板" # Navbar
+    my_classes: "我的班级"
     courses: "课程指导"
     enrollments: "学生许可"
     resources: "资源"
     help: "帮助"
     language: "编程语言"
-    edit_class_settings: "修改教室设置"
+    edit_class_settings: "修改班级设置"
     access_restricted: "需要升级账号"
-    teacher_account_required: "只有教师账号才能访问。"
-    create_teacher_account: "创建教师账号"
-    what_is_a_teacher_account: "什么是教师账号？"
-    teacher_account_explanation: "教师账号可以创建教师、跟踪学生学习进展、管理注册许可，并且可以查看相应的帮助文档以辅助构建课堂。"
-    current_classes: "当前课堂"
-    archived_classes: "已归档的课堂"
-    archived_classes_blurb: "您可以把课堂归档，便于后续查阅。课程可以解档课程，并在当前课程列表中再次查看。"
-    view_class: "查看课堂"
-    archive_class: "归档课堂"
-    unarchive_class: "解档课堂"
-    unarchive_this_class: "解档本课堂"
-    no_students_yet: "该课堂尚未有学生加入。"
-    no_students_yet_view_class: "查看课程并添加学生。"
+    teacher_account_required: "只有老师账号才能访问。"
+    create_teacher_account: "创建老师账号"
+    what_is_a_teacher_account: "什么是老师账号？"
+    teacher_account_explanation: "老师账号可以让你设立教室，当学生们学完课程后可以查看他们的进度，管理许可证以及查看相应的帮助文档以辅助构建课堂。"
+    current_classes: "当前班级"
+    archived_classes: "已归档的班级"
+    archived_classes_blurb: "您可以把班级归档，便于后续查阅。课程可以解档课程，并在当前课程列表中再次查看。"
+    view_class: "查看班级"
+    archive_class: "归档班级"
+    unarchive_class: "解档班级"
+    unarchive_this_class: "解档本班级"
+    no_students_yet: "该班级尚未有学生加入。"
+    no_students_yet_view_class: "查看班级并添加学生。"
     try_refreshing: "(您可能需要刷新页面)"
-    create_new_class: "创建新课堂"
-    class_overview: "课堂概览" # View Class page
+    create_new_class: "创建新班级"
+    class_overview: "班级概览" # View Class page
     avg_playtime: "单关平均通关时间"
     total_playtime: "总时长"
     avg_completed: "平均完成关卡数"
@@ -1480,11 +1480,11 @@
     level_not_started: "通关未开始"
     project_or_arena: "开发项目或竞技场"
     students_not_assigned: "未开始{{courseName}}的学生"
-    course_overview: "课程概览"
-    copy_class_code: "复制教室代码"
-    class_code_blurb: "未加入的学生可以在自己的功能页面输入该教室代码，或者访问网址加入。" # {change}
-    copy_class_url: "复制教室链接地址"
-    class_join_url_blurb: "新学生可以在登录之后，访问该网址加入教室。" # {change}
+    course_overview: "班级概览"
+    copy_class_code: "复制班级代码"
+    class_code_blurb: "未加入的学生可以在自己的功能页面输入该班级代码，或者访问网址加入。" # {change}
+    copy_class_url: "复制班级链接地址"
+    class_join_url_blurb: "新学生可以在登录之后，访问该网址加入班级。" # {change}
     add_students_manually: "手工添加学生" # {change}
     bulk_assign: "批量添加"
     assigned_msg_1: "{{numberAssigned}} 学生被分配 {{courseName}}。"
@@ -1527,7 +1527,7 @@
     start_date: "起始日期："
     end_date: "结束日期："
     get_enrollments_blurb: " 我们会帮您解决您的班级、学校或地区的需求。"
-    how_to_apply_licenses_blurb_1: "当教师第一次将课程分配给学生时，我们将自动应用许可证。 使用教室中的批量分配下拉菜单为所选学生分配课程：:"
+    how_to_apply_licenses_blurb_1: "当老师第一次将课程分配给学生时，我们将自动应用许可证。使用教室中的批量分配下拉菜单为所选学生分配课程:"
     how_to_apply_licenses_blurb_2: "我可以只申请许可而不分配课程吗?"
     how_to_apply_licenses_blurb_3: "是的 — 转到课堂中的\"许可状态\"选项卡，然后单击 \"应用许可\"到没有被分配许可的学生。"
     request_sent: "申请已发送！"
@@ -1550,8 +1550,8 @@
     getting_started: "入门"
     educator_faq: "常见问题"
     educator_faq_desc: "在您的学校或教室使用 CodeCombat 的常见问题。"
-    teacher_getting_started: "教师入门指南"
-    teacher_getting_started_desc: "第一次使用 CodeCombat ？下载此教师入门指南开始设置您的账号，创建第一个课堂并邀请学生参加第一门课程。"
+    teacher_getting_started: "老师入门指南"
+    teacher_getting_started_desc: "第一次使用 CodeCombat ？下载此老师入门指南开始设置您的账号，创建第一个课堂并邀请学生参加第一门课程。"
     student_getting_started: "学生快速入门指南"
     student_getting_started_desc: "您可以在讲解 CodeCombat 之前先将此入门指南发布给学生，以便他们熟悉代码编辑器。本指南可用于 Python 和 JavaScript 语言教室。"
     cs1: "计算机科学的介绍"
@@ -1571,7 +1571,7 @@
     progress_journal_desc: "鼓励学生通过进度日志跟踪他们的进步。"
     cs1_curriculum: "计算机科学的介绍 - 课程指南"
     cs1_curriculum_desc: "课程 1 的范围、顺序、课程计划和活动等。"
-    arenas_curriculum: "竞技场 － 教师指导"
+    arenas_curriculum: "竞技场 － 老师指导"
 #    arenas_curriculum_desc: "Instructions on how to run Wakka Maul, Cross Bones and Power Peak multiplayer arenas with your class."
     cs2_curriculum: "计算机科学 2 - 课程指南"
     cs2_curriculum_desc: "课程 2 的范围、顺序、课程计划和活动等。"
@@ -1579,9 +1579,9 @@
     cs3_curriculum_desc: "课程 3 的范围、顺序、课程计划和活动等。"
     cs4_curriculum: "计算机科学 4 - 课程指南"
     cs4_curriculum_desc: "课程 4 的范围、顺序、课程计划和活动等。"
-#    cs5_curriculum_js: "Computer Science 5 - Curriculum Guide (JavaScript)"
+    cs5_curriculum_js: "计算机科学 5 - 课程指南 (JavaScript)"
 #    cs5_curriculum_desc_js: "Scope and sequence, lesson plans, activities and more for Course 5 classes using JavaScript."
-#    cs5_curriculum_py: "Computer Science 5 - Curriculum Guide (Python)"
+    cs5_curriculum_py: "计算机科学 5 - 课程指南 (Python)"
 #    cs5_curriculum_desc_py: "Scope and sequence, lesson plans, activities and more for Course 5 classes using Python."
     cs1_pairprogramming: "组队编程活动"
     cs1_pairprogramming_desc: "让两个学生一起组队编程练习，这将有助于他们互相更好的学习与沟通。"
@@ -1630,8 +1630,8 @@
     student_great: "做得太好了，关卡"
     student_great_detail: "这个学生可能是一个很好的候选人，帮助其他学生通过这门课程。"
     full_license: "全课许可"
-#    starter_license: "Starter License"
-#    trial: "Trial"
+    starter_license: "入门许可"
+    trial: "试用"
 #    hoc_welcome: "Happy Computer Science Education Week"
 #    hoc_intro: "There are three ways for your class to participate in Hour of Code with CodeCombat"
 #    hoc_self_led: "Self-Led Gameplay"
@@ -1652,40 +1652,40 @@
 #    hoc_additional_contact: "Get in touch"
 #    revoke_confirm: "Are you sure you want to revoke a Full License from {{student_name}}? The license will become available to assign to another student."
 #    revoking: "Revoking..."
-#    unused_licenses: "You have unused Licenses that allow you to assign students paid courses when they're ready to learn more!"
-#    remember_new_courses: "Remember to assign new courses!"
-#    more_info: "More Info"
-#    how_to_assign_courses: "How to Assign Courses"
-#    select_students: "Select Students"
-#    select_instructions: "Click the checkbox next to each student you want to assign courses to."
-#    choose_course: "Choose Course"
-#    choose_instructions: "Select the course from the dropdown menu you’d like to assign, then click “Assign to Selected Students.”"
-#    push_projects: "We recommend assigning Web Development 1 or Game Development 1 after students have finished Introduction to Computer Science! See our {{resource_hub}} for more details on those courses."
+    unused_licenses: "您有未使用的学生许可,当学生准备学习更多课程时您可以分配给他们!"
+    remember_new_courses: "记得分配新课程!"
+    more_info: "更多信息"
+    how_to_assign_courses: "如何分配课程"
+    select_students: "选择学生"
+    select_instructions: "点击您要分配课程的每个学生旁边的复选框."
+    choose_course: "选择课程"
+    choose_instructions: "从您要分配的下拉菜单中选择课程, 然后点击 “分配给所选学生.”"
+    push_projects: "我们建议学生在完成计算机科学导论后分配网页开发1或游戏开发1! 有关这些课程的更多信息,请参阅我们的 {{resource_hub}} ."
     teacher_quest: "老师任务获得成功"
     quests_complete: "任务完成"
-    teacher_quest_create_classroom: "添加课堂"
+    teacher_quest_create_classroom: "添加班级"
     teacher_quest_add_students: "添加学生"
-    teacher_quest_teach_methods: "帮助你的学生学习如何 `调用方法`。"
-    teacher_quest_teach_methods_step1: "班级中至少有 75% 的学生通过该关卡, __Dungeons of Kithgard__"
+    teacher_quest_teach_methods: "帮助你的学生学习如何 `call methods`。"
+    teacher_quest_teach_methods_step1: "班级中至少有 75% 的学生通过该关卡 __Dungeons of Kithgard__"
     teacher_quest_teach_methods_step2: "打印资源中心的 [学生快速入门指南](http://files.codecombat.com/docs/resources/StudentQuickStartGuide.pdf) 。"
-    teacher_quest_teach_strings: "不要让你的学生跟着走, 教他们 `strings`。"
+    teacher_quest_teach_strings: "学习字符串 `strings`。"
     teacher_quest_teach_strings_step1: "班级中至少有 75% 的学生通过该关卡 __True Names__"
-    teacher_quest_teach_strings_step2: "使用 [课程指导](/teachers/courses) 页面上的教师关卡选择器预览 __True Names__。"
+    teacher_quest_teach_strings_step2: "使用 [课程指导](/teachers/courses) 页面上的老师关卡选择器预览 __True Names__。"
     teacher_quest_teach_loops: "让你的学生在循环中学习 `loops`。"
-#    teacher_quest_teach_loops_step1: "Get 75% of at least one class through __Fire Dancing__."
-#    teacher_quest_teach_loops_step2: "Use the __Loops Activity__ in the [CS1 Curriculum guide](/teachers/resources/cs1) to reinforce this concept."
-#    teacher_quest_teach_variables: "Vary it up with `variables`."
-#    teacher_quest_teach_variables_step1: "Get 75% of at least one class through __Known Enemy__."
-#    teacher_quest_teach_variables_step2: "Encourage collaboration by using the [Pair Programming Activity](/teachers/resources/pair-programming)."
-#    teacher_quest_kithgard_gates_100: "Escape the Kithgard Gates with your class."
-#    teacher_quest_kithgard_gates_100_step1: "Get 75% of at least one class through __Kithgard Gates__."
-#    teacher_quest_kithgard_gates_100_step2: "Guide students to think through hard problems using the [Engineering Cycle Worksheet](http://files.codecombat.com/docs/resources/EngineeringCycleWorksheet.pdf)."
-#    teacher_quest_wakka_maul_100: "Prepare to duel in Wakka Maul."
-#    teacher_quest_wakka_maul_100_step1: "Get 75% of at least one class to __Wakka Maul__."
-#    teacher_quest_wakka_maul_100_step2: "See the [Arena Guide](/teachers/resources/arenas) in the [Resource Hub](/teachers/resources) for tips on how to run a successful arena day."
-#    teacher_quest_reach_gamedev: "Explore new worlds!"
-#    teacher_quest_reach_gamedev_step1: "[Get licenses](/teachers/licenses) so that your students can explore new worlds, like Game Development and Web Development!"
-#    teacher_quest_done: "Want your students to learn even more code? Get in touch with our [school specialists](mailto:schools@codecombat.com) today!"
+    teacher_quest_teach_loops_step1: "班级中至少有 75% 的学生通过该关卡 __Fire Dancing__."
+    teacher_quest_teach_loops_step2: "强化学习 __Loops Activity__ 这个概念，参考 [CS1 课程指南](/teachers/resources/cs1)."
+    teacher_quest_teach_variables: "学习变量 `variables`."
+    teacher_quest_teach_variables_step1: "班级中至少有 75% 的学生通过该关卡 __Known Enemy__."
+    teacher_quest_teach_variables_step2: "通过使用 [组队编程活动](/teachers/resources/pair-programming)学习如何与团队合作."
+    teacher_quest_kithgard_gates_100: "和你的班级一起逃离 Kithgard Gates."
+    teacher_quest_kithgard_gates_100_step1: "班级中至少有 75% 的学生通过该关卡 __Kithgard Gates__."
+    teacher_quest_kithgard_gates_100_step2: "引导学生通过思考来解决难题 [Engineering Cycle Worksheet](http://files.codecombat.com/docs/resources/EngineeringCycleWorksheet.pdf)."
+    teacher_quest_wakka_maul_100: "准备在 Wakka Maul 中决斗."
+    teacher_quest_wakka_maul_100_step1: "班级中至少有 75% 的学生通过该关卡 __Wakka Maul__."
+    teacher_quest_wakka_maul_100_step2: "在 [资源中心](/teachers/resources) 中查看 [竞技场指南](/teachers/resources/arenas) ，了解如何在竞技中获得胜利."
+    teacher_quest_reach_gamedev: "探索新的世界!"
+    teacher_quest_reach_gamedev_step1: "[获得许可证](/teachers/licenses) 以便您的学生可以探索新的世界, 如 Game Development 和 Web Development!"
+    teacher_quest_done: "希望你的学生学习更多的代码? 现在就请联系我们的 [校园专家](mailto:schools@codecombat.com) !"
     teacher_quest_keep_going: "继续！ 这是你下一步可以做的:"
     teacher_quest_more: "查看所有任务"
     teacher_quest_less: "查看基础任务"
@@ -1712,7 +1712,7 @@
     game: "游戏"
     webpage: "网页"
     your_students_preview: "您的学生可以点击这里查看他们的完成项目，在老师预览中不可用。"
-    unavailable: "教师预览中不提供链接共享。"
+    unavailable: "老师预览中不提供链接共享。"
     share_game: "分享此游戏"
     share_web: "分享此网页"
     victory_share_prefix: "将这个链接分享给亲朋好友，让他们"
@@ -1762,7 +1762,7 @@
     ambassador_title: "使节"
     ambassador_title_description: "(用户支持人员)"
     ambassador_summary: "指导我们亲爱的论坛用户并为他们的问题引出一个正确的方向。我们的使节将CodeCombat带到这个世界上。"
-    teacher_title: "教师"
+    teacher_title: "老师"

   editor:
     main_title: "CodeCombat编辑器"
@@ -1917,7 +1917,7 @@
     ambassador_join_note_strong: "注意"
     ambassador_join_note_desc: "我们其中一个想法是要制造一个当玩家们在解决问题上有难度时可以要求其他比较强的玩家来协助他们的多人游戏。这将会是一个很好的办法让使节们可以更容易地协助玩家！如有任何更新，我们会在第一时间通知您！"
     ambassador_subscribe_desc: "通过电子邮件获得支持系统的现状，以及多人游戏方面的新进展。"
-    teacher_subscribe_desc: "接收针对教师的更新和公告邮件。"
+    teacher_subscribe_desc: "接收针对老师的更新和公告邮件。"
     changes_auto_save: "在您勾选复选框后，更改将自动保存。"
     diligent_scribes: "我们勤奋的文书:"
     powerful_archmages: "我们强力的大法师:"
@@ -1930,7 +1930,7 @@
     my_matches: "我的对手"
     simulate: "模拟"
     simulation_explanation: "通过模拟游戏，您可以让您的游戏更快的得到评分。"
-    simulation_explanation_leagues: "你会主要给在你的宗门或者课程的同伴帮忙模拟游戏。"
+    simulation_explanation_leagues: "你会主要给在你的部落或者课程的同伴帮忙模拟游戏。"
     simulate_games: "模拟游戏！"
     games_simulated_by: "由您模拟过的游戏数:"
     games_simulated_for: "为您模拟过的游戏数:"
@@ -2004,7 +2004,7 @@
     no_achievements: "还未得到任何成就。"
     favorite_prefix: "最喜爱的语言是 "
     favorite_postfix: "。"
-    not_member_of_clans: "还不是任何一个宗门里的成员。"
+    not_member_of_clans: "还不是任何一个部落里的成员。"

   achievements:
     last_earned: "最近取得的"
@@ -2037,7 +2037,7 @@
     cost: "花费"
     next_payment: "下次付款"
     card: "银行卡"
-    status_unsubscribed_active: "您还没有订阅所以不会被收费，但您的帐号还可以正常使用"
+    status_unsubscribed_active: "您还没有订阅所以不会被收费，但您的账号还可以正常使用"
     status_unsubscribed: "只要一个CodeCombat订阅，您就可以玩转全部新关卡，用遍全部英雄，物品，以及获得更多的宝石哦！"
     not_yet_verified: "尚未验证。"
     resend_email: "重发验证邮件"
@@ -2050,7 +2050,7 @@
     amount: "金额（美元）"
     declined: "您的付款被拒"
     invalid_amount: "请输入美元金额。"
-    not_logged_in: "登录或创建一个帐户来获得发票。"
+    not_logged_in: "登录或创建一个账号来获得发票。"
     pay: "付费发票"
     purchasing: "购买中···"
     retrying: "服务器错误，重试中。"
@@ -2061,7 +2061,7 @@
     purchase_code1: "订阅码可以为一个或多个CodeCombat账号兑换额外的订阅时间。" # {change}
     purchase_code2: "每个CodeCombat账号每次只能兑换一个订阅码。"
     purchase_code3: "订阅码时间会在账号现有的订阅时间基础上延长。"
-    purchase_code4: "订阅码适用于玩独立版CodeCombat的帐户，不能代替“课堂”版本的学生许可。"
+    purchase_code4: "订阅码适用于玩独立版CodeCombat的账号，不能代替“课堂”版本的学生许可。"
     purchase_code5: "有关学生许可的更多信息, 请联系"
     users: "玩家"
     months: "月份"


Refactor

Refactor

--- src/trix/observers/mutation_observer.coffee
+++ src/trix/observers/mutation_observer.coffee
@@ -15,6 +15,7 @@
     @start()

   start: ->
+    @reset()
     @observer.observe(@element, options)

   stop: ->
@@ -22,18 +23,18 @@

   didMutate: (mutations) =>
     clearTimeout(@debounce)
-
-    @mutations ?= []
-    @mutations.push(mutations...)
+    @mutations.push(@findSignificantMutations(mutations)...)

     @debounce = setTimeout =>
-      significantMutations = @findSignificantMutations(@mutations)
-      return unless significantMutations.length
-      @delegate?.elementDidMutate?(summarizeMutations(significantMutations))
-      @mutations = []
+      if @mutations.length
+        @delegate?.elementDidMutate?(@getMutationSummary())
+        @reset()
     , 1

   # Private
+
+  reset: ->
+    @mutations = []

   findSignificantMutations: (mutations) ->
     mutation for mutation in mutations when @mutationIsSignificant(mutation)
@@ -63,14 +64,15 @@
         nodes.push(mutation.removedNodes...)
     nodes

-  summarizeMutations = (mutations) ->
-    summarizeTextMutations(mutations)
+  getMutationSummary: ->
+    @getTextMutationSummary()

-  summarizeTextMutations = (mutations) ->
+  getTextMutationSummary: ->
     additions = []
     deletions = []

-    characterMutations = (mutation for mutation in mutations when mutation.type is "characterData")
+    characterMutations = @getMutationsByType("characterData")
+
     if characterMutations.length
       [startMutation, ..., endMutation] = characterMutations
       oldString = normalizeSpaces(startMutation.oldValue)
@@ -79,7 +81,7 @@
       additions.push(stringDifference(newString, oldString))
       deletions.push(stringDifference(oldString, newString))

-    for node in getRemovedTextNodes(mutations)
+    for node in @getRemovedTextNodes()
       deletions.push(node.data)

     summary = {}
@@ -87,12 +89,13 @@
     summary.textDeleted = deleted if deleted = deletions.join("")
     summary

-  getRemovedTextNodes = (mutations) ->
-    nodes = []
-    for mutation in mutations when mutation.type is "childList"
+  getMutationsByType: (type) ->
+    mutation for mutation in @mutations when mutation.type is type
+
+  getRemovedTextNodes: ->
+    for mutation in @getMutationsByType("childList")
       for node in mutation.removedNodes when node.nodeType is Node.TEXT_NODE
-        nodes.push(node)
-    nodes
+        node

   stringDifference = (a, b) ->
     leftIndex = 0


refactored the condition matcher and commented some"

--- lib/drivers/mongodb/conditional_matcher.coffee
+++ lib/drivers/mongodb/conditional_matcher.coffee
@@ -1,22 +1,31 @@
+#
+# matchers take the form of (actual, expected)
+#
+# actual is the actual value, and expected is
+# the value from the query
+#
+# a good way to think about it is actual <FUNCTION> expected
+# for example actual $gt expected, actual is greater than
+# expected
+
 matchers =
-  $e: (document, field, value) ->
-    return document[field] == value
+  $e: (actual, expected) ->
+    return actual == expected

-  $ne: (document, field, value) ->
-    return document[field] != value
+  $ne: (actual, expected) ->
+    return actual != expected

-  $gt: (document, field, value) ->
-    return document[field] > value
+  $gt: (actual, expected) ->
+    return actual > expected

-  $gte: (document, field, value) ->
-    return document[field] >= value
+  $gte: (actual, expected) ->
+    return actual >= expected

-  $lt: (document, field, value) ->
-    return document[field] < value
+  $lt: (actual, expected) ->
+    return actual < expected

-  $lte: (document, field, value) ->
-    return document[field] <= value
-
+  $lte: (actual, expected) ->
+    return actual <= expected
   $all: () -> throw "implement me!"
   $exists: () -> throw "implement me!"
   $mod: () -> throw "implement me!"
@@ -41,11 +50,11 @@
       Object.keys(conditions).forEach (field) ->
         condition = conditions[field]
         if typeof condition != "object"
-          matched = matched && matchers.$e document, field, condition
+          matched = matched && matchers.$e(document[field], condition)
         else
           Object.keys(condition).forEach (operator) ->
             value = condition[operator]
-            matched = matched && matchers[operator] document, field, value
+            matched = matched && matchers[operator](document[field], value)
       return matched

   return ConditionMatcher


Allow scrolling past end of doc

Allow scrolling past end of doc

--- coffee/docBase.coffee
+++ coffee/docBase.coffee
@@ -118,6 +118,7 @@
 editorSession.setUseWrapMode true
 editorSession.setUseSoftTabs true
 editor.setOption 'vScrollBarAlwaysVisible', true
+editor.setOption 'scrollPastEnd', true

 setWrapBasedOnViewportWidth()
 window.addEventListener 'resize', -> setWrapBasedOnViewportWidth()


URL encode Twitter share link to prevent trailing slash

ex: https://twitter.com/MikeHerman/status/618580842223435782
ref: http://stackoverflow.com/a/27075962/131898

--- coffee/docBase.coffee
+++ coffee/docBase.coffee
@@ -179,7 +179,7 @@
     content: """

Facebook

-
Twitter

+
Twitter

Email

     """


fix content type selector in content configurable block

--- BackofficeBundle/Resources/public/coffee/extendView/contentTypeSelector.coffee
+++ BackofficeBundle/Resources/public/coffee/extendView/contentTypeSelector.coffee
@@ -6,13 +6,11 @@
   changeContentTypeSelector: (event) ->
     event.preventDefault()
     target = $(event.currentTarget)
-    contentTypeId = target
-    url = target.data("url")
     $.ajax
       type: "GET"
-      url: url
+      url: target.data("url")
       data:
-        content_type: contentTypeId
+        content_type: target.val()
       success: (response) ->
         $("#block_contentId").find("option").remove()
         $.each response.contents, (index, item) ->


back-front bug

back-front bug


Remove period's names so tabs match up

Remove period's names so tabs match up

--- test/components/course-settings.spec.coffee
+++ test/components/course-settings.spec.coffee
@@ -40,7 +40,7 @@

   it 'renders students in the panels', ->
     # Pluck the last names from second column.  Should appear in alphabetical order and obey is_active
-    for names, tab in [[], [], [], ['Potter'], ['Ariza', 'Griffiths']]
+    for names, tab in [[], [], ['Potter'], ['Ariza', 'Griffiths']]
       rendered_names = _.pluck(@state.div.querySelectorAll(
         ".tab-content .tab-pane:nth-child(#{tab+1}) tr td:nth-child(2)"
       ), 'textContent')


return req msg

return req msg

--- scripts/tonypai.coffee
+++ scripts/tonypai.coffee
@@ -6,8 +6,10 @@
       city = "Taichung"
     else
       city = result[2]
-
-    robot.http("http://api.openweathermap.org/data/2.5/forecast/daily?q="+city+",tw")
+
+    url = "http://api.openweathermap.org/data/2.5/forecast/daily?q="+city+",tw"
+
+    robot.http(url)
       .header('Accept', 'application/json')
       .get() (err, res, body) ->

@@ -32,4 +34,4 @@

             result.send weather
           else
-            result.send "weather [Taiwan city name]"+            result.send url+"("+data.cod+")\nweather [Taiwan city name]"


Output of iteration.

Output of iteration.

--- scripts/watched.coffee
+++ scripts/watched.coffee
@@ -86,4 +86,5 @@
             ).slice(0, hourcount)

             # msg.send "The top #{hourcount} users with the most hours are: #{hourscore}"
-            msg.send "Saved hours: " + savedhours
+            # Output of savedhours is "[object Object]"
+            msg.send "Saved hours: " + watchedscores


Add test page which loads inner app

Add test page which loads inner app

--- js/inner.coffee
+++ js/inner.coffee
@@ -323,6 +323,7 @@

 $(document).ready () ->
   Sammy('#app', () ->
+    @get('/test/inner', (cx) -> ) # load nothing, wait for test runner to do something
     @get('/', (cx) => VM.loadDashboard())
     @get('/gh/:username/:project/edit(.*)', (cx) -> VM.loadEditPage cx.params.username, cx.params.project, cx.params.splat)
     @get('/account', (cx) -> VM.loadAccountPage())


Give a better message when logged out

Give a better message when logged out

--- js/inner.coffee
+++ js/inner.coffee
@@ -24,9 +24,11 @@
 $(document).ajaxSuccess (ev, xhr, options) ->
   finishAjax(xhr.event, "data-success-text", "Saved")

-$(document).ajaxError (ev, xhr, status, errorThrown) ->
+$(document).ajaxError (ev, xhr, settings, errorThrown) ->
   finishAjax(xhr.event, "data-failed-text", "Failed")
-  if xhr.responseText.indexOf("<!DOCTYPE") is 0
+  if xhr.status == 401
+    notifyError "You've been logged out, log back in to continue."
+  else if xhr.responseText.indexOf("<!DOCTYPE") is 0
     notifyError "An unknown error occurred: (#{xhr.status} - #{xhr.statusText})."
   else
     notifyError (xhr.responseText or xhr.statusText)


Test GetColors to receive a zero sized image

--- spec/GetColors.coffee
+++ spec/GetColors.coffee
@@ -39,19 +39,22 @@
     callback canvas
   return id

-describe 'GetColors component', ->
+describe.only 'GetColors component', ->
   c = null
   ins = null
   colors = null
   canvas = null
+  error = null
   beforeEach ->
     c = GetColors.getComponent()
     ins = noflo.internalSocket.createSocket()
     colors = noflo.internalSocket.createSocket()
     canvas = noflo.internalSocket.createSocket()
+    error = noflo.internalSocket.createSocket()
     c.inPorts.canvas.attach ins
     c.outPorts.colors.attach colors
     c.outPorts.canvas.attach canvas
+    c.outPorts.error.attach error

   describe 'when instantiated', ->
     it 'should have a input ports', ->
@@ -62,6 +65,7 @@
     it 'should have output ports', ->
       chai.expect(c.outPorts.colors).to.be.an 'object'
       chai.expect(c.outPorts.canvas).to.be.an 'object'
+      chai.expect(c.outPorts.error).to.be.an 'object'

   describe 'when passed a canvas', ->
     input = 'colorful-octagon.png'
@@ -153,4 +157,9 @@
         done()
       id = getCanvasWithImage input, (canvas) ->
         ins.send canvas
-
+  describe 'when given not an image', ->
+    it 'should output no colors', (done) ->
+      error.on "data", (err) ->
+        chai.expect(err).to.be.an 'object'
+        done()
+      ins.send ''


Implement a very simple fuzzy search

Implement a very simple fuzzy search

--- SingularityUI/app/utils.coffee
+++ SingularityUI/app/utils.coffee
@@ -118,4 +118,13 @@
     @isOnDemandRequest: (request) ->
         not request.daemon and not utils.isScheduledRequest(request)

+    @fuzzySearchMatch: (query, word) ->
+        i = 0
+        for letter in query
+            if letter is ' '
+                continue
+            i = word.indexOf letter, i
+            return false if i is -1
+        return true
+
 module.exports = Utils


cleanup

cleanup

--- spec/juno-spec.coffee
+++ spec/juno-spec.coffee
@@ -11,18 +11,18 @@
     waitsForPromise ->
       atom.packages.activatePackage 'julia-client'

-describe "basic client interaction", ->
+describe "managing the Julia process", ->
+  client = juno.connection.client
+  clientStatus = -> [client.isConnected(), client.isActive(), client.isWorking()]
   it "recognises the client's state before boot", ->
-    expect(juno.connection.client.isConnected()).toBeFalsy()
-    expect(juno.connection.client.isActive()).toBeFalsy()
+    expect(clientStatus()).toEqual [false, false, false]

   bootPromise = null
   it "initiates the boot", ->
     bootPromise = juno.connection.boot()

   it "recognises the client's state during boot", ->
-    expect(juno.connection.client.isConnected()).toBe(false)
-    expect(juno.connection.client.isActive()).toBe(true)
+    expect(clientStatus()).toEqual [false, true, true]

   it "waits for the boot to complete", ->
     waitsForPromise ->
@@ -30,8 +30,7 @@
         expect(pong).toBe('pong')

   it "recognises the client's state after boot", ->
-    expect(juno.connection.client.isConnected()).toBe(true)
-    expect(juno.connection.client.isActive()).toBe(true)
+    expect(clientStatus()).toEqual [true, true, false]

   {echo, evalsimple} = juno.connection.client.import ['echo', 'evalsimple']

@@ -39,7 +38,7 @@
     msg = {x: 1, y: 2}
     waitsForPromise ->
       echo(msg).then (response) ->
-        expect(msg).toBe(msg)
+        expect(response).toEqual(msg)

     waitsForPromise ->
       evalsimple("2+2").then (result) ->


reverse accidental change on a file

reverse accidental change on a file

--- lib/epitools-headers.coffee
+++ lib/epitools-headers.coffee
@@ -1,25 +1,65 @@
-{CompositeDisposable} = require 'atom'
-# headers = require './headers-format.cson' # TODO : load a cson file
+{CompositeDisposable, TextEditor} = require 'atom'
+path = require 'path'
+headersFormat = require './headers-format.json'

 module.exports =
 class EpitoolsHeaders
-    activate: (state) ->
+    core: null
+
+    activate: (state, @core) ->
+        # core.config.headers = @config
         @subscriptions = new CompositeDisposable
         # Register command
         @subscriptions.add atom.commands.add 'atom-workspace', 'epitools:header-top': => @insertHeaderTop()
         @subscriptions.add atom.commands.add 'atom-workspace', 'epitools:header-cursor': => @insertHeaderCursor()

+    deactivate: ->
+        @subscriptions.dispose()
+
     refresh: (editor, activate) ->

     hasHeader: (editor) ->
+        buffer = editor.getTextInBufferRange([[0, 0], [10, 0]])
+
+    generateDate: ->
+        date = new Date
+        format = headersFormat.datesFormat
+        format.replace /MM/g, headersFormat.monthNames[date.getMonth()]
+            .replace /DD/g, date.getDate()
+            .replace /YYYY/g, date.getFullYear()
+            .replace /HH/g, date.getHours()
+            .replace /MI/g, date.getMinutes()
+            .replace /SS/g, date.getSeconds()
+
+    replaceInfo: (header, editor) ->
+        filePath = editor.getBuffer().getPath()
+        header.replace /\$FILE_NAME/g, path.basename filePath
+            .replace /\$PATH/g, path.dirname filePath
+            .replace /\$PROJECT/g, 'project'
+            .replace /\$USER_NAME/g, atom.config.get('epitools.headers.name') or ''
+            .replace /\$USER_LOGIN/g, atom.config.get('epitools.headers.login') or ''
+            .replace /\$USER_EMAIL/g, atom.config.get('epitools.headers.email') or ''
+            .replace /\$DATE/g, @generateDate()

     updateHeader: (editor) ->

     generateHeader: (editor) ->
-
-
-    insertHeader: (pos) ->
+        # generate header
+        project = '' # TODO : guess project name
+        scope = editor.getRootScopeDescriptor().scopes[0]
+        format = headersFormat.scopes[scope] or headersFormat.scopes.default
+        str = format.head + '\n'
+        for line in headersFormat.text
+            str += format.body + ' ' + line + '\n'
+        str += format.tail + '\n'
+        @replaceInfo str, editor

     insertHeaderTop: ->
+        editor = atom.workspace.getActivePaneItem()
+        return null if (editor not instanceof TextEditor) or not @core.isActive
+        header = @generateHeader editor
+        editor.setTextInBufferRange([[0, 0], [0, 0]], header)

     insertHeaderCursor: ->
+        editor = atom.workspace.getActivePaneItem()
+        return null if (editor not instanceof TextEditor) or not @core.isActive


display search box in default

display search box in default

--- lib/insert-link-view.coffee
+++ lib/insert-link-view.coffee
@@ -55,7 +55,6 @@
     @editor = atom.workspace.getActiveEditor()
     @setLinkFromSelection()
     atom.workspaceView.append(this)
-    @searchBox.hide()
     if @textEditor.getText()
       @urlEditor.getEditor().selectAll()
       @urlEditor.focus()
@@ -195,7 +194,7 @@

   fetchPosts: ->
     if posts
-      @searchBox.show() if posts.length > 0
+      @searchBox.hide() unless posts.length > 0
     else
       uri = atom.config.get("markdown-writer.urlForPosts")
       succeed = (body) =>


Update method names

Update method names

--- lib/keybinding-panel.coffee
+++ lib/keybinding-panel.coffee
@@ -24,12 +24,12 @@

   initialize: ->
     @keyBindings = _.sortBy(global.keymap.allBindings(), (x) -> x.keystroke)
-    @appendKeyMappings(@keyBindings)
+    @appendKeyBindings(@keyBindings)

     @filter.getBuffer().on 'contents-modified', =>
-      @filterKeyMappings(@keyBindings, @filter.getText())
+      @filterKeyBindings(@keyBindings, @filter.getText())

-  filterKeyMappings: (keyBindings, filterString) ->
+  filterKeyBindings: (keyBindings, filterString) ->
     @keybindingRows.empty()
     for keyBinding in keyBindings
       {selector, keystroke, command, source} = keyBinding
@@ -37,13 +37,13 @@
       continue unless searchString

       if /^\s*$/.test(filterString) or searchString.indexOf(filterString) != -1
-        @keybindingRows.append @elementForKeyMapping(keyBinding)
+        @keybindingRows.append @elementForKeyBinding(keyBinding)

-  appendKeyMappings: (keyBindings) ->
+  appendKeyBindings: (keyBindings) ->
     for keyBinding in keyBindings
-      @keybindingRows.append @elementForKeyMapping(keyBinding)
+      @keybindingRows.append @elementForKeyBinding(keyBinding)

-  elementForKeyMapping: (keyBinding) ->
+  elementForKeyBinding: (keyBinding) ->
     {selector, keystroke, command, source} = keyBinding
     source = @determineSource(source)
     $$$ ->


fix markdown-convert online image path bug

--- lib/markdown-convert.coffee
+++ lib/markdown-convert.coffee
@@ -39,7 +39,7 @@
   output = ''

   resolvePath = (src)->
-    if src.startsWith('/')
+    if src.startsWith('/') or src.startsWith('http://') or src.startsWith('https://') or src.startsWith('file://')
       return src
     else # ./test.png or test.png
       return '/' + path.relative(projectDirectoryPath, path.resolve(rootDirectoryPath, src))


:bug: Fix error raised if the subscription for a binding was already disposed

Fixes #1

--- lib/minimap-pigments.coffee
+++ lib/minimap-pigments.coffee
@@ -42,7 +42,7 @@
       @bindingsById[editor.id] = binding

       @subscriptionsById[editor.id] = editor.onDidDestroy =>
-        @subscriptionsById[editor.id].dispose()
+        @subscriptionsById[editor.id]?.dispose()
         binding.destroy()
         delete @subscriptionsById[editor.id]
         delete @bindingsById[editor.id]


Handle local moves

Handle local moves

--- lib/models/synced-fs.coffee
+++ lib/models/synced-fs.coffee
@@ -26,6 +26,7 @@
     @workspaceView = atom.views.getView(atom.workspace)
     @projectPath = atom.project.getPaths()[0]
     @handleEvents()
+    @recentTreeViewCommand = null

   expandTreeView: ->
     atom.commands.dispatch(@workspaceView, 'tree-view:reveal-active-file')
@@ -35,6 +36,8 @@
       switch event.type
         when 'tree-view:add-file', 'tree-view:add-folder'
           @onTreeViewAdd(event)
+        when 'tree-view:move'
+          @onTreeViewMove(event)

     atom.commands.onDidDispatch (event) =>
       console.log event.type
@@ -61,67 +64,109 @@
     return unless inCodeDir

     @popupNoConnectionWarning() if @connectionState is 'closed'
-    @sendSave(editor.project, file, buffer)
+    @sendLocalEvent @localSave(editor.project, file, buffer)

   onCoreConfirm: (event) ->
     @syncAdditions()
-
-  syncAdditions: ->
-    newEntries = _.difference(fs.listTreeSync(@projectPath), @listTreeAtAdd)
-    return if _.isEmpty(newEntries)
-
-    deepestPath = _.max(newEntries, (entry) -> entry.length)
-    @sendAddFile(deepestPath) if fs.isFileSync(deepestPath)
-    @sendAddFolder(deepestPath) if fs.isDirectorySync(deepestPath)
+    @syncMoves()

   onTreeViewRemove: (event) ->
     {target} = event
     path = target.dataset.path || target.firstChild.dataset.path
     return if fs.existsSync(path)

-    @sendRemove(path)
+    @sendLocalEvent @localRemove(path)

   onTreeViewAdd: (event) =>
+    @recentTreeViewCommand = event.type
     @listTreeAtAdd = fs.listTreeSync(@projectPath)

-  sendAddFile: (path) ->
-    ipc.send 'fs-local-add-file', JSON.stringify(
-      action: 'local_add_file'
-      project:
-        path: @formatPath(@projectPath)
-      file:
-        path: @formatPath(path)
-      )
-
-  sendAddFolder: (path) ->
-    ipc.send 'fs-local-add-folder', JSON.stringify(
-      action: 'local_add_folder'
-      project:
-        path: @formatPath(@projectPath)
-      file:
-        path: @formatPath(path)
-      )
-
-  sendSave: (project, file, buffer) ->
-    ipc.send 'fs-local-save', JSON.stringify(
-      action: 'local_save'
-      project:
-        path: @formatPath(project.getPaths()[0])
-      file:
-        path: @formatPath(file.path)
-        digest: file.digest,
-      buffer:
-        content: window.btoa(unescape(encodeURIComponent(buffer.getText())))
-      )
-
-  sendRemove: (path) ->
-    ipc.send 'fs-local-delete', JSON.stringify(
-      action: 'local_delete'
-      project:
-        path: @formatPath(@projectPath)
-      file:
-        path: @formatPath(path)
-      )
+  onTreeViewMove: (event) =>
+    {target, type} = event
+    @recentTreeViewCommand = type
+    @listTreeAtMove = fs.listTreeSync(@projectPath)
+    @pathAtMove = target.dataset.path || target.firstChild.dataset.path
+
+  syncAdditions: ->
+    prevEntries = @listTreeAtAdd
+    @listTreeAtAdd = null
+
+    return unless @recentTreeViewCommand.match(/tree-view:add/)
+    @recentTreeViewCommand = null
+
+    return unless prevEntries?
+
+    newEntries = _.difference(fs.listTreeSync(@projectPath), prevEntries)
+    return unless newEntries.length
+
+    deepestPath = _.max(newEntries, (entry) -> entry.length)
+    @sendLocalEvent @localAddFile(deepestPath) if fs.isFileSync(deepestPath)
+    @sendLocalEvent @localAddFolder(deepestPath) if fs.isDirectorySync(deepestPath)
+
+  syncMoves: ->
+    prevPath = @pathAtMove
+    prevEntries = @listTreeAtMove
+    @pathAtMove = null
+    @listTreeAtMove = null
+
+    debugger
+    return unless @recentTreeViewCommand is 'tree-view:move'
+    @recentTreeViewCommand = null
+
+    console.log '>>> 3'
+    return unless prevPath? and prevEntries?
+
+    console.log '>>> 2'
+    newEntries = _.difference(fs.listTreeSync(@projectPath), prevEntries)
+    return unless newEntries.length
+
+    console.log '>>> 1'
+    deepestPath = _.max(newEntries, (entry) -> entry.length)
+    console.log "prev: #{prevPath} new: #{deepestPath}"
+    @sendLocalEvent @localMove(prevPath, deepestPath)
+
+  sendLocalEvent: (payload) ->
+    ipc.send 'fs-local-event', JSON.stringify(payload)
+
+  localAddFile: (path) ->
+    action: 'local_add_file'
+    project:
+      path: @formatPath(@projectPath)
+    file:
+      path: @formatPath(path)
+
+  localAddFolder: (path) ->
+    action: 'local_add_folder'
+    project:
+      path: @formatPath(@projectPath)
+    file:
+      path: @formatPath(path)
+
+  localSave: (project, file, buffer) ->
+    action: 'local_save'
+    project:
+      path: @formatPath(project.getPaths()[0])
+    file:
+      path: @formatPath(file.path)
+      digest: file.digest,
+    buffer:
+      content: window.btoa(unescape(encodeURIComponent(buffer.getText())))
+
+  localRemove: (path) ->
+    action: 'local_delete'
+    project:
+      path: @formatPath(@projectPath)
+    file:
+      path: @formatPath(path)
+
+  localMove: (source, target) ->
+    console.log '>>> 0'
+    action: 'local_move'
+    project:
+      path: @formatPath(@projectPath)
+    file:
+      path: @formatPath(target)
+    from: @formatPath(source)

   formatPath: (path) ->
     if path.match(/:\\/)
@@ -132,7 +177,6 @@
   convertLineEndings: (editor) ->
     editorElement = atom.views.getView(editor)
     atom.commands.dispatch(editorElement, 'line-ending-selector:convert-to-LF')
-

   popupNoConnectionWarning: ->
     noConnectionPopup = document.createElement 'div'


fix firefox

fix firefox

--- src/Stylesheet.coffee
+++ src/Stylesheet.coffee
@@ -334,8 +334,7 @@
       return ''
     return @key

-  continue: (engine, operation, continuation = '') ->
-    return continuation#engine.Query::continue.apply(@, arguments) + @DESCEND
+  continue: Query::continue#.apply(@, arguments) + @DESCEND

 class Stylesheet.Import extends Query
   type: 'Import'
@@ -361,26 +360,26 @@
         node = undefined
       else
         unless src = @getUrl(node)
-          text = node.innerText
+          text = node.innerText || node.textContent

         type ||= node.getAttribute?('type')

-
       path = @getGlobalPath(engine, operation, continuation, node)
+
       if stylesheet = engine.queries[path]
         command = stylesheet.command
-        stylesheet.splice(0)
-        if node.parentNode
-          command.users = 0
-          @uncontinuate(engine, path)
-          if text
-            stylesheet.push.apply(stylesheet, command.parse(engine, type, text))
-            @continuate(engine, path)
-            return
-        else
-          debugger
-          @clean(engine, path)
-          return
+        if stylesheet.length
+          stylesheet.splice(0)
+          if node.parentNode
+            command.users = 0
+            @uncontinuate(engine, path)
+            if text
+              stylesheet.push.apply(stylesheet, command.parse(engine, type, text))
+              @continuate(engine, path)
+              return
+          else
+            @clean(engine, path)
+            return
       else
         stylesheet = []
         command = stylesheet.command = new Stylesheet(engine, operation, continuation, node)


fix xss in some currently unused code

fix xss in some currently unused code

this UI is not currently accessible and is probably going away, but just
in case...

Change-Id: I6a1fb50696a66f6fd6855d7c7b37a3d4d859b377
Reviewed-on: https://gerrit.instructure.com/46291
Tested-by: Jenkins <d95b56ce41a2e1ac4cecdd398defd7414407cc08@instructure.com>
Reviewed-by: Cody Cutrer <c4fe2b1d90ef8f2548c8aeabfa633434316f0305@instructure.com>
Product-Review: Jon Jensen <44f878afe53efc66b76772bd845eb65944ed8232@instructure.com>
QA-Review: Jon Jensen <44f878afe53efc66b76772bd845eb65944ed8232@instructure.com>

--- app/coffeescripts/ember/modules/components/mm_sortable_module_component.coffee
+++ app/coffeescripts/ember/modules/components/mm_sortable_module_component.coffee
@@ -38,7 +38,7 @@
       rect = @get('element').getBoundingClientRect()
       x = event.originalEvent.clientX - rect.left
       ghost = $('<div class="module-ghost"/>')
-      ghost.html(@get('module.name'))
+      ghost.text(@get('module.name'))
       ghost.appendTo(document.body)
       event.dataTransfer.setDragImage(ghost[0], x, 10)
       Ember.run.later(ghost, 'remove', 0)


Move unregister_all_mocked_methods() into class AllExpectations.

--- src/TinyMockJS.coffee
+++ src/TinyMockJS.coffee
@@ -12,7 +12,7 @@
     AllExpectations.verify_all_expectations()
   finally
     uninstall_expects_method()
-    uninstall_all_mocked_methods()
+    AllExpectations.uninstall_all_mocked_methods()
     AllExpectations.unregister_all_expectations()

 install_expects_method = ->
@@ -20,11 +20,6 @@

 uninstall_expects_method = ->
   delete Object.prototype.expects
-
-# TODO: write comment about uninstalling in reverse
-# TODO: move into AllExpectations
-uninstall_all_mocked_methods = ->
-  expectation.uninstall_mocked_method() for expectation in AllExpectations._expectations by -1

 build_convenience_mock_objects = ->
   ( new Object() for i in [1..5] )
@@ -87,6 +82,10 @@
 	@verify_all_expectations: ->
 		errors = AllExpectations._find_all_errors()
 		throw new Error(errors) unless errors == ""
+
+	# TODO: write comment about uninstalling in reverse
+	@uninstall_all_mocked_methods: ->
+	  expectation.uninstall_mocked_method() for expectation in AllExpectations._expectations by -1

 	@unregister_all_expectations: ->
 		AllExpectations._expectations.length = 0


add elm

add elm

--- atom/packages.cson
+++ atom/packages.cson
@@ -28,6 +28,7 @@
   "language-diff"
   "language-docker"
   "language-elixir"
+  "language-elm"
   "language-lisp"
   "language-nginx"
   "language-rust"
@@ -38,6 +39,7 @@
   "linter-csslint"
   "linter-doiuse"
   "linter-elixirc"
+  "linter-elm-make"
   "linter-erb"
   "linter-erlc"
   "linter-golint"


decoding html characters for invitation text areas

--- client/app/Applications/Groups.kdapplication/Views/groupsinvitationview.coffee
+++ client/app/Applications/Groups.kdapplication/Views/groupsinvitationview.coffee
@@ -115,7 +115,7 @@
           label          : 'Message'
           type           : 'textarea'
           cssClass       : 'message-input'
-          defaultValue   : @policy.communications?.inviteApprovedMessage or @getDefaultInvitationMessage()
+          defaultValue   : Encoder.htmlDecode @policy.communications?.inviteApprovedMessage or @getDefaultInvitationMessage()
           validate       :
             rules        :
               required   : yes
@@ -147,7 +147,7 @@
           label          : 'Message'
           type           : 'textarea'
           cssClass       : 'message-input'
-          defaultValue   : @policy.communications?.invitationMessage or @getDefaultInvitationMessage()
+          defaultValue   : Encoder.htmlDecode @policy.communications?.invitationMessage or @getDefaultInvitationMessage()
           validate       :
             rules        :
               required   : yes


clarify help

clarify help

--- src/tf.coffee
+++ src/tf.coffee
@@ -30,10 +30,11 @@
       "#{tfName} create key - create rsa key, for git operations"
       "#{tfName} destroy key - erase key"
       "#{tfName} show key - display public key"
-      "#{tfName} clone <repourl> <projectname>"
+      "#{tfName} clone <repourl> <projectname> - clone git repo into projectname directory"
       "#{tfName} (plan|refresh|apply|get|destroy) <projectname> - tf operations"
-      "#{tfName} env <projectname> set <key>=<value>"
-      "#{tfName} env <projectname> unset <key>"
+      "#{tfName} env <projectname> set <key>=<value> - set env var"
+      "#{tfName} env <projectname> unset <key> - unset environmental variable"
+      "#{tfName} env <projectname> list - show env for project"
     ]

     for str in arr


abstract fileExist check into fileMissingSendAndReturnTrue, etc functions

--- src/tf.coffee
+++ src/tf.coffee
@@ -34,6 +34,19 @@
     return true
   msg.send {room: msg.message.user.name}, "Not authorized.  Missing #{tfRole} role."
   return false
+
+fileExistsSendAndReturnTrue = (msg, file, failresponse) ->
+  if fs.existsSync file
+    msg.send {room: msg.message.user.name}, failresponse
+    return true
+  return false  # does not exist
+
+fileMissingSendAndReturnTrue = (msg, file, failresponse) ->
+  if ! fs.existsSync file
+    msg.send {room: msg.message.user.name}, failresponse
+    return true
+  return false  # file exists
+

 module.exports = (robot) ->

@@ -62,9 +75,7 @@

   robot.respond /tf create key$/i, (msg) ->
     return unless isAuthorized robot, msg
-
-    if fs.existsSync("#{publickey}")
-      return msg.send {room: msg.message.user.name}, "Key exists!  Destroy it first."
+    return if fileExistsSendAndReturnTrue msg, publickey, "Key exists!  Destroy it first."

     exec "ssh-keygen -f #{privatekey} -b 1024 -C hubot-tf -N ''", (error, stdout, stderr) ->
       #msg.send {room: msg.message.user.name}, "```\n#{stdout}\n```"
@@ -73,18 +84,14 @@

   robot.respond /tf show key$/i, (msg) ->
     return unless isAuthorized robot, msg
-
-    if ! fs.existsSync("#{publickey}")
-      return msg.send {room: msg.message.user.name}, "No key on file!  Create one first."
+    return if fileMissingSendAndReturnTrue msg, publickey, "No key on file!  Create one first."

     pubkey = fs.readFileSync("#{publickey}", 'utf-8').toString()
     return msg.send {room: msg.message.user.name}, "```\n#{pubkey}\n```"

   robot.respond /tf destroy key$/i, (msg) ->
     return unless isAuthorized robot, msg
-
-    if ! fs.existsSync("#{publickey}")
-      return msg.send {room: msg.message.user.name}, "No key on file!  Create one first."
+    return if fileMissingSendAndReturnTrue msg, publickey, "No key on file!  Create one first."

     fs.unlinkSync("#{privatekey}")
     fs.unlinkSync("#{publickey}")
@@ -120,12 +127,9 @@
     return msg.send {room: msg.message.user.name}, dir.join "\n"

   robot.respond /tf remote ([^\s]+)$/i, (msg) ->
-    return unless isAuthorized robot, msg
-
-    projname = msg.match[1].replace /\//, "_"
-
-    unless fs.existsSync("#{basepath}/#{projname}")
-      return msg.send {room: msg.message.user.name}, "Invalid project name: `#{projname}`"
+    projname = msg.match[1].replace /\//, "_"
+    return unless isAuthorized robot, msg
+    return if fileMissingSendAndReturnTrue msg, "#{basepath}/#{projname}", "Invalid project name: `#{projname}`"

     exec "cd #{basepath}/#{projname} ; git remote -v", (error, stdout, stderr) ->
       if stderr
@@ -136,13 +140,10 @@
         msg.send {room: msg.message.user.name}, "```\n#{stdout}\n```"

   robot.respond /tf (get) ([^\s]+)$/i, (msg) ->
-    return unless isAuthorized robot, msg
-
     action = msg.match[1]
     projname = msg.match[2].replace /\//, "_"
-
-    unless fs.existsSync("#{basepath}/#{projname}")
-      return msg.send {room: msg.message.user.name}, "Invalid project name: `#{projname}`"
+    return unless isAuthorized robot, msg
+    return if fileMissingSendAndReturnTrue msg, "#{basepath}/#{projname}", "Invalid project name: `#{projname}`"

     exec "cd #{basepath}/#{projname}; terraform #{action} -no-color", (error, stdout, stderr) ->
       if stderr
@@ -153,25 +154,19 @@
         msg.send {room: msg.message.user.name}, "```\n#{stdout}\n```"

   robot.respond /tf delete ([^\s]+)$/i, (msg) ->
-    return unless isAuthorized robot, msg
-
-    projname = msg.match[1].replace /\//, "_"
-
-    unless fs.existsSync("#{basepath}/#{projname}")
-      return msg.send {room: msg.message.user.name}, "Invalid project name: `#{projname}`"
+    projname = msg.match[1].replace /\//, "_"
+    return unless isAuthorized robot, msg
+    return if fileMissingSendAndReturnTrue msg, "#{basepath}/#{projname}", "Invalid project name: `#{projname}`"

     return exec "cd #{basepath}; rm -rf #{projname}", (error, stdout, stderr) ->
       msg.send {room: msg.message.user.name}, "Project deleted: #{projname}"

   robot.respond /tf (plan|refresh|apply|destroy) ([^\s]+)( verbose)?$/i, (msg) ->
-    return unless isAuthorized robot, msg
-
     action = msg.match[1]
     projname = msg.match[2].replace /\//, "_"
     verbose = true if msg.match[3]
-
-    unless fs.existsSync("#{basepath}/#{projname}")
-      return msg.send {room: msg.message.user.name}, "Invalid project name: `#{projname}`"
+    return unless isAuthorized robot, msg
+    return if fileMissingSendAndReturnTrue msg, "#{basepath}/#{projname}", "Invalid project name: `#{projname}`"

     brainloc = "hubot-tf_#{projname}"
     localstorage = JSON.parse(robot.brain.get brainloc) or {}
@@ -212,14 +207,11 @@
         msg.send {room: msg.message.user.name}, "error:\n```\n#{error}\n```"

   robot.respond /tf env ([^\s]+) set ([^\s]+)=(.+)$/i, (msg) ->
-    return unless isAuthorized robot, msg
-
     projname = msg.match[1].replace /\//, "_"
     ekey = msg.match[2]
     evalue = msg.match[3]
-
-    unless fs.existsSync("#{basepath}/#{projname}")
-      return msg.send {room: msg.message.user.name}, "Invalid project name: `#{projname}`"
+    return unless isAuthorized robot, msg
+    return if fileMissingSendAndReturnTrue msg, "#{basepath}/#{projname}", "Invalid project name: `#{projname}`"

     brainloc = "hubot-tf_#{projname}"
     localstorage = JSON.parse(robot.brain.get brainloc) or {}
@@ -230,13 +222,10 @@
     return msg.send {room: msg.message.user.name}, "`#{projname}` env set: `#{ekey}` = `#{evalue}`"

   robot.respond /tf env ([^\s]+) unset ([^\s]+)$/i, (msg) ->
-    return unless isAuthorized robot, msg
-
     projname = msg.match[1].replace /\//, "_"
     ekey = msg.match[2]
-
-    unless fs.existsSync("#{basepath}/#{projname}")
-      return msg.send {room: msg.message.user.name}, "Invalid project name: `#{projname}`"
+    return unless isAuthorized robot, msg
+    return if fileMissingSendAndReturnTrue msg, "#{basepath}/#{projname}", "Invalid project name: `#{projname}`"

     brainloc = "hubot-tf_#{projname}"
     localstorage = JSON.parse(robot.brain.get brainloc) or {}
@@ -247,12 +236,9 @@
     return msg.send {room: msg.message.user.name}, "`#{projname}` env `#{ekey}` unset."

   robot.respond /tf env ([^\s]+)(?:\slist)?$/i, (msg) ->
-    return unless isAuthorized robot, msg
-
-    projname = msg.match[1].replace /\//, "_"
-
-    unless fs.existsSync("#{basepath}/#{projname}")
-      return msg.send {room: msg.message.user.name}, "Invalid project name: `#{projname}`"
+    projname = msg.match[1].replace /\//, "_"
+    return unless isAuthorized robot, msg
+    return if fileMissingSendAndReturnTrue msg, "#{basepath}/#{projname}", "Invalid project name: `#{projname}`"

     brainloc = "hubot-tf_#{projname}"
     localstorage = JSON.parse(robot.brain.get brainloc) or {}


Prevent columns from breaking on footnotes

--- vendor/assets/javascripts/regionFlow.coffee
+++ vendor/assets/javascripts/regionFlow.coffee
@@ -159,6 +159,21 @@
   populateRegions: ->
     @lastRegion().appendNode(node.clone()) for node in @contentNodes

+
+  # Verify that a node can be separated from it's predecessor. Used to prevent
+  # breaks on context-dependent elements, e.g., footnote references.
+  #
+  contextDependent:(node) ->
+    conditions = [
+      -> Boolean(@nodeName is 'SUP')
+    ]
+    result = false
+    i = conditions.length
+    while i--
+      result = conditions[i].call(node)
+      if result then break
+    result
+
   # Recursive layout call.
   #
   # @params options - hash
@@ -182,7 +197,7 @@
       formerParent = $(childNode).parent()
       $(childNode).remove().appendTo(targetNode)

-      if @oversetRegion().updateOverset() is 'overset'
+      if @oversetRegion().updateOverset() is 'overset' and not @contextDependent(childNode)

         # move it back where it was
         $(childNode).remove().prependTo(formerParent)
@@ -197,7 +212,6 @@

       else if $(childNode).hasClass('break-after-always')
         return false
-

   # Helper function, recurses up the DOM looking for a block element
   # @params elem - jQuery object
@@ -210,9 +224,6 @@
       elem
     return blockElem

-
-
-
   # Given a text node and a target node (in the overset region), find the number
   # of words which fit.
   #
@@ -223,8 +234,7 @@
   breakUpText: (options = {}) ->
     textNode = options.node
     targetNode = document.createTextNode("")
-    inlineElements =
-    $(targetNode).appendTo(options.into)
+    inlineElements = $(targetNode).appendTo(options.into)

     words = textNode.nodeValue.split(/[ ]+/)
     highIndex = words.length - 1
@@ -248,7 +258,7 @@
     else
       targetNode.textContent = words[0..tryIndex].join(" ")
       textNode.nodeValue = words[tryIndex+1..words.length-1].join(" ")
-      $parentElem = @getBlockParent $(textNode).parent() # prevents us from attaching classes to <em>, <i>, etc
+      $parentElem = @getBlockParent $(textNode).parent() # prevents us from attaching classes to inline elements
       $parentElem.addClass("region-flow-post-text-break")

     @oversetRegion().updateOverset() unless RegionFlow::lazyMode


trying windows path fixs

trying windows path fixs

--- lib/build.coffee
+++ lib/build.coffee
@@ -116,8 +116,8 @@
           fileBaseName = atom.workspace.getActiveTextEditor().buffer.file.getBaseName()
           folderNamePath = path.replace ///#{fileBaseName}///, ''
           folderNamePath = folderNamePath.replace pathHeRegex, ''
-          var isWin = /^win/.test(process.platform)
-          folderName =  if !isWin ? folderNamePath.split "/" : folderNamePath.split("\\/")
+          isWin = /^win/.test(process.platform)
+          folderName =  if isWin then folderNamePath.split("\\/") else folderNamePath.split "/"
           fileName = fileBaseName.split "."
           if(fileName.length > 1)
             fileNameParsed = if fileName.length > 2 then fileName[0]+'.'+fileName[1] else fileName[0]


Added news

Added news

--- server/materials/data/news.coffee
+++ server/materials/data/news.coffee
@@ -3,8 +3,8 @@

 export default allNews = () ->
     return news([
-        newsItem("21 апреля занятия в лицее не будет", String.raw"""
-            21 апреля занятия в лицее 40 не будет. Занимайтесь из дома.
+        newsItem("Смена «Информатика.Регионы» в Сириусе", String.raw"""
+            Школьники 7-9 классов (текущего учебного года), обратите внимание на ноябрьскую смену «Информатике.Регионы» в Сириусе. Настоятельно рекомендую попробовать туда поехать.
         """),
         newsItem("Олимпиада им. В. Д. Лелюха", String.raw"""
             Добавлена информация про Нижегородскую олимпиаду школьников по информатике им. В. Д. Лелюха.


Only show current file errors in linter status bar

--- atom/config.cson
+++ atom/config.cson
@@ -123,6 +123,7 @@
     panelHeight: 200
     showPanel: true
     showTooltip: false
+    statusBarRepresents: "Current File"
   "markdown-preview":
     useGitHubStyle: true
   "one-dark-ui":


Atom font goes to 11

Atom font goes to 11

--- atom/config.cson
+++ atom/config.cson
@@ -12,7 +12,7 @@
     autoIndentOnPaste: false
     scrollPastEnd: true
     invisibles: {}
-    fontSize: 13
+    fontSize: 11
   "spell-check":
     grammars: [
       "text.plain"


Désactivation de la télémétrie

Désactivation de la télémétrie

--- atom/config.cson
+++ atom/config.cson
@@ -12,7 +12,7 @@
       "linter"
     ]
     excludeVcsIgnoredPaths: false
-    telemetryConsent: "limited"
+    telemetryConsent: "no"
     themes: [
       "one-dark-ui"
       "monokai"


Switch to using dot swatches for pigments

--- atom/config.cson
+++ atom/config.cson
@@ -74,3 +74,4 @@
       "\\.comment"
       "\\.string"
     ]
+    markerType: "dot"


Don't hide VCS ignored files in Atom tree view

--- atom/config.cson
+++ atom/config.cson
@@ -95,5 +95,6 @@
     leftEditorColor: "red"
     rightEditorColor: "green"
     syncHorizontalScroll: true
+  "tree-view": {}
   welcome:
     showOnStartup: false


Change write-good warnings level

Change write-good warnings level

Former-commit-id: 61a1de89fd83a17a5a94be20b575647a6504ef93 [formerly f3a4232946a034db4178af3a8f1630ac765a9026]
Former-commit-id: 0b3185abe1dce82a904a61c3a197c00aa9b76092

--- atom/config.cson
+++ atom/config.cson
@@ -28,6 +28,8 @@
     userId: "c9c9df71-2011-aac7-d454-6313fe94f6d9"
   linter:
     showErrorPanel: false
+  "linter-write-good":
+    severityLevel: "Warning"
   minimap:
     absoluteMode: true
     adjustAbsoluteModeHeight: true


Add selectors for spell checking.

Add selectors for spell checking.

--- atom/config.cson
+++ atom/config.cson
@@ -27,3 +27,9 @@
   'ensureSingleTrailingNewline': true
 'auto-copyright':
   'owner': 'Lifted Studios'
+'spell-check':
+  'grammars': [
+    'source.gfm'
+    'text.git-commit'
+    'text.plain'
+  ]


Disable 'exceptino-reporting' so userId GUID isn’t checked in

--- atom/config.cson
+++ atom/config.cson
@@ -1,5 +1,8 @@
 "*":
   core:
+    disabledPackages: [
+      "exception-reporting"
+    ]
     telemetryConsent: "limited"
     themes: [
       "one-light-ui"


Add ctrl-alt-cmd-\ key binding for toggling tree view

--- atom/keymap.cson
+++ atom/keymap.cson
@@ -5,8 +5,9 @@
   # Disable cmd-o because it always gets in the way of typing cmd-i
   'cmd-o': 'unset!'

-  # Disable cmd-\ for toggling tree view because I always accidentally hit it
+  # Rebind key binding for toggling tree view to something not so easily pressed
   'cmd-\\': 'unset!'
+  'ctrl-alt-cmd-\\': 'tree-view:toggle'

 '.platform-darwin atom-workspace':


If selecting 'output.pdf', set the filename to project-name.pdf

--- services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee
+++ services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee
@@ -253,9 +253,13 @@
 			# auto-set filename based on selected file
 			$scope.$watch 'data.selectedProjectOutputFile', (newVal, oldVal) ->
 				return if !newVal
-				fileName = newVal.split('/').reverse()[0]
-				if fileName
-					$scope.data.name = fileName
+				if newVal == 'output.pdf'
+					project = _.find($scope.data.projects, (p) -> p._id == $scope.data.selectedProjectId)
+					$scope.data.name = if project?.name? then "#{project.name}.pdf" else 'output.pdf'
+				else
+					fileName = newVal.split('/').reverse()[0]
+					if fileName
+						$scope.data.name = fileName

 			_setInFlight = (type) ->
 				$scope.state.inFlight[type] = true


clean up docHistory pack decoding

clean up docHistory pack decoding

--- app/coffee/MongoManager.coffee
+++ app/coffee/MongoManager.coffee
@@ -69,37 +69,47 @@
 			extraQuery[param] = {'$lt' : rangeQuery['$gte']}
 		else if rangeQuery?['$gt']
 			extraQuery[param] = {'$lte' : rangeQuery['$gt']}
+		else
+			delete extraQuery[param]

 		filterFn = (item) ->
-			#console.log 'filter', item, rangeQuery
 			return false if rangeQuery?['$gte']? && item[param] < rangeQuery['$gte']
 			return false if rangeQuery?['$lte']? && item[param] > rangeQuery['$lte']
 			return false if rangeQuery?['$lt']? && item[param] >= rangeQuery['$lt']
 			return false if rangeQuery?['$gt']? && item[param] <= rangeQuery['$gt']
-			#console.log 'accepted'
 			return true

-		# need to support limit here
-
-		cursor.toArray (err, updates) ->
-			console.log 'query=', query, 'UPDATES=', updates
-			all = MongoManager._unpackResults(updates).filter(filterFn)
-			if all.length == 0
+		needMore = false
+		cursor.toArray (err, result) ->
+			unpackedSet = MongoManager._unpackResults(result)
+			updates = unpackedSet.filter(filterFn)
+			updates = updates.slice(0, limit) if limit?
+			last = if unpackedSet.length then unpackedSet[unpackedSet.length-1] else null
+			if limit? && updates.length == limit
+				needMore = false
+			else if extraQuery[param]? && last? && filterFn(last)
+				needMore = true
+			else if extraQuery[param]? && updates.length == 0
+				needMore = true
+			if needMore
 				# need an extra result set
-				console.log 'extraQuery', extraQuery
 				extra = db.docHistory
 					.find(extraQuery)
 					.sort(sort)
 					.limit(1)
-				extra.toArray (err, updates2) ->
-					all2 = MongoManager._unpackResults(updates2).filter(filterFn)
-					console.log 'got extra', all2
-					callback err, all2
+				extra.toArray (err, result2) ->
+					if err?
+						return callback err, updates
+					else
+						extraSet = MongoManager._unpackResults(result2).filter(filterFn)
+						updates = updates.concat extraSet
+						updates = updates.slice(0, limit) if limit?
+						callback err, updates
 				return
 			if err?
+				callback err, result
+			else
 				callback err, updates
-			else
-				callback err, all

 	_unpackResults: (updates) ->
 		result = []
@@ -121,7 +131,7 @@
 			item.project_id = project_id
 			item
 		return result.reverse()
-
+
 	getDocUpdates:(doc_id, options = {}, callback = (error, updates) ->) ->
 		query =
 			doc_id: ObjectId(doc_id.toString())


get rid of debuger

get rid of debuger

--- app/components/new-task.coffee
+++ app/components/new-task.coffee
@@ -1,7 +1,6 @@
 NewTaskComponent = Ember.Component.extend
   actions:
     save: ->
-      debugger
       if @get('task')?
         @get('task').save()


form_controls_fixed fix doubling buttons


Removed introId from make a new app button.

--- client/landing/app/Applications/StartTab.kdapplication/AppView.coffee
+++ client/landing/app/Applications/StartTab.kdapplication/AppView.coffee
@@ -45,7 +45,6 @@
       icon        : yes
       iconClass   : "plus-black"
       title       : "Make a new App"
-      introId     : "makeANewApp"
       callback    : =>
         @appsController.makeNewApp()


Test deduplicating a file by merging ozaria and codecombat copies

One commit for making the files identical and then symlinking oz copy.

`git subrepo push` to make the change on the codecombat side
`git push` to make the change on the ozaria side

--- app/core/api/users.coco.coffee
+++ app/core/api/users.coco.coffee
@@ -93,6 +93,11 @@
       method: 'PUT'
     }))

+  fetchCreatorOfPrepaid: ({prepaidId}, options={}) ->
+    fetchJson("/db/prepaid/#{prepaidId}/creator", _.assign({}, options, {
+      method: 'GET'
+    }))
+
   provisionSubscription: ({ userId }) ->
     fetchJson("/db/user/#{userId}/provision-subscription", _.assign({}, {
       method: 'PUT'


Update zh-HANS.coffee (#5755)

Update zh-HANS.coffee (#5755)

--- app/locale/zh-HANS.coco.coffee
+++ app/locale/zh-HANS.coco.coffee
@@ -782,7 +782,7 @@
     goal_survive_time: "生存 __seconds__ 秒。"
     goal_defeat: "击败所有怪物。"
     goal_defeat_amount: "击败 __amount__ 个怪物。"
-    goal_move: "移动的所有的红X标记。"
+    goal_move: "移动到所有红X标记。"
     goal_collect: "收集所有物品。"
     goal_collect_amount: "收集 __amount__ 个物品。"


Removed unused angular-xeditable optionos object, fixes issue #105

--- src/coffee/app.coffee
+++ src/coffee/app.coffee
@@ -59,8 +59,6 @@

   Steps.run Array '$rootScope', '$http', '$window', '$location', (scope, http, $window, $loc) ->
     scope.startingPoints = []
-    editableOptions = {}
-    editableOptions.theme = 'bs3'

     scope.$on '$routeChangeSuccess', (event, route) ->
       $window.scrollTo 0, 0


Fat arrows

Fat arrows

--- src/coffee/run.coffee
+++ src/coffee/run.coffee
@@ -141,12 +141,12 @@
         sftpClient.openSftp()
         .then (sftp) =>
           fs.list(@outputDir)
-          .then (files) ->
+          .then (files) =>
             logger.info "About to upload #{_.size files} file(s) from #{@outputDir} to #{sftpTarget}"
-            Qutils.processList files, (filename) ->
+            Qutils.processList files, (filename) =>
               logger.debug "Uploading #{@outputDir}/#{filename}"
               sftpClient.safePutFile(sftp, "#{@outputDir}/#{filename}", "#{sftpTarget}/#{filename}")
-              .then ->
+              .then =>
                 xml = _.find @orderReferences, (r) -> r.name is filename
                 if xml
                   logger.debug "About to sync order #{filename}"


Use eslint indent as default for jsx indents.

--- lib/auto-indent.coffee
+++ lib/auto-indent.coffee
@@ -41,7 +41,6 @@
       ]
       jsxIndent: [1,1]            # 1 = enabled, 1=#tabs
       jsxIndentProps: [1,1]       # 1 = enabled, 1=#tabs
-      indent: [1,1]               # 1 = enabled, 1=#tabs

     # regex to search for tag open/close tag and close tag
     @JSXREGEXP = /(<)([$_A-Za-z](?:[$_.:\-A-Za-z0-9])*)|(\/>)|(<\/)([$_A-Za-z](?:[$._:\-A-Za-z0-9])*)(>)|(>)|({)|(})|(\?)|(:)/g
@@ -486,11 +485,11 @@
       when JSXBRACE_OPEN
         @indentRow({row: row, blockIndent: token.firstCharIndentation, jsxIndent: 1 })
       when BRACE_OPEN
-        @indentRow({row: row, blockIndent: token.firstCharIndentation, jsIndent: 1 })
+        @indentRow({row: row, blockIndent: token.firstCharIndentation, jsxIndent: 1 })
       when JSXTAG_SELFCLOSE_END, JSXBRACE_CLOSE, JSXTAG_CLOSE_ATTRS
         @indentRow({row: row, blockIndent: tokenStack[token.parentTokenIdx].firstCharIndentation, jsxIndentProps: 1})
       when BRACE_CLOSE
-        @indentRow({row: row, blockIndent: tokenStack[token.parentTokenIdx].firstCharIndentation, jsIndent: 1 })
+        @indentRow({row: row, blockIndent: tokenStack[token.parentTokenIdx].firstCharIndentation, jsxIndent: 1 })

   # get the token at the given match position or return truthy false
   getToken: (bufferRow, match) ->
@@ -553,40 +552,45 @@

       return if not eslintRules?

+      ES_DEFAULT_INDENT = 4 # default eslint indent as spaces
+
+
+      # read indent if it exists and use it as the default indent for JSX
       rule = eslintRules['indent']
-      if typeof rule is 'number'
-        @eslintIndentOptions.indent[0] = rule
-        @eslintIndentOptions.indent[1] = 4 / @atomTabLength
+      if typeof rule is 'number' or typeof rule is 'string'
+        defaultIndent  = ES_DEFAULT_INDENT / @atomTabLength
       else if typeof rule is 'object'
-        @eslintIndentOptions.indent[0] = rule[0]
         if typeof rule[1] is 'number'
-          @eslintIndentOptions.indent[1] = rule[1] / @atomTabLength
-        else @eslintIndentOptions.indent[1] = 1
+          defaultIndent  = rule[1] / @atomTabLength
+        else defaultIndent  = 1
+      else defaultIndent  = 1

       rule = eslintRules['react/jsx-indent']
-      if typeof rule is 'number'
+      if typeof rule is 'number' or typeof rule is 'string'
         @eslintIndentOptions.jsxIndent[0] = rule
-        @eslintIndentOptions.jsxIndent[1] = 4 / @atomTabLength
+        @eslintIndentOptions.jsxIndent[1] = ES_DEFAULT_INDENT / @atomTabLength
       else if typeof rule is 'object'
         @eslintIndentOptions.jsxIndent[0] = rule[0]
         if typeof rule[1] is 'number'
           @eslintIndentOptions.jsxIndent[1] = rule[1] / @atomTabLength
         else @eslintIndentOptions.jsxIndent[1] = 1
+      else @eslintIndentOptions.jsxIndent[1] = defaultIndent

       rule = eslintRules['react/jsx-indent-props']
-      if typeof rule is 'number'
+      if typeof rule is 'number' or typeof rule is 'string'
         @eslintIndentOptions.jsxIndentProps[0] = rule
-        @eslintIndentOptions.jsxIndentProps[1] = 4 / @atomTabLength
+        @eslintIndentOptions.jsxIndentProps[1] = ES_DEFAULT_INDENT / @atomTabLength
       else if typeof rule is 'object'
         @eslintIndentOptions.jsxIndentProps[0] = rule[0]
         if typeof rule[1] is 'number'
           @eslintIndentOptions.jsxIndentProps[1] = rule[1] / @atomTabLength
         else @eslintIndentOptions.jsxIndentProps[1] = 1
+      else @eslintIndentOptions.jsxIndentProps[1] = defaultIndent

       rule = eslintRules['react/jsx-closing-bracket-location']
       @eslintIndentOptions.jsxClosingBracketLocation[1].selfClosing = TAGALIGNED
       @eslintIndentOptions.jsxClosingBracketLocation[1].nonEmpty = TAGALIGNED
-      if typeof rule is 'number'
+      if typeof rule is 'number' or typeof rule is 'string'
         @eslintIndentOptions.jsxClosingBracketLocation[0] = rule
       else if typeof rule is 'object' # array
         @eslintIndentOptions.jsxClosingBracketLocation[0] = rule[0]
@@ -602,9 +606,8 @@

   # get tab indents from eslint indent
   getEslintIndent: () ->
-    if @eslintIndentOptions.indent[0] then jsIndent = @eslintIndentOptions.indent[1]
-    else jsIndent = 0
-    jsIndent
+    if @eslintIndentOptions.jsxIndent[0] then return  @eslintIndentOptions.jsxIndent[1]
+    else return 0

   # allign nonEmpty and selfClosing tags based on eslint rules
   # row to be indented based upon a parentTags properties and a rule type
@@ -633,12 +636,8 @@
   # other indents are the required indent based on eslint conditions for React
   # option contains row to indent and allowAdditionalIndents flag
   indentRow: (options) ->
-    { row, allowAdditionalIndents, blockIndent, jsIndent, jsxIndent, jsxIndentProps } = options
+    { row, allowAdditionalIndents, blockIndent, jsxIndent, jsxIndentProps } = options
     # calc overall indent
-    if jsIndent
-      if @eslintIndentOptions.indent[0]
-        if @eslintIndentOptions.indent[1]
-          blockIndent += jsIndent * @eslintIndentOptions.indent[1]
     if jsxIndent
       if @eslintIndentOptions.jsxIndent[0]
         if @eslintIndentOptions.jsxIndent[1]


Fixed RULE setup for srs/htmldoc.coffee

Fixed RULE setup for srs/htmldoc.coffee

--- src/coffeelint.coffee
+++ src/coffeelint.coffee
@@ -62,10 +62,6 @@
 # Merge default and user configuration.
 mergeDefaultConfig = (userConfig) ->
     config = {}
-    for rule, RuleConstructor of _rules
-        tmp = new RuleConstructor
-        RULES[rule] = tmp.rule
-
     for rule, ruleConfig of RULES
         config[rule] = defaults(userConfig[rule], ruleConfig)

@@ -114,7 +110,8 @@
             typeof p.lintAST isnt 'function'
         e "Rules must implement lintToken, lintLine, or lintAST"

-
+    # Capture the default options for the new rule.
+    RULES[p.rule.name] = p.rule
     _rules[p.rule.name] = RuleConstructor

 # These all need to be explicitly listed so they get picked up by browserify.


Minor code organization fix

Minor code organization fix

--- src/collection.coffee
+++ src/collection.coffee
@@ -11,7 +11,7 @@
   prepare: (params) ->
     return prepare params

-  find: (params, options, fn) ->
+  _find: (params, options, fn) ->
     {params, options, fn} = utils.normalize params, options, fn

     @db.open @name, (err, col) ->
@@ -40,13 +40,17 @@
       return
     return

+  find: (params, options, fn) ->
+    @_find params, options, fn
+    return
+
   findOne: (params, options, fn) ->
     {params, options, fn} = utils.normalize params, options, fn

     options = {} unless options
     options.limit = 1

-    @find params, options, (error, results) ->
+    @_find params, options, (error, results) ->
       results = results[0] ? false
       fn error, results

@@ -63,8 +67,13 @@

     params = _id: id
     options = if utils.is.arr fields then {fields} else {}
+    options.limit = 1

-    @findOne params, options, fn
+    @_find params, options, (error, results) ->
+      results = results[0] ? false
+      fn error, results
+
+      return
     return

   save: (params, fn) ->
@@ -108,7 +117,7 @@
       return
     return

-  remove: (params, fn) ->
+  _remove: (params, fn) ->
     {params, fn} = utils.normalize params, fn

     @db.open @name, (err, col) ->
@@ -126,8 +135,12 @@
       return
     return

+  remove: (params, fn) ->
+    @_remove params, fn
+    return
+
   removeById: (id, fn) ->
-    @remove {_id: id}, fn
+    @_remove {_id: id}, fn
     return

   count: (params, fn) ->


Added placeholders to registration

Added placeholders to registration

--- picoCTF-web/web/coffee/front-page.coffee
+++ picoCTF-web/web/coffee/front-page.coffee
@@ -56,20 +56,20 @@
               {if @props.emailFilter.length > 0 and not @props.rid then showEmailFilter() else <span/>}
             </div>
             <Col md={6}>
-              <Input type="text" id="first-name" valueLink={@props.firstname} label="First Name"/>
-            </Col>
-            <Col md={6}>
-              <Input type="text" id="last-name" valueLink={@props.lastname} label="Last Name"/>
+              <Input type="text" id="first-name" valueLink={@props.firstname} label="First Name" placeholder="Jane"/>
+            </Col>
+            <Col md={6}>
+              <Input type="text" id="last-name" valueLink={@props.lastname} label="Last Name" placeholder="Doe"/>
             </Col>
           </Row>
           <Row>
             <Col md={12}>
-              <Input type="email" id="email" valueLink={@props.email} label="E-mail"/>
-            </Col>
-          </Row>
-          <Row>
-            <Col md={6}>
-              <Input type="text" id="affiliation" valueLink={@props.affiliation} label="Affiliation"/>
+              <Input type="email" id="email" valueLink={@props.email} label="E-mail" placeholder="email@example.com"/>
+            </Col>
+          </Row>
+          <Row>
+            <Col md={6}>
+              <Input type="text" id="affiliation" valueLink={@props.affiliation} label="Affiliation" placeholder="Example School, Pittsbugh, PA"/>
             </Col>
             <Col md={6}>
               <Input type="select" label="Status" placeholder="Competitor" valueLink={@props.eligibility}>


ComputeController: fetchMachine: fetch and create a single Machine instance from provided uid or mongo id of a JMachine document

--- client/Main/providers/computecontroller.coffee
+++ client/Main/providers/computecontroller.coffee
@@ -45,6 +45,12 @@
           machines.push new Machine { machine }

       callback null, @machines = machines
+
+  fetchMachine: (idOrUid, callback = noop)->
+
+    KD.remote.api.JMachine.one idOrUid, (err, machine)->
+      if KD.showError err then callback err
+      else if machine? then callback null, new Machine { machine }

   credentialsFor: (provider, callback)->


ComputeController: instead of checking with plans, check with total # of allowance

--- client/Main/providers/computecontroller.coffee
+++ client/Main/providers/computecontroller.coffee
@@ -524,7 +524,7 @@
           limits  = plans[plan]
           options = { plan, limits, usage }

-          if plan in ['developer', 'professional', 'super']
+          if limits.total > 1

             new ComputePlansModal.Paid options
             @_inprogress = no


We need this

We need this

--- client/app/MainApp/kodingappscontroller.coffee
+++ client/app/MainApp/kodingappscontroller.coffee
@@ -1,3 +1,5 @@
+KDApps = {}
+
 class KodingAppsController extends KDController

   escapeFilePath = FSHelper.escapeFilePath


manifest authorNick fix

manifest authorNick fix

--- client/app/MainApp/kodingappscontroller.coffee
+++ client/app/MainApp/kodingappscontroller.coffee
@@ -295,6 +295,7 @@
           warn err
           callback? err
         else
+          manifest.authorNick = KD.whoami().profile.nickname
           jAppData   =
             title    : manifest.name        or "Application Title"
             body     : manifest.description or "Application description"


delete comment

delete comment

--- lib/entry.coffee
+++ lib/entry.coffee
@@ -31,9 +31,6 @@
   isDestroyed: ->
     @destroyed

-  # getInfo: ->
-  #   {@URI, @point}
-
   inspect: ->
     "#{@point}, #{path.basename(@URI)}"


Change camel case event names.

Change camel case event names.

--- client/app/lib/payment/paymentconstants.coffee
+++ client/app/lib/payment/paymentconstants.coffee
@@ -48,5 +48,5 @@
     ERR_USER_NOT_CONFIRMED: 'You need to confirm your email to purchase a Koding subscription.'

   events:
-    WORKFLOW_STARTED: 'workflowStarted'
-    WORKFLOW_COULD_NOT_START: 'workflowCouldNotStart'
+    WORKFLOW_STARTED: 'WorkflowStarted'
+    WORKFLOW_COULD_NOT_START: 'WorkflowCouldNotStart'


StackEditor: setData -> setTemplateData to allow usage on constructor

--- client/new-stack-editor/lib/views/index.coffee
+++ client/new-stack-editor/lib/views/index.coffee
@@ -42,13 +42,14 @@
       cssClass: 'readme'
       title: 'Readme'

+    @setTemplateData()
     @emit 'ready'

-  setData: (@data) ->
-
+  setTemplateData: ->

     { description, template } = @getData()
+    return  unless description or template

     @editor.setContent Encoder.htmlDecode template.rawContent
     @readme.setContent description


Fixed typo in ticket perform widget.

Fixed typo in ticket perform widget.

--- app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+++ app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
@@ -24,7 +24,7 @@
           elements["#{groupKey}.email"] = { name: 'email', display: 'Email' }
           elements["#{groupKey}.sms"] = { name: 'sms', display: 'SMS' }
         else if groupKey is 'article'
-          elements["#{groupKey}.note"] = { name: 'note', display: 'Notiz' }
+          elements["#{groupKey}.note"] = { name: 'note', display: 'Note' }
       else

         for row in App[groupMeta.model].configure_attributes


Removed log

Removed log

--- app/scripts/models/game.coffee
+++ app/scripts/models/game.coffee
@@ -3,7 +3,6 @@
   initialize: ->
     @board = new Board
     @listenTo @board, "board:isFull", =>
-      console.log @
       @stopTicking()
       @set 'isOver', yes
     @newGame()


Fix bug with sortCache

Fix bug with sortCache

--- source/scripts/models/task.coffee
+++ source/scripts/models/task.coffee
@@ -43,6 +43,8 @@
       @sortCache = null

     @on 'before:destroy:model', (task) =>
+      return unless @sortCache
+      console.log 'deleting from sortCache'
       index = @sortCache.indexOf task
       @sortCache.splice index, 1

@@ -54,7 +56,7 @@

   sort: =>
     if @sortCache then return @sortCache
-    allTasks.sort @slice()
+    @sortCache = allTasks.sort @slice()

 ###*
  * Holds every task ever made


remove unused brunch file

remove unused brunch file

--- brunch-config.coffee
+++ brunch-config.coffee
@@ -1,35 +0,0 @@
-exports.config =
-  # See http://brunch.io/#documentation for docs.
-  files:
-    javascripts:
-      joinTo:
-        'js/app.js': /^app/
-        'js/vendor.js': /^vendor|bower_components/
-      order:
-        before: [
-          'app/js/module.js'
-        ]
-
-    stylesheets:
-      joinTo:
-        'css/app.css': /^app/
-        'css/vendor.css': /^vendor|bower_components/
-
-    templates:
-      joinTo: 'scripts/app.js'
-
-  modules:
-    wrapper: false
-
-  plugins:
-
-    autoReload:
-      enabled:
-        css: on
-        js: on
-        assets: on
-      port: [1234, 2345, 3456]
-      delay: 200 if require('os').platform() is 'win32'
-
-    uglify:
-      mangle: false


Changed date fetching modus operandi for interventions/_form scope updates to allow browsers who don't handle the datetimepicker to work. Should fix #799

--- app/assets/javascripts/backend/interventions.js.coffee
+++ app/assets/javascripts/backend/interventions.js.coffee
@@ -153,7 +153,9 @@

   $(document).on "keyup change", "input.intervention-started-at", ->
     $(this).each ->
-      E.interventions.updateParcelsScopes ($(this).data("datetimepicker").getFormattedDate())
+      #date = $(this).data("datetimepicker").getFormattedDate()
+      date = $(this).val()
+      E.interventions.updateParcelsScopes(date)

   # $(document).on 'change', '*[data-procedure-global="at"]', ->
   #   $(this).each ->


Now adding the data attribute to the actual component instead of the parent container.

--- app/assets/javascripts/backend/interventions.js.coffee
+++ app/assets/javascripts/backend/interventions.js.coffee
@@ -17,7 +17,7 @@
     updateProcedureLevelAttributes: (form, attributes) ->
       for name, properties of attributes
         parameterContainer = $("[data-intervention-parameter='#{name}']").parent('.nested-association')
-        $(parameterContainer).attr('data-display-status', properties.display)
+        parameterContainer.find(".display-info .status").attr('data-display-status', properties.display)

     handleComponents: (form, attributes, prefix = '') ->
       for name, value of attributes