Add bunch of Russian translation

Add bunch of Russian translation

--- app/locale/ru.coffee
+++ app/locale/ru.coffee
@@ -132,7 +132,7 @@
     help_suff: "и мы свяжемся!"

   modal:
-#    cancel: "Cancel"
+    cancel: "Отмена"
     close: "Закрыть"
     okay: "OK"

@@ -234,7 +234,7 @@
     None: "значение, которое не указывает ни на один объект (ничто)"

   share_progress_modal:
-    blurb: "Вы отлично продвигаетесь! Расскажите своим родителям, как много вы уже выучили с CodeCombat."
+    blurb: "Вы показываете отличные результаты! Расскажите своим родителям, как много вы уже выучили с CodeCombat."
     email_invalid: "Email-адрес некорректен."
     form_blurb: "Введите их email-адреса ниже, и мы покажем им!"
     form_label: "Email-адрес"
@@ -243,9 +243,9 @@

   login:
     sign_up: "Создать аккаунт"
-#    email_or_username: "Email or username"
+    email_or_username: "Email или имя пользователя"
     log_in: "Войти"
-    logging_in: "Вход..."
+    logging_in: "Входим..."
     log_out: "Выйти"
     forgot_password: "Забыли пароль?"
     authenticate_gplus: "Аутентификация G+"
@@ -256,72 +256,72 @@
     signup_switch: "Хотите создать аккаунт?"

   signup:
-#    create_student_header: "Create Student Account"
-#    create_teacher_header: "Create Teacher Account"
-#    create_individual_header: "Create Individual Account"
-#    create_header: "Create Account"
-    email_announcements: "Получать оповещения по email" # {change}
-    creating: "Создание аккаунта..."
-#    create_account: "Create Account"
+    create_student_header: "Создать учетную запись Ученика"
+    create_teacher_header: "Создать учетную запись Учителя"
+    create_individual_header: "Создать личную учетную запись"
+    create_header: "Создать учетную запись"
+    email_announcements: "Получать оповещения по email о новых уровнях и возможностях на CodeCombat"
+    creating: "Создаем учетную запись..."
+    create_account: "Создать учетную запись"
     sign_up: "Регистрация"
     log_in: "вход с паролем"
     required: "Войдите для того, чтобы продолжить."
-    login_switch: "Уже есть аккаунт?"
-    school_name: "Название школы and город"
+    login_switch: "Уже есть учетная запись?"
+    school_name: "Название школы и город"
     optional: "не обязательно"
     school_name_placeholder: "Школа № 2, город Электросталь, Московская область"
-#    connect_with: "Connect with:"
+    connect_with: "Зарегистрироваться с помощью:"
     connected_gplus_header: "Вы успешно авторизовались через Google+!"
     connected_gplus_p: "Теперь можно войти используя аккаунт Google+."
     gplus_exists: "У вас уже имеется аккаунт связанный с Google+!"
     connected_facebook_header: "Вы успешно авторизовались через Facebook!"
     connected_facebook_p: "Теперь можно войти используя аккаунт Facebook."
-    facebook_exists: "У вас уже имеется аккаунт связанный сh Facebook!"
+    facebook_exists: "У вас уже имеется аккаунт связанный с Facebook!"
     hey_students: "Студенты, введите код от класса вашего учителя."
-#    birthday: "Birthday"
-#    parent_email_blurb: "We know you can't wait to learn programming — we're excited too! Your parents will receive an email with further instructions on how to create an account for you. Email {{email_link}} if you have any questions."
-#    classroom_not_found: "No classes exist with this Class Code. Check your spelling or ask your teacher for help."
-#    checking: "Checking..."
-#    account_exists: "This email is already in use:" # {change}
-#    sign_in: "Sign in"
-#    email_good: "Email looks good!"
-#    name_taken: "Username already taken! Try {{suggestedName}}?"
-#    name_available: "Username available!"
-#    name_is_email: "Username may not be an email"
-#    choose_type: "Choose your account type:"
-#    teacher_type_1: "Teach programming using CodeCombat!"
-#    teacher_type_2: "Set up your class"
-#    teacher_type_3: "Access Course Guides"
-#    teacher_type_4: "View student progress"
-#    signup_as_teacher: "Sign up as a Teacher"
-#    student_type_1: "Learn to program while playing an engaging game!"
-#    student_type_2: "Play with your class"
-#    student_type_3: "Compete in arenas"
-#    student_type_4: "Choose your hero!"
-#    student_type_5: "Have your Class Code ready!"
-#    signup_as_student: "Sign up as a Student"
-#    individuals_or_parents: "Individuals & Parents"
-#    individual_type: "For players learning to code outside of a class. Parents should sign up for an account here."
-#    signup_as_individual: "Sign up as an Individual"
-#    enter_class_code: "Enter your Class Code"
-#    enter_birthdate: "Enter your birthdate:"
-#    ask_teacher_1: "Ask your teacher for your Class Code."
-#    ask_teacher_2: "Not part of a class? Create an "
-#    ask_teacher_3: "Individual Account"
-#    ask_teacher_4: " instead."
-#    about_to_join: "You're about to join:"
-#    enter_parent_email: "Enter your parent’s email address:"
-#    parent_email_error: "Something went wrong when trying to send the email. Check the email address and try again."
-#    parent_email_sent: "We’ve sent an email with further instructions on how to create an account. Ask your parent to check their inbox."
-#    account_created: "Account Created!"
-#    confirm_student_blurb: "Write down your information so that you don't forget it. Your teacher can also help you reset your password at any time."
-#    confirm_individual_blurb: "Write down your login information in case you need it later. Verify your email so you can recover your account if you ever forget your password - check your inbox!"
-#    write_this_down: "Write this down:"
-#    start_playing: "Start Playing!"
-#    sso_connected: "Successfully connected with:"
+    birthday: "День рождения"
+    parent_email_blurb: "Мы знаем что вы в нетерпении начать учиться программировать — мы тоже! Ваши родители получат письмо с дальнейшими инструкциями, как создать учетную запись. Пишите нам на email {{email_link}} если есть любые вопросы."
+    classroom_not_found: "Нет класса с таким кодом. Проверьте написание или попросите учителя помочь."
+    checking: "Проверяем..."
+    account_exists: "Этот email-адрес уже используется:" # {change}
+    sign_in: "Зарегистрироваться"
+    email_good: "С email-адресом все в порядке!"
+    name_taken: "Имя пользователя уже используется! Может {{suggestedName}}?"
+    name_available: "Имя пользователя доступно!"
+    name_is_email: "Имя пользователя не должно быть email-адресом"
+    choose_type: "Выбирите тип учетной записи:"
+    teacher_type_1: "Обучайте с помощью CodeCombat!"
+    teacher_type_2: "Настраивайте ваш класс"
+    teacher_type_3: "Получите доступ к учебным материалам "
+    teacher_type_4: "Следите за прогрессом учеников"
+    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: "Для игроков, которые учатся без класса. Родители должны создать учетную запись."
+    signup_as_individual: "Зарегистрироваться как индивидуальный игрок"
+    enter_class_code: "Введите ваш код для класса"
+    enter_birthdate: "Введите вашу дату рождения:"
+    ask_teacher_1: "Спросите вашего учителя код для класса."
+    ask_teacher_2: "Не относишься к учебному классу? Создай "
+    ask_teacher_3: "Личную учетную запись"
+    ask_teacher_4: " вместо этого."
+    about_to_join: "Вы присоединяетесь к:"
+    enter_parent_email: "Введите email-адрес одного из родителей:"
+    parent_email_error: "Что-то пошло не так, когда мы отправляли письмо. Проверьте email-адрес и повторите."
+    parent_email_sent: "Мы послали электронное письмо с дальнейшими инструкциями, как создать учетную запись. Попроси родителей проверить их входящие письма."
+    account_created: "Учетная запись создана!"
+    confirm_student_blurb: "Запишите ваши данные, чтобы не забыть их. Ваш учитель, если что сможет помочь сбросить пароль в любое время."
+    confirm_individual_blurb: "Write down your login information in case you need it later. Verify your email so you can recover your account if you ever forget your password - check your inbox!"
+    write_this_down: "Запишите это:"
+    start_playing: "Начать играть!"
+    sso_connected: "Успешно подключились с помощью:"

   recover:
-    recover_account_title: "Восстановить аккаунт"
+    recover_account_title: "Восстановить учетную запись"
     send_password: "Отправить пароль для восстановления"
     recovery_sent: "Письмо с паролем отправлено."

@@ -340,8 +340,8 @@
     saving: "Сохранение..."
     sending: "Отправка..."
     send: "Отправить"
-#    sent: "Sent"
-#    type: "Type"
+    sent: "Отправлено"
+    type: "Тип"
     cancel: "Отмена"
     save: "Сохранить"
     publish: "Опубликовать"
@@ -411,8 +411,8 @@
     wizard: "Волшебник"
     first_name: "Имя"
     last_name: "Фамилия"
-#    last_initial: "Last Initial"
-    username: "Имя юзера"
+    last_initial: "Инициалы фамилии"
+    username: "Имя пользователя"

   units:
     second: "секунда"
@@ -431,13 +431,13 @@
     years: "лет"

   play_level:
-    level_complete: "Уровень завершен"
-    completed_level: "Завершённый уровень:"
+    level_complete: "Уровень пройден"
+    completed_level: "Пройденный уровень:"
     course: "Курс:"
     done: "Готово"
     next_level: "Следующий уровень"
     next_game: "Следующая игра"
-#    programming_language: "Programming language"
+    programming_language: "Язык программирования"
     show_menu: "Показать меню игры"
     home: "На главную" # Not used any more, will be removed soon.
     level: "Уровень" # Like "Level: Dungeons of Kithgard"
@@ -458,12 +458,12 @@
     reload_confirm: "Перезагрузить всё"
     victory: "Победа"
     victory_title_prefix: "Уровень "
-    victory_title_suffix: " завершён"
+    victory_title_suffix: " пройден"
     victory_sign_up: "Зарегистрируйтесь, чтобы сохранить прогресс"
-    victory_sign_up_poke: "Хотите сохранить ваш код? Создайте бесплатный аккаунт!"
+    victory_sign_up_poke: "Хотите сохранить ваш код? Создайте бесплатную учетную запись!"
     victory_rate_the_level: "Оцените уровень:" # {change}
-    victory_return_to_ladder: "Вернуться к ладдеру"
-    victory_saving_progress: "Процесс сохранения"
+    victory_return_to_ladder: "Вернуться к таблице"
+    victory_saving_progress: "Сохранить прогресс"
     victory_go_home: "На главную"
     victory_review: "Расскажите нам больше!"
     victory_review_placeholder: "Как вам уровень?"
@@ -474,13 +474,13 @@
     victory_new_item: "Новый предмет"
     victory_viking_code_school: "Ого, это было тяжелый уровень! Если вы еще не разработчик программ, вам стоит им стать. Вы только что ускорири принятие в Школу Викингов, где вы сможете поднять свои навыки на новый уровень и стать профессиональным веб-разработчиком за 14 недель."
     victory_become_a_viking: "Станьте Викингом"
-    victory_no_progress_for_teachers: "Прогресс не сохраняется для учителей. Но, вы можете для себя добавить аккаунт студента в свою классную комнату."
+    victory_no_progress_for_teachers: "Прогресс не сохраняется для учителей. Но, вы можете для себя добавить учетную запись ученика в свою классную комнату."
     guide_title: "Руководство"
     tome_cast_button_run: "Запустить"
     tome_cast_button_running: "В процессе"
     tome_cast_button_ran: "Запущен"
     tome_submit_button: "Завершить"
-    tome_reload_method: "Загрузить оригинальный код для этого метода" # {change}
+    tome_reload_method: "Загрузить оригинальный код и начать уровень заново" # {change}
     tome_available_spells: "Доступные заклинания"
     tome_your_skills: "Ваши навыки"
     tome_current_method: "Текущий метод"
@@ -491,13 +491,13 @@
     keyboard_shortcuts: "Горячие клавиши"
     loading_ready: "Готово!"
     loading_start: "Начать уровень"
-    problem_alert_title: "Исправьте ваш Код"
+    problem_alert_title: "Исправьте код"
     time_current: "Текущее:"
     time_total: "Максимальное:"
     time_goto: "Перейти на:"
     non_user_code_problem_title: "Невозможно загрузить уровень"
     infinite_loop_title: "Обнаружен бесконечный цикл"
-    infinite_loop_description: "Код сотворения мира не завершил выполнение. Это могло случиться из-за реально медленного кода или наличия бесконечного цикла. Или там может быть баг. Вы можете попытаться запустить этот код еще раз или сбросить код в состояние по умолчанию. Если проблема не будет решена, дайте нам знать."
+    infinite_loop_description: "Код сотворения мира не завершил выполнение. Это могло случиться из-за очень медленного кода или наличия бесконечного цикла. Или там может быть баг. Вы можете попытаться запустить этот код еще раз или сбросить код в состояние по умолчанию. Если проблема не будет решена, дайте нам знать."
     check_dev_console: "Вы так же можете открыть консоль разработчика, чтобы увидеть, что может идти не так."
     check_dev_console_link: "(инструкции)"
     infinite_loop_try_again: "Попробовать снова"
@@ -542,32 +542,32 @@
     tip_optimization_operator: "В каждом языке есть оператор оптимизации. В большинстве языков это оператор ‘//’"
     tip_lines_of_code: "Измерение прогресса программирования в строках кода - это как измерять прогресс построения самолета по его весу. — Bill Gates"
     tip_source_code: "Я хочу изменить мир, но они вряд ли дадут мне исходники."
-    tip_javascript_java: "Java к JavaScript относится так же, как кол относится к колготкам. - Chris Heilmann (перефраз.)"
-    tip_move_forward: "Что бы вы ни делали, вы должны двигаться вперед. - Martin Luther King Jr"
+    tip_javascript_java: "Java к JavaScript относится так же, как кол относится к колготкам. (перефраз.) - Крис Хейльман"
+    tip_move_forward: "Что бы вы ни делали, вы должны двигаться вперед. - Мартин Лютер Кинг Мл."
     tip_google: "У вас проблема, которую вы не можете решить? Гуглите!"
     tip_adding_evil: "Добавим щепотку зла."
-    tip_hate_computers: "Есть одна вещь в людях, которые думают, что они ненавидят компьютеры. Что они на самом деле ненавидят, так это плохих программистов. - Larry Niven"
-    tip_open_source_contribute: "Вы можете помочь сделать CodeCombat лучше!"
+    tip_hate_computers: "Есть одна вещь в людях, которые думают, что они ненавидят компьютеры. Что они на самом деле ненавидят, так это плохих программистов. - Ларри Нивен"
+    tip_open_source_contribute: "Ты можешь помочь сделать CodeCombat лучше!"
     tip_recurse: "Итерация свойственна человеку, рекурсия божественна. - L. Peter Deutsch"
-    tip_free_your_mind: "Отвлекись от всего, Нео. Страх, неверие, сомнения отбрось — очисти свой разум. - Morpheus"
-    tip_strong_opponents: "Даже сильнейший противник имеет слабость. - Itachi Uchiha"
+    tip_free_your_mind: "Отвлекись от всего, Нео. Страх, неверие, сомнения отбрось — очисти свой разум. - Морфей"
+    tip_strong_opponents: "Даже сильнейший противник имеет слабость. - Итачи Учиха"
     tip_paper_and_pen: "Прежде чем начать программировать, вы всегда можете попробовать с листом бумаги и ручкой."
     tip_solve_then_write: "Сперва реши задачу, затем пиши код. - Джон Джонсон"
     tip_compiler_ignores_comments: "Порой мне кажется, что компилятор игнорирует мои комментарии."
     tip_understand_recursion: "Единственный способ понять рекурсию, это понять рекурсию."
     tip_life_and_polymorphism: "Открытый исходный код это как полоностью полиморфная гетерогенная структура: Все типы приветствуются."
     tip_mistakes_proof_of_trying: "Ошибка в коде подтвердила твои старания."
-#    tip_adding_orgres: "Rounding up ogres."
-#    tip_sharpening_swords: "Sharpening the swords."
+    tip_adding_orgres: "Собираем огров."
+    tip_sharpening_swords: "Точим мечи."
 #    tip_ratatouille: "You must not let anyone define your limits because of where you come from. Your only limit is your soul. - Gusteau, Ratatouille"
 #    tip_nemo: "When life gets you down, want to know what you've gotta do? Just keep swimming, just keep swimming. - Dory, Finding Nemo"
 #    tip_internet_weather: "Just move to the internet, it's great here. We get to live inside where the weather is always awesome. - John Green"
 #    tip_nerds: "Nerds are allowed to love stuff, like jump-up-and-down-in-the-chair-can't-control-yourself love it. - John Green"
-#    tip_self_taught: "I taught myself 90% of what I've learned. And that's normal! - Hank Green"
-#    tip_luna_lovegood: "Don't worry, you're just as sane as I am. - Luna Lovegood"
-#    tip_good_idea: "The best way to have a good idea is to have a lot of ideas. - Linus Pauling"
+    tip_self_taught: "Я самостоятельно научился 90% из того чему учился. И это нормально! - Хэнк Грин"
+    tip_luna_lovegood: "Не переживай, ты также в своем уме, как и я. - Луна Лавгуд"
+    tip_good_idea: "Лучший способ найти хорошую идею - иметь множество идей. - Линус Полинг"
 #    tip_programming_not_about_computers: "Computer Science is no more about computers than astronomy is about telescopes. - Edsger Dijkstra"
-#    tip_mulan: "Believe you can, then you will. - Mulan"
+    tip_mulan: "Верь, что можешь и получится. - Мулан"

   game_menu:
     inventory_tab: "Инвентарь"
@@ -694,7 +694,7 @@
     javascript_blurb: "Язык для Сети."
     coffeescript_blurb: "Улучшенный синтаксис JavaScript."
     lua_blurb: "Скриптовый язык для игр."
-#    java_blurb: "(Subscriber Only) Android and enterprise."
+    java_blurb: "(только для подписчиков) Андроид и бизнес."
     status: "Статус"
     hero_type: "Тип"
     weapons: "Оружие"
@@ -705,7 +705,7 @@
     health: "Жизнь"
     speed: "Скорость"
     regeneration: "Регенерация"
-    range: "Зона" # As in "attack or visual range"
+    range: "Дальность" # As in "attack or visual range"
     blocks: "Блокирует" # As in "this shield blocks this much damage"
     backstab: "Со спины" # As in "this dagger does this much backstab damage"
     skills: "Умения"
@@ -775,32 +775,33 @@
     story_link: "История"
     press_link: "Прессе"
     mission_title: "Наша задача: сделать доступным программирование для каждого учащегося на земле."
-#    mission_description_1: "Programming is magic. It's the ability to create things from pure imagination. We started CodeCombat to give learners the feeling of wizardly power at their fingertips by using typed code."
-#    mission_description_2: "As it turns out, that enables them to learn faster too. WAY faster. It's like having a conversation instead of reading a manual. We want to bring that conversation to every school and to every student, because everyone should have the chance to learn the magic of programming."
+    mission_description_1: "Программирование - это магия.
+    Это способность создавать что-то с помощью воображения. Мы создавали CodeCombat, чтобы дать учащимся чувство силы волшебства на кончиках пальцев, когда они  пишут код."
+    mission_description_2: "Как оказалось, это позволяет им учится быстрее. СИЛЬНО быстрее. Это как живой рассказ вместо чтения учебника. Мы хотим принести этот метод в каждую школу и каждому ученику, потому что все должны иметь шанс научится магии программирования."
     team_title: "Присоединяйтесь к команде CodeCombat"
-#    team_values: "We value open and respectful dialog, where the best idea wins. Our decisions are grounded in customer research and our process is focused on delivering tangible results for them. Everyone is hands-on, from our CEO to our GitHub contributors, because we value growth and learning in our team."
-    nick_title: "Сооснователь" # {change}
+    team_values: "Мы ценим открытый и вежливый диалог, где побеждают лучшие идеи. Наши решения основаны на иследовании пожеланий клиентов и наш процесс направлен на то, чтобы приносить осязаемые результаты им. У нас все при деле, от CEO до контрибьютеров на GitHub, потому что мы ценим рост и обучение в нашей команде."
+    nick_title: "Сооснователь, CEO"
     nick_blurb: "Гуру мотивации"
-    matt_title: "Сооснователь" # {change}
-    cat_title: "Главный ремесленник" # {change}
+    matt_title: "Сооснователь, CTO"
+    cat_title: "Гейм дизайнер"
     cat_blurb: "Повелитель стихий"
-    scott_title: "Сооснователь" # {change}
-    scott_blurb: "Разумный"
-#    maka_title: "Customer Advocate"
-#    maka_blurb: "Storyteller"
-#    rob_title: "Software Engineer"
-#    rob_blurb: "Codes things and stuff"
-#    josh_c_title: "Game Designer"
-#    josh_c_blurb: "Designs games"
-#    robin_title: "UX Design & Research"
-#    robin_blurb: "Scaffolding"
-    josh_title: "Дизайнер игры"
+    scott_title: "Сооснователь, инженер программист" # {change}
+    scott_blurb: "Благоразумный"
+    maka_title: "Адвокат клиентов"
+    maka_blurb: "Рассказчик"
+    rob_title: "Инженер программист"
+    rob_blurb: "Программирует все"
+    josh_c_title: "Гейм дизайнер"
+    josh_c_blurb: "Делает игры"
+    robin_title: "UX дизайнер & Исследования"
+    # robin_blurb: "Scaffolding"
+    josh_title: "Гейм дизайнер"
     josh_blurb: "Пол - это лава"
-#    phoenix_title: "Software Engineer"
-#    nolan_title: "Territory Manager"
-#    elliot_title: "Partnership Manager"
-#    elliot_blurb: "Mindreader"
-#    lisa_title: "Market Development Rep"
+    phoenix_title: "Инженер программист"
+    nolan_title: "Региональный менеджер"
+    elliot_title: "Менеджер партнерства"
+    elliot_blurb: "Читающий мысли"
+    lisa_title: "Представитель по развитию рынка"
     retrostyle_title: "Иллюстрирование"
     retrostyle_blurb: "RetroStyle Games"
     jose_title: "Музыка"


Update ru.coffee

Update ru.coffee

--- app/locale/ru.coffee
+++ app/locale/ru.coffee
@@ -317,12 +317,12 @@
     tip_brute_force: "Когда сомневаешься используй грубую силу. - Кен Томпсон"
     tip_extrapolation: "Есть два типа людей: те, кто могут экстраполировать неполные данные..."
     tip_superpower: "Программирование делает нас ближе к обретению суперсилы."
-#    tip_control_destiny: "In real open source, you have the right to control your own destiny. - Linus Torvalds"
-#    tip_no_code: "No code is faster than no code."
-#    tip_code_never_lies: "Code never lies, comments sometimes do. — Ron Jeffries"
-#    tip_reusable_software: "Before software can be reusable it first has to be usable."
-#    tip_optimization_operator: "Every language has an optimization operator. In most languages that operator is ‘//’"
-#    tip_lines_of_code: "Measuring programming progress by lines of code is like measuring aircraft building progress by weight. — Bill Gates"
+    tip_control_destiny: "В настоящей среде открытого кода у вас есть право контроллировать свою собственную судьбу. - Linus Torvalds"
+    tip_code_never_lies: "Код никогда не врет. Комментарии - иногда. — Ron Jeffries"
+    tip_no_code: "Нет кода быстрее, чем его отсутствие."
+    tip_reusable_software: "Прежде, чем программное обеспечение станет повторно используемым, оно должно стать в принципе используемым."
+    tip_optimization_operator: "В каждом языке есть оператор оптимизации. В большинстве языков это оператор ‘//’"
+    tip_lines_of_code: "Измерение прогресса программирования в строках кода - это как измерять прогресс построения самолета по его весу. — Bill Gates"

   game_menu:
     inventory_tab: "Инвентарь"


Visual/edit modes: better (?) guessing for caret mode.

When caret mode guesses a selection score text nodes by their visible
area and the number of non-whitespace characters they contain.  Choose
the first non-whitespace character in the highest-scoring node.

The intention is to avoid making a poor choice for the initial caret
position.

--- content_scripts/mode_visual_edit.coffee
+++ content_scripts/mode_visual_edit.coffee
@@ -619,25 +619,32 @@
     @selection.modify "extend", forward, character

   # When visual mode starts and there's no existing selection, we launch CaretMode and try to establish a
-  # selection.  As a heuristic, we pick the first non-whitespace character of the first visible text node
-  # which seems to be big enough to be interesting.
-  # TODO(smblott).  It might be better to do something similar to Clearly or Readability; that is, try to find
-  # the start of the page's main textual content.
+  # selection.  As a heuristic, we score visible text nodes by their visible area and the number of
+  # non-whitespace characters they contain.  We pick the first non-whitespece character in the highest scoring
+  # node.
   establishInitialSelectionAnchor: ->
     nodes = document.createTreeWalker document.body, NodeFilter.SHOW_TEXT
-    while node = nodes.nextNode()
-      # Don't choose short text nodes; they're likely to be part of a banner.
-      if node.nodeType == 3 and 50 <= node.data.trim().length
+
+    # Find and score candidate text nodes.
+    candidates =
+      for node in (n while n = nodes.nextNode())
+        continue unless node.nodeType == 3 and 0 < node.data.trim().length
         element = node.parentElement
-        if DomUtils.getVisibleClientRect(element) and not DomUtils.isEditable element
-          # Start at the offset of the first non-whitespace character.
-          offset = node.data.length - node.data.replace(/^\s+/, "").length
-          range = document.createRange()
-          range.setStart node, offset
-          range.setEnd node, offset
-          @setSelectionRange range
-          return true
-    false
+        rect = DomUtils.getVisibleClientRect element
+        continue unless rect and not DomUtils.isEditable element
+        area = (rect.bottom - rect.top) * (rect.right - rect.left)
+        chars = node.data.split(/\s+/).join().length
+        { node: node, score: chars * area }
+
+    if 0 < candidates.length
+      node = (candidates.sort (a,b) -> b.score - a.score)[0].node
+      # Start at the offset of the first non-whitespace character.
+      offset = node.data.length - node.data.replace(/^\s+/, "").length
+      range = document.createRange()
+      range.setStart node, offset
+      range.setEnd node, offset
+      @setSelectionRange range
+      return range

 class EditMode extends Movement
   constructor: (options = {}) ->


Proxying lineman requests to sinatra

Proxying lineman requests to sinatra

--- frontend/config/application.coffee
+++ frontend/config/application.coffee
@@ -8,8 +8,16 @@

 module.exports = require(process.env['LINEMAN_MAIN']).config.extend('application', {
   removeTasks:
-    common: ["pages:dev", "webfonts:dev", "images:dev"]
+    common: [ "webfonts:dev", "images:dev"]
     dist: ["images:dist", "webfonts:dist", "pages:dist"]
+
+  server:
+    apiProxy:
+      enabled: true
+      host: 'localhost'
+      port: 4567
+
+

   #  enableSass: false


Deivan uk locale patch 2 (#5970)

Deivan uk locale patch 2 (#5970)

* august update for translate

* Fixed UA locale

* Update uk.coffee

Co-authored-by: Valentin Bryukhanov <53545457af642b0f8712bb2cc004f120ce4436f4@gmail.com>

--- app/locale/uk.coffee
+++ app/locale/uk.coffee
@@ -1646,72 +1646,72 @@
     web_dev_language_transition: "Програма HTML/JavaScript для всіх класів для цього курсу. Класи, що використовували Python, розпочнуть з додаткових вступних рівнів JavaScript, щоб полегшити перехід. Для класів, що вже використовують JavaScript, вступні рівні буде пропущено."
     course_membership_required_to_play: "Вам потрібно приєднатись до курсу щоб зіграти в цей рівень."
     license_required_to_play: "Попроси свого вчителя призначити тобі ліцензію для продовження гри на CodeCombat!"
-#    update_old_classroom: "New school year, new levels!"
+    update_old_classroom: "Новий навчальний рік, нові рівні!"
 #    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."
-#    view_assessments: "View Assessments"
-#    view_challenges: "view challenge levels"
-#    view_ranking: "view ranking"
-#    ranking_position: "Position"
-#    ranking_players: "Players"
-#    ranking_completed_leves: "Completed levels"
-#    challenge: "Challenge:"
-#    challenge_level: "Challenge Level:"
-#    status: "Status:"
-#    assessments: "Assessments"
-#    challenges: "Challenges"
-#    level_name: "Level Name:"
-#    keep_trying: "Keep Trying"
-#    start_challenge: "Start Challenge"
-#    locked: "Locked"
-#    concepts_used: "Concepts Used:"
-#    show_change_log: "Show changes to this course's levels"
-#    hide_change_log: "Hide changes to this course's levels"
-#    concept_videos: "Concept Videos"
-#    concept: "Concept:"
-#    basic_syntax: "Basic Syntax"
-#    while_loops: "While Loops"
-#    variables: "Variables"
+    teacher_dashboard: "дошка вчителя"
+    update_old_classroom_detail_2: "та надання учням нового коду класу, який з'явився."
+    view_assessments: "Дивитись оцінки"
+    view_challenges: "дивитись рівні випробувань"
+    view_ranking: "дивитись рейтинг"
+    ranking_position: "Позиція"
+    ranking_players: "Гравці"
+    ranking_completed_leves: "Завершені рівні"
+    challenge: "Випробування:"
+    challenge_level: "Рівень випробування:"
+    status: "Стан:"
+    assessments: "Оцінки"
+    challenges: "Випробування"
+    level_name: "Назва рівня:"
+    keep_trying: "Продовжуй спроби"
+    start_challenge: "Почати випробування"
+    locked: "Заблоковано"
+    concepts_used: "Поняття, що використані:"
+    show_change_log: "Показати зміни рівня цього курсу"
+    hide_change_log: "Сховати зміни рівня цього курсу"
+    concept_videos: "Відео поняттів"
+    concept: "Поняття:"
+    basic_syntax: "Базовий сінтаксис"
+    while_loops: "Цикли While"
+    variables: "Змінні"
 #    basic_syntax_desc: "Syntax is how we write code. Just as spelling and grammar are important in writing narratives and essays, syntax is important when writing code. Humans are good at figuring out what something means, even if it isn't exactly correct, but computers aren't that smart, and they need you to write very precisely."
 #    while_loops_desc: "A loop is a way of repeating actions in a program. You can use them so you don't have to keep writing repetitive code, and when you don't know exactly how many times an action will need to occur to accomplish a task."
 #    variables_desc: "Working with variables is like organizing things in shoeboxes. You give the shoebox a name, like \"School Supplies\", and then you put things inside. The exact contents of the box might change over time, but whatever's inside will always be called \"School Supplies\". In programming, variables are symbols used to store data that will change over the course of the program. Variables can hold a variety of data types, including numbers and strings."
-#    locked_videos_desc: "Keep playing the game to unlock the __concept_name__ concept video."
-#    unlocked_videos_desc: "Review the __concept_name__ concept video."
-#    video_shown_before: "shown before __level__"
-#    link_google_classroom: "Link Google Classroom"
-#    select_your_classroom: "Select Your Classroom"
-#    no_classrooms_found: "No classrooms found"
-#    create_classroom_manually: "Create classroom manually"
-#    classes: "Classes"
-#    certificate_btn_print: "Print"
-#    certificate_btn_toggle: "Toggle"
-#    ask_next_course: "Want to play more? Ask your teacher for access to the next course."
-#    set_start_locked_level: "Lock levels starting at"
+    locked_videos_desc: "Продовжуйте грати для розблокування __concept_name__ віідео поняття."
+    unlocked_videos_desc: "Переглянути __concept_name__ відео поняття."
+    video_shown_before: "показано раніше __level__"
+    link_google_classroom: "Зв'яжіть класну кімнату Google"
+    select_your_classroom: "Виберіть вашу класну кімнату"
+    no_classrooms_found: "Не знайдено класних кімнат"
+    create_classroom_manually: "Створити класну кімнату самостійно"
+    classes: "Класи"
+    certificate_btn_print: "Друкувати"
+    certificate_btn_toggle: "Переключити"
+    ask_next_course: "Хочете грати ще? Спитайте вашого вчителя щодо доступу до наступного курсу."
+    set_start_locked_level: "Призначити на рівень"
 #    no_level_limit: "-- (no levels locked)"

-#  project_gallery:
-#    no_projects_published: "Be the first to publish a project in this course!"
-#    view_project: "View Project"
-#    edit_project: "Edit Project"
+  project_gallery:
+    no_projects_published: " Будьте першим, хто опублікує проект у цьому курсі!"
+    view_project: "Дивитись проект"
+    edit_project: "Редагувати проект"

   teacher:
     assigning_course: "Призначення курсу"
-#    back_to_top: "Back to Top"
-#    click_student_code: "Click on any level that the student has started or completed below to view the code they wrote."
-#    code: "__name__'s Code"
-#    complete_solution: "Complete Solution"
-#    course_not_started: "Student has not started this course yet."
+    back_to_top: "Догори"
+    click_student_code: "Клікайте на будь-який рівень студента нижче, що почався чи закінчився, - щоб дивитися написаний ним код."
+    code: "__name__'s код"
+    complete_solution: "Повне рішення"
+    course_not_started: "Студент ще не почав цій курс."
 #    hoc_happy_ed_week: "Happy Computer Science Education Week!"
-#    hoc_blurb1: "Learn about the free"
-#    hoc_blurb2: "Code, Play, Share"
-#    hoc_blurb3: "activity, download a new teacher lesson plan, and tell your students to log in to play!"
-#    hoc_button_text: "View Activity"
-#    no_code_yet: "Student has not written any code for this level yet."
+    hoc_blurb1: "Дізнайтеся про безкоштовне"
+    hoc_blurb2: "Кодуй, грай, ділись"
+    hoc_blurb3: "активність, завантажте новий план уроків для вчителя та скажіть своїм учням увійти, щоб грати!"
+    hoc_button_text: "Дивитись активність"
+    no_code_yet: "Студент ще не написав будь-який код для цього рівня."
 #    open_ended_level: "Open-Ended Level"
-#    partial_solution: "Partial Solution"
+    partial_solution: "Часткове рішення"
 #    capstone_solution: "Capstone Solution"
-#    removing_course: "Removing course"
+    removing_course: "Видалити курс course"
 #    solution_arena_blurb: "Students are encouraged to solve arena levels creatively. The solution provided below meets the requirements of the arena level."
 #    solution_challenge_blurb: "Students are encouraged to solve open-ended challenge levels creatively. One possible solution is displayed below."
 #    solution_project_blurb: "Students are encouraged to build a creative project in this level. Please refer to curriculum guides in the Resource Hub for information on how to evaluate these projects."
@@ -1720,7 +1720,7 @@
     level_overview_solutions: "Огляд та рішення рівня"
     no_student_assigned: "Жодного учня не призначено на цей курс."
     paren_new: "(новий)"
-#    student_code: "__name__'s Student Code"
+    student_code: "Код студента __name__"
     teacher_dashboard: "Учительська панель" # Navbar
     my_classes: "Мої класи"
     courses: "Путівники курсу"
@@ -1766,14 +1766,14 @@
     latest_completed: "Останній завершений:"
     sort_by: "Сортувати"
     progress: "Прогрес"
-#    concepts_used: "Concepts used by Student:"
-#    concept_checked: "Concept checked:"
+    concepts_used: "Поняття, що використував студент:"
+    concept_checked: "Перевірені поняття:"
     completed: "Завершено"
     practice: "Практикувати"
     started: "Розпочато"
     no_progress: "Немає прогресу"
     not_required: "Не обов'язково"
-#    view_student_code: "Click to view student code"
+    view_student_code: "Клікніть, щоб дивитись код студента"
     select_course: "Оберіть курс для перегляду"
     progress_color_key: "Кольори прогресу:"
     level_in_progress: "Рівень в процесі"
@@ -1791,8 +1791,8 @@
     assigned_msg_2: "{{numberEnrolled}} ліцензій було застосовано."
     assigned_msg_3: "У вас залишилось {{remainingSpots}} доступних ліцензій."
     assign_course: "Призначити курси"
-#    removed_course_msg: "{{numberRemoved}} students were removed from {{courseName}}."
-#    remove_course: "Remove Course"
+    removed_course_msg: "{{numberRemoved}} студентів були видалені з {{courseName}}."
+    remove_course: "Видалити курс"
 #    not_assigned_msg_1: "Cannot add users to a course instance until they are added to a prepaid that includes this course"
     not_assigned_modal_title: "Курси не були призначені"
     not_assigned_modal_starter_body_1: "Цей курс вимагає початкової ліцензії. У вас недостатньо початкових ліцензій для призначення цього курсу усім __selected__ обраним учням."
@@ -1821,17 +1821,17 @@
     changed: "Змінено"
     available_credits: "Доступні ліцензії"
     pending_credits: "Ліцензії в очікуванні"
-#    empty_credits: "Exhausted Licenses"
-#    license_remaining: "license remaining"
-#    licenses_remaining: "licenses remaining"
+    empty_credits: "Вичерпані ліцензії"
+    license_remaining: "ліцензія залишилась"
+    licenses_remaining: "ліцензій залишилось"
 #    student_enrollment_history: "Student Enrollment History"
 #    enrollment_explanation_1: "The"
 #    enrollment_explanation_2: "Student Enrollment History"
 #    enrollment_explanation_3: "displays the total number of unique students who were enrolled across all teachers and classrooms added to your dashboard. This includes students in both archived and unarchived classrooms with a class creation date between July 1- June 30 of each respective school year."
 #    enrollment_explanation_4: "Remember"
 #    enrollment_explanation_5: "classes may be archived and licenses may be reused throughout the school year, so this view allows administrators to understand how many students truly participated in the program overall."
-#    one_license_used: "1 out of __totalLicenses__ licenses has been used"
-#    num_licenses_used: "__numLicensesUsed__ out of __totalLicenses__ licenses have been used"
+    one_license_used: "одна з __totalLicenses__ ліцензій була використана"
+    num_licenses_used: "__numLicensesUsed__ з __totalLicenses__ ліцензій були використані"
     starter_licenses: "початкові ліцензії"
     start_date: "дата початку:"
     end_date: "дата закінчення:"
@@ -1847,9 +1847,9 @@
     status_enrolled: "Термін дії закінчиться {{date}}"
     select_all: "Обрати все"
     project: "Проект"
-#    project_gallery: "Project Gallery"
-#    view_project: "View Project"
-#    unpublished: "(unpublished)"
+    project_gallery: "Галерея проектів"
+    view_project: "Дивитись проект"
+    unpublished: "(неопубліковано)"
     view_arena_ladder: "Переглянути рейтинг арени"
     resource_hub: "Хаб ресурсів"
 #    pacing_guides: "Classroom-in-a-Box Pacing Guides"
@@ -1871,7 +1871,7 @@
     cs2: "Комп'ютерні науки 2"
     cs3: "Комп'ютерні науки 3"
     cs4: "Комп'ютерні науки 4"
-#    cs5: "Computer Science 5"
+    cs5: "Комп'ютерні науки 5"
     cs1_syntax_python: "Курс 1 Путівник по синтаксису Python"
     cs1_syntax_python_desc: "Шпаргалка з посиланнями на загальний синтаксис Python, який учні вивчать у Вступі то комп'ютерних наук."
     cs1_syntax_javascript: "Курс 1 Путівник по синтаксису JavaScript"


Don't make a new router every time we match

--- lib/adapter.coffee
+++ lib/adapter.coffee
@@ -16,6 +16,7 @@

   expandedDefs = _.flatten (_.map routeDefs, makeResource)
   resolved = resolve services, expandedDefs
+  router = makeRouter resolved

   # given a request:
   #   1. find the first matching route
@@ -25,7 +26,6 @@
     method = req.method.toLowerCase()
     pathname = req._parsedUrl.pathname

-    router = makeRouter resolved
     found = router.match pathname

     return {


updating

updating

--- projects.cson
+++ projects.cson
@@ -1,26 +1,31 @@
+# Arch Scripts
 "arch-scripts":
   title: "arch-scripts"
   paths: [
     "/home/fusion809/Shell"
   ]
   icon: "fl-archlinux"
+# Atom Installer
 "atom-installer":
   title: "atom-installer"
   paths: [
     "/home/fusion809/GitHub/mine/atom-installer"
   ]
   icon: "devicons devicons-atom"
+# The Hornery main repo
 "fusion809.github.io":
   title: "fusion809.github.io"
   paths: [
     "/home/fusion809/GitHub/mine/fusion809.github.io"
   ]
   icon: "fa fa-linux"
+# The Hornery Hexo repo
 "hexo-site":
   title: "hexo-site"
   paths: [
     "/home/fusion809/GitHub/mine/hexo-site"
   ]
+# PKGBUILDs repo
 PKGBUILDs:
   title: "PKGBUILDs"
   paths: [


resources link

resources link

--- upload.coffee
+++ upload.coffee
@@ -14,7 +14,7 @@
       """
     logged_out = """

Upload photos

-
Upload photos of UK orchids that you have found during the 2015 field season here. See the resources section to view field recording instructions and find out more about the 29 orchid species that we are asking you to find. You can also learn why we are asking you to tell us your name and sign-in to Zooniverse, along with how your images and observation data will be managed.

+
Upload photos of UK orchids that you have found during the 2015 field season here. See the resources section to view field recording instructions and find out more about the 29 orchid species that we are asking you to find. You can also learn why we are asking you to tell us your name and sign-in to Zooniverse, along with how your images and observation data will be managed.

Please log in to upload photos.

       """
     template = if User.current then logged_in else logged_out


use trigger.io API for location

use trigger.io API for location

--- app/models/found_friend.coffee
+++ app/models/found_friend.coffee
@@ -74,16 +74,27 @@
 #      new @(objects)

   @fetch: ->
-    # If no geolocation interface on this browser
-    return super unless navigator.geolocation?
-    navigator.geolocation.getCurrentPosition((location) =>
-      location.created_at = new Date()
-      Spine.Ajax.defaults.headers['X-Location'] = [location.coords.latitude, location.coords.longitude, location.coords.accuracy].join(',')
-      super
-      console.log 'Retrieved location', location
-    , ((error) ->
-      console.log 'Could not get location'
-      super
-      ))
+    if Config.env=='ios'
+      forge.geolocation.getCurrentPosition((location) =>
+        location.created_at = new Date()
+        Spine.Ajax.defaults.headers['X-Location'] = [location.coords.latitude, location.coords.longitude, location.coords.accuracy].join(',')
+        super
+        console.log 'Retrieved location', location
+      , (error) ->
+        console.log 'Could not get location'
+        super
+      , enableHighAccuracy: true)
+    else
+      # If no geolocation interface on this browser
+      return super unless navigator.geolocation?
+      navigator.geolocation.getCurrentPosition( (location) =>
+        location.created_at = new Date()
+        Spine.Ajax.defaults.headers['X-Location'] = [location.coords.latitude, location.coords.longitude, location.coords.accuracy].join(',')
+        super
+        console.log 'Retrieved location', location
+      , (error) ->
+        console.log 'Could not get location'
+        super
+      )

 module.exports = FoundFriend


more minors

more minors

--- library/membrane/visual.coffee
+++ library/membrane/visual.coffee
@@ -68,7 +68,7 @@
     # Refer to `inlineAutocalls` method for params interpretation.
     @autocall: (parameters, method) ->
         isRemote = _.isObject method?.remote
-        notFunction = "no function is passed"
+        notFunction = "no function is passed in"
         method = _.find arguments, _.isFunction
         method = external method unless isRemote
         assert _.isFunction(method), notFunction
@@ -219,9 +219,9 @@
     # Method also does some optimizations, such as scripts unifying.
     compressContext: (context) ->
         assert compression = "visual:compression"
-        sources = _.toArray context.sources or []
-        scripts = _.toArray context.scripts or []
-        emptySources = "the JS sources are empty"
+        sources = _.toArray context.sources or Array()
+        scripts = _.toArray context.scripts or Array()
+        emptySources = "context JS sources are empty"
         u = (v) -> v.match(RegExp "^(.+)/(.+)$")[2]
         context.sheets = _.unique context.sheets
         context.styles = _.unique context.styles, u


Finish off the last tweaks of the composer PoC sys

--- library/nucleus/compose.coffee
+++ library/nucleus/compose.coffee
@@ -40,7 +40,7 @@
 # between the foreign and common peers in the inheritance chain. Do
 # refer to the implementation for the understanding of what happens.
 Object.defineProperty Object::, "compose",
-    enumerable: no, value: (compound) ->
+    enumerable: no, value: (compound, shader) ->
         current = this.hierarchy()
         foreign = compound.hierarchy()
         identity = compound.name or compound.nick
@@ -52,6 +52,7 @@
         throw new Error orphans if _.isEmpty commons
         duplicate = (parent) -> class extends parent
         differentiated = _.take current, orphan
+        duplicate = _.identity unless shader
         alternative = _.map differentiated, duplicate
         tails = alternative.pop().rebased compound
         rebased = (acc, cls) -> cls.rebased acc; cls
@@ -89,6 +90,8 @@
         throw new Error noClass unless isClass
         _.extend r = receiver = this, baseclass
         `function ctor() {this.constructor = r}`
+        original = this.prototype or {}
         ctor.prototype = baseclass.prototype
         this.__super__ = baseclass.prototype
-        this.prototype = new ctor(); this
+        this.prototype = new ctor() or original
+        _.extend this.prototype, original; @


Remove upload module if  subject limit has been reached

--- app/pages/lab/subject-set.cjsx
+++ app/pages/lab/subject-set.cjsx
@@ -199,17 +199,19 @@

-

-        <UploadDropTarget accept={"text/csv, text/tab-separated-values, image/*#{if isAdmin() then ', video/*' else ''}"} multiple onSelect={@handleFileSelection}>
-          Drag-and-drop or click to upload manifests and subject images here.

-          Manifests must be .csv or .tsv. The first row should define metadata headers. All other rows should include at least one reference to an image filename in the same directory as the manifest.

-          Headers that begin with "#" or "//" denote private fields that will not be visible to classifiers in the main classification interface or in the Talk discussion tool.

-          Headers that begin with "!" denote fields that will not be visible to classifiers in the main classification interface but will be  visible after classification in the Talk discussion tool.

-          Subject images can be up to {MAX_FILE_SIZE / 1024}KB and any of: {{ext}{', ' if VALID_SUBJECT_EXTENSIONS[i + 1]?} for ext, i in VALID_SUBJECT_EXTENSIONS}{' '}
-          and may not contain {{char}{', ' if INVALID_FILENAME_CHARS[i + 1]?} for char, i in INVALID_FILENAME_CHARS}

-        </UploadDropTarget>
-

-
+      {if @props.user.uploaded_subjects_count >= @props.user.subject_limit and !localStorage.adminFlag
+
You've reached your subject upload limit. Please  contact us to request changes to your allowance.'

+      else
+

+          <UploadDropTarget accept={"text/csv, text/tab-separated-values, image/*#{if isAdmin() then ', video/*' else ''}"} multiple onSelect={@handleFileSelection}>
+            Drag-and-drop or click to upload manifests and subject images here.

+            Manifests must be .csv or .tsv. The first row should define metadata headers. All other rows should include at least one reference to an image filename in the same directory as the manifest.

+            Headers that begin with "#" or "//" denote private fields that will not be visible to classifiers in the main classification interface or in the Talk discussion tool.

+            Headers that begin with "!" denote fields that will not be visible to classifiers in the main classification interface but will be  visible after classification in the Talk discussion tool.

+            Subject images can be up to {MAX_FILE_SIZE / 1024}KB and any of: {{ext}{', ' if VALID_SUBJECT_EXTENSIONS[i + 1]?} for ext, i in VALID_SUBJECT_EXTENSIONS}{' '}
+            and may not contain {{char}{', ' if INVALID_FILENAME_CHARS[i + 1]?} for char, i in INVALID_FILENAME_CHARS}

+          </UploadDropTarget>
+
}

           {subjectsToCreate = 0


Copy user clientPermissions schema property from CodeCombat

--- app/schemas/models/user.coffee
+++ app/schemas/models/user.coffee
@@ -90,6 +90,16 @@
     }
   }
   clientCreator: c.objectId({description: 'Client which created this user'})
+  clientPermissions:
+    description: 'More APIClients with permissions on this user, apart from clientCreator.'
+    type: 'array'
+    items:
+      type: 'object'
+      additionalProperties: false
+      properties:
+        client: {type: c.objectId({description: 'APIClient with permissions on this user'})}
+        access: {type: 'string', 'enum': ['read', 'grant', 'write', 'owner']}  # 'grant' permissions allow APIClients to grant licenses to a user
+    format: 'hidden'

   wizardColor1: c.pct({title: 'Wizard Clothes Color'})  # No longer used
   volume: c.pct({title: 'Volume'})


remove references to unneeded vars

remove references to unneeded vars

--- elasticsearch.coffee
+++ elasticsearch.coffee
@@ -9,7 +9,10 @@

 elasticsearch = require('elasticsearch')
 esConfig = require('./config.js').elasticsearch
-client = new elasticsearch.Client {host: esConfig.host, log: 'trace'}
+client = new elasticsearch.Client {
+  host: esConfig.host,
+  log: 'trace'
+}

 # number of missing files
 missing = 0
@@ -136,7 +139,7 @@
         index: 'blockbuilder'
         type: 'blocks'
         id: gist.id
-      , (err, response) ->
+      , (err) ->
         if err
           # post to elastic search, we don't have it indexed yet
           client.index
@@ -144,9 +147,9 @@
             type: 'blocks'
             id: gist.id
             body: es
-          , (err, response) ->
+          , (err) ->
             console.log "indexed", gist.id
-            return gistCb(err, response)
+            return gistCb(err)
         else
           return gistCb()
     else
@@ -156,9 +159,9 @@
         type: 'blocks'
         id: gist.id
         body: es
-      , (err, response) ->
+      , (err) ->
         console.log "indexed", gist.id
-        return gistCb(err, response)
+        return gistCb(err)

 deleteGist = (gistId, gistCb) ->
   client.delete
@@ -183,4 +186,4 @@
   gistMeta = JSON.parse fs.readFileSync(metaFile).toString()
   console.log gistMeta.length, "gists"

-  async.eachLimit gistMeta, 50, gistParser, done
+  async.eachLimit gistMeta, 20, gistParser, done


Minor changes in the exposure docs

Minor changes in the exposure docs

--- library/membrane/exposure.coffee
+++ library/membrane/exposure.coffee
@@ -76,6 +76,6 @@
     # documented function is implemented. Please refer to `Document`
     # class and its module implementation for more information on it.
     @describe @prototype.GET, (method, service) ->
-        @notes "See the Document class for more information"
         @synopsis "Get all of the APIs available in the system"
         @outputs "An array of objects, each describes a service"
+        @notes "See the Document class for the API structure"


non-essential upgrades to the securing module

--- library/membrane/securing.coffee
+++ library/membrane/securing.coffee
@@ -74,7 +74,7 @@
         assert statusCode = try response.statusCode
         return next() unless (statusCode or 0) is 200
         sha1 = -> require("crypto").createHash "sha1"
-        key = (x) -> "securing:rduplex:token:#{x}"
+        key = (x) -> "securing:marking:token:#{x}"
         gen = (x) -> sha1().update(x).digest "hex"
         internal = (e) -> "internal Redis error: #{e}"
         noUuid = "the request has no UUID attached"
@@ -96,7 +96,7 @@
         assert acc = "Verified %s request".green
         assert rej = "Militarized %s request".red
         sha1 = -> require("crypto").createHash "sha1"
-        key = (x) -> "securing:rduplex:token:#{x}"
+        key = (x) -> "securing:marking:token:#{x}"
         gen = (x) -> sha1().update(x).digest "hex"
         internal = (e) -> "internal Redis error: #{e}"
         noUuid = "the request has no UUID attached"


Don't cache markdown in page plugin

Don't cache markdown in page plugin

--- src/plugins/markdown.coffee
+++ src/plugins/markdown.coffee
@@ -46,13 +46,12 @@

     getHtml: (base=env.config.baseUrl) ->
       ### parse @markdown and return html. also resolves any relative urls to absolute ones ###
-      if not @_html? # cache html
-        options = env.config.markdown or {}
-        options.typogr ?= true
-        @_html = parseMarkdownSync @markdown, @getLocation(base), options
-        if options.typogr
-          @_html = typogr.typogrify @_html
-      return @_html
+      options = env.config.markdown or {}
+      options.typogr ?= true
+      html = parseMarkdownSync @markdown, @getLocation(base), options
+      if options.typogr
+          html = typogr.typogrify html
+      return html

   MarkdownPage.fromFile = (filepath, callback) ->
     async.waterfall [


Add KeymapManager::clear to clear all state

--- src/keymap-manager.coffee
+++ src/keymap-manager.coffee
@@ -106,12 +106,17 @@
   #     is `document.body` to allow for a catch-all element when nothing is focused.
   constructor: (options={}) ->
     @[key] = value for key, value of options
+    @watchSubscriptions = {}
+    @clear()
+    @enableDvorakQwertyWorkaroundIfNeeded()
+
+  # Public: Clear all registered key bindings and enqueued keystrokes. For use
+  # in tests.
+  clear: ->
     @emitter = new Emitter
     @keyBindings = []
     @queuedKeyboardEvents = []
     @queuedKeystrokes = []
-    @watchSubscriptions = {}
-    @enableDvorakQwertyWorkaroundIfNeeded()

   # Public: Unwatch all watched paths.
   destroy: ->


Camera: Allow passing no options.

Camera: Allow passing no options.

--- engine/camera.coffee
+++ engine/camera.coffee
@@ -8,7 +8,7 @@
 class Camera extends GameObject

-    constructor: (context, options) ->
+    constructor: (context, options={}) ->
         super context
         @type = 'CAMERA'
         {


fix typo

fix typo

--- beaglebone.coffee
+++ beaglebone.coffee
@@ -4,7 +4,7 @@
 BBB_FLASH = 'Power up the <%= TYPE_NAME %> while holding down the small button near the SD slot.
 You need to keep it pressed until the blue LEDs start flashing wildly.'

-postProvisioningInstruction = [
+postProvisioningInstructions = [
 	instructions.BOARD_SHUTDOWN
 	instructions.REMOVE_INSTALL_MEDIA
 	instructions.BOARD_REPOWER
@@ -18,14 +18,14 @@
 	state: 'released'

 	stateInstructions:
-		postProvisioning: postProvisioningInstruction
+		postProvisioning: postProvisioningInstructions

 	instructions: [
 		instructions.ETCHER_SD
 		instructions.EJECT_SD
 		instructions.FLASHER_WARNING
 		BBB_FLASH
-	].concat(postProvisioningInstruction)
+	].concat(postProvisioningInstructions)

 	gettingStartedLink:
 		windows: 'http://docs.resin.io/#/pages/installing/gettingStarted-BBB.md#windows'


Basic \let support

Basic \let support


Use existing variable instead of fetching path again

--- lib/helpers.coffee
+++ lib/helpers.coffee
@@ -25,7 +25,7 @@
   userStyleSheetPath = atom.styles.getUserStyleSheetPath()

   if fs.isFileSync(userStyleSheetPath)
-    userStyleSheet = fs.readFileSync(atom.styles.getUserStyleSheetPath(), 'utf8')
+    userStyleSheet = fs.readFileSync(userStyleSheetPath, 'utf8')
     linter.checkSyntaxStylesheet(userStyleSheet, {
       packageName: "your local #{path.basename(userStyleSheetPath)} file"
       packagePath: ""


Code peek: don't ask if declaration window is closed with modifications without save

--- atom/.atom/config.cson
+++ atom/.atom/config.cson
@@ -3,6 +3,8 @@
     confirmCompletion: "tab always, enter when suggestion explicitly selected"
   "autocomplete-python":
     useKite: false
+  "code-peek":
+    askIfSaveOnModified: false
   core:
     closeDeletedFileTabs: true
     closeEmptyWindows: false
@@ -28,6 +30,8 @@
   "git-plus":
     remoteInteractions:
       pullBeforePush: true
+  "goto-definition":
+    performanceMode: true
   latex:
     buildOnSave: true
     opener: "pdf-view"


Fork it on github

Fork it on github

--- packages/rocketchat-lib/settings/server/startup.coffee
+++ packages/rocketchat-lib/settings/server/startup.coffee
@@ -70,7 +70,7 @@
 	RocketChat.settings.add 'Layout_Home_Body', 'Welcome to Rocket.Chat
 Go to APP SETTINGS -> Layout to customize this intro.', { type: 'string', multiline: true, group: 'Layout', public: true }
 	RocketChat.settings.add 'Layout_Terms_of_Service', 'Terms of Service
 Go to APP SETTINGS -> Layout to customize this page.', { type: 'string', multiline: true, group: 'Layout', public: true }
 	RocketChat.settings.add 'Layout_Privacy_Policy', 'Privacy Policy
 Go to APP SETTINGS -> Layout to customize this page.', { type: 'string', multiline: true, group: 'Layout', public: true }
-	RocketChat.settings.add 'Layout_Sidenav_Footer', '   {{_ "Fork_it_on_github"}} ', { type: 'string', group: 'Layout', public: true, i18nDescription: 'Layout_Sidenav_Footer_description' }
+	RocketChat.settings.add 'Layout_Sidenav_Footer', '   Fork it on github ', { type: 'string', group: 'Layout', public: true, i18nDescription: 'Layout_Sidenav_Footer_description' }

 	if process?.env? and not process.env['MAIL_URL']? and RocketChat.settings.get('SMTP_Host') and RocketChat.settings.get('SMTP_Username') and RocketChat.settings.get('SMTP_Password')
 		process.env['MAIL_URL'] = "smtp://" + encodeURIComponent(RocketChat.settings.get('SMTP_Username')) + ':' + encodeURIComponent(RocketChat.settings.get('SMTP_Password')) + '@' + encodeURIComponent(RocketChat.settings.get('SMTP_Host'))


Change /invite to use addUserToRoom instead joinRoom

--- packages/rocketchat-slashcommands-invite/server.coffee
+++ packages/rocketchat-slashcommands-invite/server.coffee
@@ -37,8 +37,9 @@
 			}
 			return

-		Meteor.runAsUser user._id, ->
-			Meteor.call 'joinRoom', item.rid
+		Meteor.call 'addUserToRoom',
+			rid: item.rid
+			username: user.username

 RocketChat.slashCommands.add 'invite', Invite


Refactor DOM API

Refactor DOM API

--- src/marbles/dom.js.coffee
+++ src/marbles/dom.js.coffee
@@ -1,307 +1,354 @@
 #= require ../polyfills/querySelector
 #= require ./core
+#= require ./dependency_manager
 #= require_self
-#= require ./dom/input_selection
-
-Marbles.DOM = DOM = {
-  querySelector: (selector, el) -> (if arguments.length == 2 then el else document)?.querySelector(selector)
-  querySelectorAll: (selector, el) -> (if arguments.length == 2 then el else document)?.querySelectorAll(selector) || []
-
-  parentQuerySelector: (el, selector) ->
-    node = el
-    while node = node.parentNode
-      return node if @match(node, selector)
-    null
-
-  match: (el, selector) ->
-    return unless el
-    _.any @querySelectorAll(selector, el.parentNode), (_el) => _el == el
-
-  hasAttr: (el, name) ->
-    return unless el
-    !!el.attributes?.getNamedItem(name)
-
-  attr: (el, name) ->
-    return unless el
-    el.attributes?.getNamedItem(name)?.value
-
-  setAttr: (el, name, value) ->
-    return unless el
-    el.setAttribute(name, value)
-
-  removeAttr: (el, name) ->
-    return unless el
-    el.removeAttribute(name)
-
-  removeChildren: (el) ->
-    el.innerHTML = ""
-    el
-
-  replaceChildren: (el, new_children...) ->
-    DOM.removeChildren(el)
-
-    for child in new_children
-      el.appendChild(child)
-
-    el
-
-  offsetTop: (el) ->
-    return unless el
-    offset_top = el.offsetTop
-    ref = el
-    while ref = ref.offsetParent
-      offset_top += ref.offsetTop
-    offset_top
-
-  offsetLeft: (el) ->
-    return unless el
-    offset_left = el.offsetLeft
-    ref = el
-    while ref = ref.offsetParent
-      offset_left += ref.offsetLeft
-    offset_left
-
-  replaceWithHTML: (el, html) ->
-    new_el = document.createElement('div')
-    DOM.appendHTML(new_el, html)
-    new_el = if new_el.childElementCount == 1
-      new_el.children[0]
+
+DOM = Marbles.DOM ?= {}
+
+##
+# Dependencies
+#
+# Should be compatible with IE 9+, Firefox, Chrome, Safari, Opera, and all major mobile browsers.
+#
+# NOTE: Functions/properties available in all browsers are not listed
+
+Marbles.registerDependency({
+  keypath: "document"
+  context: Marbles.global
+  functions: [
+    { name: "querySelector" },
+    { name: "querySelectorAll" }
+  ]
+})
+
+Marbles.registerDependency({
+  keypath: "HTMLElement.prototype"
+  context: Marbles.global
+  functions: [
+    { name: "querySelector" },
+    { name: "querySelectorAll" }
+  ]
+})
+
+Marbles.registerDependency({
+  keypath: 'document.defaultView'
+  context: Marbles.global
+  functions: [
+    { name: 'getComputedStyle' }
+  ]
+})
+
+Marbles.registerDependency({
+  keypath: 'Array.prototype'
+  context: Marbles.global
+  functions: [
+    { name: 'indexOf' }
+  ]
+})
+
+##
+# Querying
+
+DOM.matchesQuerySelector = (el, selector) ->
+  for node in el.parentNode.querySelectorAll(selector)
+    return true if node == el
+
+  return false
+
+DOM.parentQuerySelector = (el, selector) ->
+  node = el
+  while node = node.parentNode
+    return node if @match(node, selector)
+
+  return null
+
+DOM.enumerateParentNodes = (el, callback) ->
+  should_stop = false
+  stopFn = ->
+    should_stop = true
+
+  node = el
+  while node = node.parentNode
+    callback(node, stopFn)
+    break if should_stop
+
+  return null
+
+DOM.parentNodes = (el) ->
+  nodes = []
+
+  node = el
+  while node = node.parentNode
+    nodes.push(node)
+
+  return nodes
+
+##
+# Reflection
+
+DOM.isVisible = (el) ->
+  @getStyle(el, 'display') != 'none' &&
+  @getStyle(el, 'visibility') != 'hidden'
+
+DOM.isNodeInDocument = (el) ->
+  for node in @parentNodes(el)
+    return true if node == document.body
+
+  return false
+
+DOM.getInnerText = (el) ->
+  el.innerText || el.textContent
+
+DOM.getStyle = (el, name) ->
+  # convert to camel case (e.g. 'padding-left' to 'paddingLeft')
+  name = name.replace(/-([a-z])/ig, ((match, char) -> char.toUpperCase()))
+
+  val = el.style[name]
+  val = @getComputedStyle(el, name) if !val || val.match(/^[\s\r\t\n]*$/)
+
+  return val
+
+DOM.getComputedStyle = (el, name) ->
+  document.defaultView.getComputedStyle(el)[name]
+
+DOM.absoluteOffsetTop = (el) ->
+  return unless el
+  offset_top = el.offsetTop
+  ref = el
+  while ref = ref.offsetParent
+    offset_top += ref.offsetTop
+
+  return offset_top
+
+DOM.absoluteOffsetLeft = (el) ->
+  return unless el
+  offset_left = el.offsetLeft
+  ref = el
+  while ref = ref.offsetParent
+    offset_left += ref.offsetLeft
+
+  return offset_left
+
+DOM.innerWidth = (el) ->
+  width = parseFloat @getComputedStyle(el, 'width')
+  padding = parseFloat(@getStyle(el, 'padding-left'))
+  padding += parseFloat(@getStyle(el, 'padding-right'))
+
+  return width - padding
+
+DOM.windowHeight = ->
+  if typeof window.innerHeight != undefined
+    return window.innerHeight
+
+  if typeof document.body.offsetHeight != undefined
+    return document.body.offsetHeight
+
+  return document.documentElement.offsetHeight
+
+DOM.windowWidth = ->
+  if typeof window.innerWidth != undefined
+    return window.innerWidth
+
+  if typeof document.body.offsetWidth != undefined
+    return document.body.offsetWidth
+
+  return document.documentElement.offsetWidth
+
+##
+# Contruction
+
+DOM.createElementFromHTML = (html) ->
+  el = document.createElement('div')
+  @appendHTML(el, html)
+
+  if el.childElementCount == 1
+    el = el.children[0]
+
+  return el
+
+##
+# Manipulation
+
+DOM.addClass = (el, class_names) ->
+  classes = el.className.split(' ')
+
+  for name in class_names.split(' ')
+    classes.push(name) if classes.indexOf(name) == -1
+
+  el.className = classes.join(' ')
+
+  return el
+
+DOM.removeClass = (el, class_names) ->
+  exclude_names = class_names.split(' ')
+
+  classes = []
+
+  for name in el.className.split(' ')
+    continue if exclude_names.indexOf(name) != -1
+
+    classes.push(name)
+
+  el.className = classes.join(' ')
+
+  return el
+
+DOM.replaceNode = (el, new_el) ->
+  el.parentNode.replaceChild(new_el, el)
+
+DOM.removeNode = (el) ->
+  el.parentNode.removeChild(el)
+
+DOM.removeAllChildren = (el) ->
+  el.innerHTML = ""
+
+  return el
+
+DOM.setInnerText = (el, value) ->
+  el.textContent = value
+  el.innerText = value
+
+DOM.setInnerHTML = (el, html) ->
+  @removeAllChildren(el)
+  @appendHTML(el, html)
+
+DOM.prependChild = (el, node) ->
+  if el.firstChild
+    el.insertBefore(node, el.firstChild)
+  else
+    el.appendChild(node)
+
+DOM.prependHTML = (el, html) ->
+  tmp_fragment = document.createDocumentFragment()
+
+  @appendHTML(tmp_fragment, html)
+
+  child_nodes = tmp_fragment.childNodes
+
+  for index in [(child_nodes.length-1)..0]
+    node = child_nodes[index]
+
+    continue unless node
+
+    @prependChild(el, node)
+
+  return el
+
+DOM.appendHTML = (el, html) ->
+  tmp_el = document.createElement('div')
+  tmp_el.innerHTML = html
+
+  # Hack to make appending script tags work as expected
+  for script_node in tmp_el.querySelectorAll('script')
+    new_node = document.createElement('script')
+    new_node.type = script_node.type
+
+    if script_node.src
+      new_node.src = script_node.src
     else
-      new_el
-
-    DOM.replaceWith(el, new_el)
-
-  replaceWith: (el, new_el) ->
-    el.parentNode.replaceChild(new_el, el)
-    new_el
-
-  prependChild: (el, node) ->
-    if el.firstChild
-      el.insertBefore(node, el.firstChild)
-    else
-      el.appendChild(node)
-
-  removeNode: (el) ->
-    el.parentNode?.removeChild(el)
-
-  prependHTML: (el, html) ->
-    tmp_fragment = document.createDocumentFragment()
-    DOM.appendHTML(tmp_fragment, html)
-    child_nodes = tmp_fragment.childNodes
-    for index in [(child_nodes.length-1)..0]
-      node = child_nodes[index]
-      continue unless node
-      DOM.prependChild(el, node)
-    el
-
-  appendHTML: (el, html) ->
-    tmp_el = document.createElement('div')
-    tmp_el.innerHTML = html
-
-    # Hack to make appending script tags work as expected
-    for script_node in DOM.querySelectorAll('script', tmp_el)
-      new_node = document.createElement('script')
-      new_node.type = script_node.type
-      if script_node.src
-        new_node.src = script_node.src
+      new_node.innerHTML = script_node.innerHTML
+
+    @replaceNode(script_node, new_node)
+
+  while node = tmp_el.firstChild
+    el.appendChild(node)
+
+  return el
+
+DOM.insertHTMLBefore = (html, reference_el) ->
+  fragment = document.createDocumentFragment()
+  @appendHTML(fragment, html)
+  @insertBefore(fragment, reference_el)
+
+DOM.insertHTMLAfter = (html, reference_el) ->
+  fragment = document.createDocumentFragment()
+  @appendHTML(fragment, html)
+  @insertAfter(fragment, reference_el)
+
+DOM.insertBefore = (el, reference_el) ->
+  reference_el.parentNode.insertBefore(el, reference_el)
+
+DOM.insertAfter = (el, reference_el) ->
+  @insertBefore(el, reference_el)
+  @insertBefore(reference_el, el)
+
+  return el
+
+##
+# Events
+
+DOM._add_event_listener_fn_name = (typeof HTMLElement.prototype.addEventListener == 'function') ? 'addEventListener' : 'attachEvent'
+DOM.addEventListener = (el, events, callback, capture=false) ->
+  return for event in events.split(' ')
+    el[@_add_event_listener_fn_name](event, callback, capture)
+
+DOM.addSingleFireEventListener = (el, events, callback, capture=false) ->
+  _callback = ->
+    callback.apply(this, arguments)
+
+    DOM.removeEventListener(el, events, capture)
+
+  @addEventListener(el, events, _callback, capture)
+
+DOM._remove_event_listener_fn_name = (typeof HTMLElement.prototype.removeEventListener == 'function') ? 'removeEventListener' : 'detachEvent'
+DOM.removeEventListener = (el, events, callback, capture=false) ->
+  return for event in events.split(' ')
+    el[@_remove_event_listener_fn_name](event, callback, capture)
+
+##
+# Form Serialization / Deserialization
+
+DOM.getFormElementValue = (el) ->
+  if el.type == 'file'
+    return el.files
+
+  if el.nodeName.toLowerCase() == 'select'
+    is_multi_select = el.multiple
+
+DOM.    value = is_multi_select ? []  = ""
+
+    for option in el.querySelectorAll('option')
+      continue unless option.selected
+
+      if is_multi_select
+        value.push(option.value)
       else
-        new_node.innerHTML = script_node.innerHTML
-      DOM.replaceWith(script_node, new_node)
-
-    while node = tmp_el.firstChild
-      el.appendChild(node)
-    el
-
-  insertHTMLAfter: (html, reference_el) ->
-    fragment = document.createDocumentFragment()
-    DOM.appendHTML(fragment, html)
-    DOM.insertAfter(fragment, reference_el)
-
-  insertBefore: (el, reference_el) ->
-    reference_el.parentNode?.insertBefore(el, reference_el)
-
-  insertAfter: (el, reference_el) ->
-    DOM.insertBefore(el, reference_el)
-    DOM.insertBefore(reference_el, el)
-    el
-
-  windowHeight: ->
-    return window.innerHeight if window.innerHeight
-    return document.body.offsetHeight if document.body.offsetHeight
-    document.documentElement?.offsetHeight
-
-  windowWidth: ->
-    return window.innerWidth if window.innerWidth
-    return document.body.offsetWidth if document.body.offsetWidth
-    document.documentElement?.offsetWidth
-
-  innerWidth: (el) ->
-    width = parseFloat @getComputedStyle(el, 'width')
-    padding = parseFloat(@getStyle(el, 'padding-left'))
-    padding += parseFloat(@getStyle(el, 'padding-right'))
-    width - padding
-
-  addClass: (el, class_name) ->
-    return unless el
-    classes = el.className.split(' ')
-    classes = _.uniq(classes.concat(class_name.split(' ')))
-    el.className = classes.join(' ')
-    el
-
-  removeClass: (el, class_name) ->
-    return unless el
-    classes = el.className.split(' ')
-    classes = _.without(classes, class_name.split(' ')...)
-    el.className = classes.join(' ')
-    el
-
-  show: (el, options={}) ->
-    return unless el
-    if options.visibility
-      el.style.visibility = 'visible'
-    else
-      el.style.display = 'block'
-
-  hide: (el, options={}) ->
-    return unless el
-    if options.visibility
-      el.style.visibility = 'hidden'
-    else
-      el.style.display = 'none'
-
-  isVisible: (el, options={}) ->
-    DOM.getStyle(el, 'display') != 'none' &&
-    DOM.getStyle(el, 'visibility') != 'hidden' &&
-    (!options.check_exists || DOM.exists(el))
-
-  exists: (el) ->
-    _.any(DOM.parentNodes(el), (_el) -> _el == document.body)
-
-  getStyle: (el, name) ->
-    # convert to camel case (e.g. 'padding-left' to 'paddingLeft')
-    name = name.replace(/-([a-z])/ig, ((match, char) -> char.toUpperCase()))
-
-    val = el.style[name]
-    val = DOM.getComputedStyle(el, name) if !val || val.match(/^[\s\r\t\n]*$/)
-    val
-
-  getComputedStyle: (el, name) ->
-    document.defaultView?.getComputedStyle(el)[name]
-
-  setStyle: (el, name, value) ->
-    el.style[name] = value
-
-  setStyles: (el, styles) ->
-    for name, value of styles
-      @setStyle(el, name, value)
-
-  setInnerText: (el, value) ->
-    el.textContent = value
-    el.innerText = value
-    value
-
-  parentNodes: (el) ->
-    nodes = []
-    node = el
-    while node = node.parentNode
-      nodes.push(node)
-    nodes
-
-  _events: {}
-  _event_id_counter: 0
-  _generateEventId: -> @_event_id_counter++
-
-  on: (el, events, callback, capture=false) ->
-    return unless el
-    method = 'addEventListener' if el.addEventListener
-    method ?= 'attachEvent' if el.attachEvent
-    return unless method
-
-    for event in events.split(' ')
-      event_id = @_generateEventId()
-      el._events ?= []
-      @_events[event_id] = el[method](event, callback, capture)
-      el._events.push(event_id)
-
-  off: (el, events, callback, capture=false) ->
-    return unless el
-    method = 'removeEventListener' if el.removeEventListener
-    method ?= 'detachEvent' if el.detachEvent
-    return unless method
-
-    for event in events.split(' ')
-      el[method](event, callback, capture)
-
-  once: (el, events, callback, capture=false) ->
-    _callback = ->
-      callback.apply(this, arguments)
-      Marbles.DOM.off(el, events, _callback, capture)
-
-    @on(el, events, _callback, capture)
-
-  formElementValue: (el) ->
-    if el.nodeName.toLowerCase() == 'select'
-      multiple = el.multiple
-      value = if multiple then [] else ""
-      for option in DOM.querySelectorAll('option', el)
-        continue unless option.selected
-        if multiple
-          value.push option.value
-        else
-          value = option.value
-          break
-      value
-    else
-      el.value
-
-  serializeForm: (form, options = {}) ->
-    params = {}
-    for el in DOM.querySelectorAll('[name]', form)
-      value = if el.type == 'file'
-        el.files
-      else
-        DOM.formElementValue(el)
-
-      continue if el.type is 'radio' && !el.checked
-      continue if el.type is 'checkbox' && !el.checked
-
-      if options.expand_nested && el.name.match(/^([^\[]+)\[([^\[]+)\]/)
-        name = RegExp.$1
-        parts = []
-
-        _name = el.name
-        _offset = name.length
-        while _name.slice(_offset, _name.length).match(/^\[([^\[]+)\]/)
-          if _part = RegExp.$1
-            _offset += _part.length + 2
-            parts.push(_part)
-
-        if options.use_keypath
-          params["#{name}.#{parts.join('.')}"] = value
-        else
-          _obj = (params[name] ?= {})
-          for _part, index in parts
-            if index == parts.length-1
-              _obj[_part] = value
-            else
-              _obj = (_obj[_part] = {})
-      else
-        params[el.name] = value
-    params
-
-  setElementValue: (el, val) ->
-    if el.nodeName.toLowerCase() == 'select' && el.multiple
-      val = [val] unless _.isArray(val)
-      for option in DOM.querySelectorAll('option', el)
-        option.selected = true if val.indexOf(option.value) != -1
-    else
-      el.value = val
-
-  loadFormParams: (form, params) ->
-    for key, val of params
-      el = DOM.querySelector("[name=#{key}]")
-      continue unless el
-      DOM.setElementValue(el, val)
-
-}
-
+        return option.value
+
+  else
+    return el.value
+
+DOM.setFormElementValue = (el, value) ->
+  if el.nodeName.toLowerCase() == 'select' && el.multiple
+    value = [value] unless typeof value == 'object' && typeof value.length is 'number'
+
+    for option in el.querySelectorAll('option')
+      continue if value.indexOf(option.value) == -1
+
+      option.selected = true
+
+  else
+    el.value = value
+
+  return null
+
+DOM.serializeForm = (form_el) ->
+  values = {}
+
+  for el in form_el.querySelectorAll('[name]')
+    value = @getFormElementValue(el)
+
+    continue if el.type is 'checkbox' && !el.checked
+    continue if el.type is 'radio' && !el.checked
+
+    values[el.name] = value
+
+  return values
+
+DOM.deserializeForm = (form_el, values) ->
+  for name, value of values
+    for el in form_el.querySelectorAll("[name=#{key}]")
+      @setFormElementValue(el, value)
+
+  return null
+


no longer running install on script start

--- rbn-pi.coffee
+++ rbn-pi.coffee
@@ -26,9 +26,10 @@
 		# @loadPackages()

 		NPM.load (err, @npm) =>
-			# Install!
-			@install =>
-				@watchReleases()
+			# Start the script
+			@startRunning()
+			# Poll for new releases every once in a while
+			@watchReleases()

 	log: winston


Fix pattern matching on tuples

Fix pattern matching on tuples

--- src/js/app/compilers/teascript/compiler.coffee
+++ src/js/app/compilers/teascript/compiler.coffee
@@ -200,7 +200,7 @@

 labelNames = (pattern) ->
   if Array.isArray pattern
-    if isList pattern
+    if isList(pattern) or isTuple pattern
       (inside pattern).map labelNames
     else
       [op, args...] = inside pattern
@@ -576,8 +576,7 @@
         else
           [elem, "$sequenceAt(#{i}, #{exp})"])
   (pattern) ->
-    [constr, elems...] = inside pattern
-    label = "'#{constr.token}'"
+    elems = inside pattern
     trigger: isTuple pattern
     cache: true
     cond: (exp) ->


Call `update` `interactiveFrame` to set the plugin title as part of first call to CODAP plugin API [#145641427]

--- src/code/models/codap-connect.coffee
+++ src/code/models/codap-connect.coffee
@@ -40,17 +40,22 @@
     # or if we are in standalone mode.
     @codapPhone.call([
       {
-        action: 'get',
+        action: 'update'
+        resource: 'interactiveFrame'
+        values: { title: tr "~CODAP.INTERACTIVE_FRAME.TITLE"}
+      },
+      {
+        action: 'get'
         resource: 'interactiveFrame'
       },
       {
-        action: 'get',
+        action: 'get'
         resource: 'dataContext'
       }
     ], (ret) =>
       if ret
-        frame   = ret[0]
-        context = ret[1]
+        frame   = ret[1]
+        context = ret[2]

         @graphStore.setUsingCODAP true

@@ -64,11 +69,6 @@
         # (API 1.0). We ignore the dataContext if we find game state in the interactiveFrame
         state = frame?.values.savedState or
                 context?.values?.contextStorage?.gameState
-
-        @codapPhone.call
-          action: 'update'
-          resource: 'interactiveFrame'
-          values: { title: tr "~CODAP.INTERACTIVE_FRAME.TITLE"}

         if state?
           @graphStore.deleteAll()


More tests for computed attrs.

More tests for computed attrs.


Only prevent requests when restoring with a snapshot

--- src/turbolinks/visit.coffee
+++ src/turbolinks/visit.coffee
@@ -125,7 +125,10 @@
       when "advance", "restore" then "pushHistoryWithLocationAndRestorationIdentifier"

   shouldIssueRequest: ->
-    @action is "advance" or not @hasSnapshot()
+    if @action is "restore"
+      not @hasSnapshot()
+    else
+      true

   saveSnapshot: ->
     unless @snapshotSaved


Grey out in search results nodes added to the pallete

--- src/code/stores/palette-store.coffee
+++ src/code/stores/palette-store.coffee
@@ -148,7 +148,8 @@
     @palette.splice(0, 0, @palette.splice(index, 1)[0])

   inPalette: (node) ->
-    _.find @palette, {key: node.key}
+    # node in Pallete is standardized, arg node not always
+    _.find @palette, {key: node.key || @makeNodeSignature(node)}

   findByUUID: (uuid) ->
     _.find @palette, {uuid: uuid}


BUGFIX: nodes fixes: - syntax error @ line 18 - default timeout of 0 causes all http requests to fail with "socket hung up"

--- src/urlhandlers/node.coffee
+++ src/urlhandlers/node.coffee
@@ -5,7 +5,7 @@

 class NodeURLHandler
     @get: (url, options, cb) ->
-
+
         url = uri.parse(url)
         if url.protocol is 'file:'
             fs.readFile url.pathname, 'utf8', (err, data) ->
@@ -15,7 +15,7 @@
         else
             data = ''

-            timeout_wrapper ( req ) ->
+            timeout_wrapper = ( req ) ->
                 return ->
                     req.abort( );

@@ -23,7 +23,7 @@
                 res.on 'data', (chunk) ->
                     data += chunk
                     clearTimeout( timing );
-                    timing = setTimeout( fn, timeout );
+                    timing = setTimeout( fn, options.timeout or 120000 );
                 res.on 'end', ->
                     clearTimeout( timing );
                     xml = new DOMParser().parseFromString(data)
@@ -31,8 +31,8 @@
             req.on 'error', (err) ->
                 clearTimeout( timing );
                 cb(err)
-
+
             fn = timeout_wrapper req
-            timing = setTimeout( fn, timeout );
+            timing = setTimeout( fn, options.timeout or 120000 );

-module.exports = NodeURLHandler+module.exports = NodeURLHandler


Replace deep-pluck with keyfinder module

--- lib/request.coffee
+++ lib/request.coffee
@@ -1,13 +1,13 @@

-_       = require 'lodash'
-request = require 'request'
-deepPluck = require 'deep-pluck'
+_         = require 'lodash'
+request   = require 'request'
+keyfinder = require 'keyfinder'

 isError = (errorCode) ->
   if errorCode? and errorCode.toString() isnt '0' then true else false

 createErrorMsg = (body, codeKey, msgKey) ->
-  [code, msg] = [deepPluck(body, codeKey)[0], deepPluck(body, msgKey)[0]]
+  [code, msg] = [keyfinder(body, codeKey)[0], keyfinder(body, msgKey)[0]]
   if isError(code) then "#{code} - #{msg}" else null

 parseResponse = (body) ->


fix handlebars help

fix handlebars help

--- src/client/handlebars-helpers.coffee
+++ src/client/handlebars-helpers.coffee
@@ -360,13 +360,14 @@
      * write a value or a fallback if the value is falsy
      *
      *      foo='bar'
-     *
-     *      1. {{outputIf foo 'bar'}}
-     *      2. {{outputIf foo 'baz' 'checked'}}
+     *      baz=0
+     *
+     *      1. {{fallback foo 'default_value'}}
+     *      2. {{fallback baz 'default_value'}}
      *
      *      =>
-     *      1.
-     *      2. 'checked'
+     *      1. 'bar'
+     *      2. 'default_value'
     ###

     HandlebarsHelpers.fallback = (obj, fb)->


gave timeout a name

gave timeout a name

--- js-library/app/js/views/balloon.coffee
+++ js-library/app/js/views/balloon.coffee
@@ -1,3 +1,5 @@
+stop_loading_timeout = 17000
+
 class Factlink.Balloon
   constructor: (fact) ->
     @_fact = fact
@@ -37,7 +39,7 @@

   startLoading: ->
     @_loading = true
-    @_loadingTimeoutID = setTimeout (=> @stopLoading()), 17000
+    @_loadingTimeoutID = setTimeout (=> @stopLoading()), stop_loading_timeout
     @$el.addClass "fl-loading"

   stopLoading: ->


Added copy button to JSON dialog

Added copy button to JSON dialog

--- SingularityUI/app/utils.coffee
+++ SingularityUI/app/utils.coffee
@@ -43,15 +43,38 @@
         if type is 'requestHistory'
             lookupObject = app.allRequestHistories

+        copyButton =
+            text: "Copy"
+            type: "button"
+            className: "vex-dialog-button-secondary copy-button"
+
         vex.dialog.alert
             buttons: [
-                $.extend({}, vex.dialog.buttons.YES, text: 'Done')
+                $.extend({}, vex.dialog.buttons.YES, text: 'Done'),
+                copyButton
             ]
             className: 'vex-theme-default vex-theme-default-json-view'
             message: "<pre>#{ utils.htmlEncode lookupObject[objectId].JSONString }</pre>"
             afterOpen: ($vexContent) ->
                 utils.scrollPreventDefaultAtBounds $vexContent.find('pre')
                 utils.scrollPreventAlways $vexContent.parent()
+
+                # Dity hack to make ZeroClipboard play along
+                # The Flash element doesn't work if it falls outside the
+                # bounds of the body, even if it's inside the dialog
+                overlayHeight = $(".vex-overlay").height()
+                $("body").css "min-height", overlayHeight + "px"
+
+                $button = $vexContent.find ".copy-button"
+                $button.attr "data-clipboard-text", $vexContent.find("pre").html()
+
+                zeroClipboardClient = new ZeroClipboard $button[0],
+                    moviePath: "#{ config.appRoot }/static/swf/ZeroClipboard.swf"
+
+                zeroClipboardClient.on "load", ->
+                    zeroClipboardClient.on "complete", ->
+                        $button.val "Copied"
+                        setTimeout (-> $button.val "Copy"), 800

     @scrollPreventDefaultAtBounds: ($scroll) ->
         $scroll.bind 'mousewheel', (e, delta) ->


Fix ZeroClipboard swf movie path causing #118

--- SingularityUI/app/utils.coffee
+++ SingularityUI/app/utils.coffee
@@ -153,7 +153,7 @@
         $pre.prepend $copyButton

         zeroClipboardClient = new ZeroClipboard $copyButton[0],
-            moviePath: "#{ constants.appRoot }/static/swf/ZeroClipboard.swf"
+            moviePath: "#{ constants.appRoot }static/swf/ZeroClipboard.swf"
             forceHandCursor: true

         copiedTimeout = undefined


#0 @bug #done 修复会返回用户密码的bug

#0 @bug #done 修复会返回用户密码的bug

--- biz/member.coffee
+++ biz/member.coffee
@@ -88,8 +88,10 @@

   #获取用户列表
   allMember: (req, res, next)->
-    @find null, (err, result)->
+    options =
+      fields: ['id', 'username', 'email', 'realname', 'git']
+    @find null, options, (err, result)->
       res.json result

-module.exports = Member+module.exports = Member


Points to symbols

Points to symbols

--- src/components/scoreboard_row.coffee
+++ src/components/scoreboard_row.coffee
@@ -7,16 +7,37 @@
 ScoreboardRow = React.createClass
   displayName: 'ScoreboardRow'

+  pointsToSymbols: (points) ->
+    switch points
+      when 0 then ''
+      when 1 then '\\'
+      when 2 then 'X'
+      else
+        points -= 3
+        output = String.fromCharCode('9421')
+        while points >= 5
+          output += '| | | |'
+          points -= 5
+        while points > 0
+          output += ' |'
+          points--
+        output
+
   render: ->
     tr {},
       td {},
-        div {className: 'right'}, '' # Team 1 points
+        div
+          className: 'score-ticks right'
+          dangerouslySetInnerHTML: {__html: @pointsToSymbols(@props.teamPoints[@props.teams.get(0).get('id')])}
       td {},
         div {className: 'middle'}, Cricket[@props.points]
       td {},
-        div {className: 'left'}, '' # Team 2 points
+        div
+          className: 'score-ticks left'
+          dangerouslySetInnerHTML: {__html: @pointsToSymbols(@props.teamPoints[@props.teams.get(1).get('id')])}

 mapStateToProps = (state) ->
   teams: state.team.get 'teams'
+  teamPoints: {1: 10, 2: 1}

 module.exports = connect(mapStateToProps)(ScoreboardRow)


Load exercise id if it's in url

Load exercise id if it's in url

--- src/components/exercise-wrapper.cjsx
+++ src/components/exercise-wrapper.cjsx
@@ -11,6 +11,11 @@

   getInitialState: ->
     {}
+
+  componentDidMount: ->
+    if @props.exerciseId
+      this.refs.exerciseId.getDOMNode().value = @props.exerciseId
+      @loadExercise()

   publishExercise: ->
     if confirm('Are you sure you want to publish?')


Refresh translations after adding a mark Bit of a hack to fix a translation bug. Closes #79

--- limit-marks.coffee
+++ limit-marks.coffee
@@ -26,3 +26,6 @@
     stopThatDialog.show() unless hasBeenOpened
     stopThatDialog.el.find('button').focus()
     hasBeenOpened = true
+
+  setTimeout => # ewwwwwww
+    t7e.refresh()


Fix rspec test for properties host

Fix rspec test for properties host

--- spec/javascripts/mixingpanel_properties_spec.js.coffee
+++ spec/javascripts/mixingpanel_properties_spec.js.coffee
@@ -4,7 +4,7 @@

   it "should map URI and host", ->
     mpp = new MixingpanelProperties(@internal_domain, "http://www.example.com:8080/blah?meh#wat#wat")
-    expect(mpp.host).toBe("example.com:8080")
+    expect(mpp.host).toBe("example.com")
     expect(mpp.uri.protocol).toBe('http:')
     expect(mpp.uri.hostname).toBe('www.example.com')
     expect(mpp.uri.host).toBe('www.example.com:8080')


Removed page UUID from cnxfeature tags

Removed page UUID from cnxfeature tags

--- src/components/tags/cnx-feature.cjsx
+++ src/components/tags/cnx-feature.cjsx
@@ -10,12 +10,12 @@
     actions: React.PropTypes.object.isRequired

   validateInput: (value) ->
-    'Must match CNX module ID with feature ID' unless value.match(
-      /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}#[\w\-]+$/i
+    'Must match feature ID' unless value.match(
+      /^[\w\-]+$/i
     )

   cleanInput: (val) ->
-    val.replace(/[^\w\-#]/g, '')
+    val.replace(/[^\w\-]/g, '')

   render: ->
     <MultiInput
@@ -23,7 +23,7 @@
       label='CNX Feature'
       prefix='context-cnxfeature'
       cleanInput={@cleanInput}
-      placeholder='#########-####-###-####-############feature-id'
+      placeholder='feature-id'
       validateInput={@validateInput}
     />


modalId

modalId

--- app/assets/javascripts/media_lib/jquery.nmdUploader.js.coffee
+++ app/assets/javascripts/media_lib/jquery.nmdUploader.js.coffee
@@ -21,16 +21,15 @@
 $ ->
   methods =
     init: (options) ->
-      settings = _.extend
-        open: false
-        multiselect: false
+      settings = $.extend true,
+        {}
       , options

       throw "host isn't defined" unless settings.host
       throw "tenant isn't defined" unless settings.tenant

       @each ->
-        $uploader = $(@)
+        $uploader = $ @
         uploader = $uploader.data()

         uploaderId = $.md5(Math.random())[0..3]
@@ -39,11 +38,11 @@

         getUrl = ->
           # "Живой" префикс
-          prefix = settings.prefix or $uploader.data('prefix')
-          throw "prefix isn't defined" unless prefix?
+          prefix = settings.prefix ? $uploader.data('prefix')
+          throw "prefix isn't defined" unless prefix

-          multiselect = settings.multiselect or $uploader.data('multiselect')
-          type = settings.type or $uploader.data('type')
+          multiselect = settings.multiselect ? $uploader.data('multiselect')
+          type = settings.type ? $uploader.data('type')

           "#{settings.host}/#
             tenant/#{settings.tenant}/
@@ -53,31 +52,37 @@
             prefix/#{prefix}
           ".replace /\ /g, ''

-        openUploader = ->
-          if settings.open
-            $uploaderIframes = $("##{settings.open}").find('.js-uploader-iframe:first')
+        getIframe = ->
+          modalId = settings.modalId ? $uploader.data('modalId')
+          if modalId
+            $uploaderIframe = $("##{modalId}").find('.js-uploader-iframe')
           else
-            $uploaderIframes = $uploader.find('.js-uploader-iframe:first')
-
-          if $uploaderIframes.length
+            $uploaderIframe = $uploader.find('.js-uploader-iframe')
+          $uploaderIframe.first()
+
+        initIframe = ->
+          $iframe = getIframe()
+          if $iframe.length
             iframeTemplate = _.template '''
               <iframe width='<%= width %>' height='<%= height %>' src='<%= src %>' frameborder='0'></iframe>
             '''

-            $uploaderIframes.each ->
-              $iframe = $ @
-              iframeData = $iframe.data()
-              $iframe.html iframeTemplate
-                src: getUrl()
-                width: iframeData.width or '100%'
-                height: iframeData.height or 500
+            $iframe.html iframeTemplate $.extend
+              src: getUrl()
+              width: '100%'
+              height: 500
+            , $iframe.data()

-            $uploaderIframes.closest('div.modal').modal('show') if $.fn.modal?
+        openUploader = ->
+          $iframe = getIframe()
+          if $iframe.length
+            $iframe.closest('div.modal').modal('show') if $.fn.modal?
+            initIframe()
           else
             popupWindow = open getUrl(), 'NMD Media Lib', 'scrollbars=1, width=800, height=500'
             popupWindow.focus()

-        if settings.open
+        if settings.open ? $uploader.data('open')
           openUploader()
         else
           $uploader.find('.js-uploader-open').on 'click', (e) ->


remove console.log debugging line

remove console.log debugging line

--- services/web/app/coffee/router.coffee
+++ services/web/app/coffee/router.coffee
@@ -218,7 +218,6 @@
 				throw new Error("Test error")

 		app.post '/error/client', (req, res, next) ->
-			console.log req.body
 			logger.error err: req.body.error, meta: req.body.meta, "client side error"
 			res.send(204)


Min fixes for get payment method value

Min fixes for get payment method value

--- app/assets/javascripts/zahlen/subscription_checkout.js.coffee
+++ app/assets/javascripts/zahlen/subscription_checkout.js.coffee
@@ -14,10 +14,11 @@
   formSubmitHandler: ->
     self = @
     form_selector = @constructor.form_selector
-    payment_method = @constructor.payment_method
+    self.payment_method = $("input[data-zahlen='payment-method']:checked").val()
+
     $(document).off("submit#{form_selector}").on "submit#{form_selector}", "#{form_selector}", (e) ->
       e.preventDefault()
-      if payment_method in ['card']
+      if self.payment_method in ['card']
         if !self.validateForm()
           SubscriptionCheckout.showError(Zahlen.locale().errors.not_valid)
           return false
@@ -25,11 +26,11 @@
       $(this).find(':submit').prop('disabled', true);
       $('.zahlen-spinner').show();

-      if !payment_method?
+      if !self.payment_method?
         console.log "No payment method has been found. Please check your form."
         return false

-      if payment_method in ['card']
+      if self.payment_method in ['card']
         Conekta.Token.create($(form_selector), self.conektaSuccessResponseHandler, self.conektaErrorResponseHandler)
       else
         self.offlineReponseHandler()
@@ -96,8 +97,6 @@
     form.append $('<input type="hidden" name="gateway_card_id">').val(token.id)
     # Remove all card fields from beign submit
     form.find("input[data-conekta*='card']").remove()
-
-    console.log form

     base_path = '/zahlen'
     action = form.attr('action')


Fix never resolving getAllObjects call

Fix never resolving getAllObjects call

--- lib/clientModules/object.stream.coffee
+++ lib/clientModules/object.stream.coffee
@@ -145,8 +145,6 @@
   new Promise (resolve, reject) =>
     done = false
     fail = (error) ->
-      if done
-        return
       done = true
       return reject(errors.ensureRetsError('getObject', error))
     req = retsHttp.streamRetsMethod('getObject', @retsSession, options, fail, null, @client)


noop: add some comments

noop: add some comments

--- background_scripts/main.coffee
+++ background_scripts/main.coffee
@@ -116,6 +116,8 @@
     switch position
       when "start" then tabIndex = 0
       when "before" then tabIndex = request.tab.index
+      # if on Chrome or on Firefox but without openerTabId, `tabs.create` opens a tab at the end.
+      # but on Firefox and with openerTabId, it opens a new tab next to the opener tab
       when "end" then tabIndex = (if Utils.isFirefox() then 9999 else null)
       # "after" is the default case when there are no options.
       else tabIndex = request.tab.index + 1
@@ -130,6 +132,7 @@
     tabConfig.openerTabId = request.tab.id if canUseOpenerTabId

     chrome.tabs.create tabConfig, (tab) ->
+      # clean position and active, so following `openUrlInNewTab(request)` will create a tab just next to this new tab
       callback extend request, {tab, tabId: tab.id, position: "", active: false}

   # Opens request.url in new window and switches to it.


Add donate route

Add donate route

--- src/scripts/router.coffee
+++ src/scripts/router.coffee
@@ -16,11 +16,14 @@
       @route '', 'index', () ->
         @appView.render('home')

+      @route 'workspace', 'workspace', () ->
+        @appView.render('workspace')
+
+      @route 'donate', 'donate', () ->
+        @appView.render('donate')
+
       @route 'contents', 'contents', () ->
         @appView.render('contents')
-
-      @route 'workspace', 'workspace', () ->
-        @appView.render('workspace')

       # Match and extract uuid and page numbers separated by a colon
       @route /^contents\/([^:]+):?([0-9]*)/, 'media', (uuid, page) ->


added layouts to main.coffee

added layouts to main.coffee

--- bokehjs/src/coffee/main.coffee
+++ bokehjs/src/coffee/main.coffee
@@ -117,6 +117,13 @@
 Bokeh.HoverTool              = require("./tool/inspectors/hover_tool")
 Bokeh.CrosshairTool          = require("./tool/inspectors/crosshair_tool")

+# widgets
+Bokeh.Layout                 = require('./widget/layout')
+Bokeh.BaseBox                = require('./widget/basebox')
+Bokeh.HBox                   = require('./widget/hbox')
+Bokeh.VBox                   = require('./widget/vbox')
+Bokeh.VBoxForm               = require('./widget/vboxform')
+
 # Add the jquery plugin
 require("./api/plugin")


Fix listener race condition crash

Fix listener race condition crash

--- src/server/session.coffee
+++ src/server/session.coffee
@@ -139,6 +139,8 @@
       #p "Registering listener on #{docName} by #{socket.id} at #{version}"

       docState[docName].listener = listener = (opData) ->
+        # Listener can be called after close
+        return unless docState?[docName]
         throw new Error 'Consistency violation - doc listener invalid' unless docState[docName].listener == listener

         #p "listener doc:#{docName} opdata:#{i opData} v:#{version}"


Changes wrong class name.

Changes wrong class name.

--- client/app/lib/activity/sidebar/channelsidebarmorelink.coffee
+++ client/app/lib/activity/sidebar/channelsidebarmorelink.coffee
@@ -2,7 +2,7 @@
 SidebarMoreLink       = require './sidebarmorelink'

-module.exports = class SidebarMoreLink extends SidebarMoreLink
+module.exports = class ChannelSidebarMoreLink extends SidebarMoreLink

   constructor: (options = {}, data) ->


Add tests for remove filter

Add tests for remove filter

--- spec/filter/CommentFilter_spec.coffee
+++ spec/filter/CommentFilter_spec.coffee
@@ -159,6 +159,23 @@
           filter.addRule
             'mode': '???').toThrow()

+    describe '.removeRule', ->
+      rule =
+        'mode': 'reject',
+        'subject': 'text',
+        'op': '=',
+        'value': 'Foo'
+
+      it 'removes an added rule', ->
+        filter.addRule rule
+        expect(filter.removeRule rule).toBe true
+        expect(filter.rules.length).toBe 0
+
+      it 'returns false when rule is not found', ->
+        newrule =
+          'mode': 'reject'
+        expect(filter.removeRule newrule).toBe false
+
     describe '.addModifier', ->
       modifier = (cmt) ->
         cmt.color = 0xffffff


Fix bug with mute button (bad selector)

Fix bug with mute button (bad selector)

--- src/frontend/modules/userList.coffee
+++ src/frontend/modules/userList.coffee
@@ -83,15 +83,14 @@

#{login}

-
mute

+
mute

kick

promote

#{login}

"
         ).appendTo jqLi
-        muteBtn = (jqLi.first 'li')
-        muteBtn.click @muteStream
+        (jqLi.find '.muteBtn').click @muteStream

     # Update the user list window.
     display         : () =>


Fix selector.

Fix selector.

--- common/lib/xmodule/xmodule/js/src/tabs/tabs-aggregator.coffee
+++ common/lib/xmodule/xmodule/js/src/tabs/tabs-aggregator.coffee
@@ -9,7 +9,7 @@
     ###

     # hide editor/settings bar
-    $('.component-edit-header').hide()
+    @element.closest('.component-editor').find('.component-edit-header').hide()

     @$tabs = $(".tab", @element)
     @$content = $(".component-tab", @element)


Better installed button copy

Better installed button copy

--- core/app/backbone/views/react_install_extension_button.coffee
+++ core/app/backbone/views/react_install_extension_button.coffee
@@ -43,7 +43,7 @@
     else if @props.huge_button && extension_installed
       _button ['button', 'button-huge',
         disabled: true],
-        'Factlink already installed.'
+        'Factlink has been installed!'
     else if (extension_installed || !browserName) && !@props.huge_button
       _div [] # nothing.
     else