mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-12 00:31:21 -05:00
Merge branch 'master' into production
This commit is contained in:
commit
07bf0238b4
31 changed files with 606 additions and 564 deletions
Binary file not shown.
Before Width: | Height: | Size: 68 KiB |
BIN
app/assets/images/pages/about/new_languages.png
Normal file
BIN
app/assets/images/pages/about/new_languages.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 55 KiB |
BIN
app/assets/images/pages/about/new_languages_xs.png
Normal file
BIN
app/assets/images/pages/about/new_languages_xs.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
|
@ -52,6 +52,7 @@ module.exports = class CocoRouter extends Backbone.Router
|
||||||
'artisans/solution-problems': go('artisans/SolutionProblemsView')
|
'artisans/solution-problems': go('artisans/SolutionProblemsView')
|
||||||
'artisans/thang-tasks': go('artisans/ThangTasksView')
|
'artisans/thang-tasks': go('artisans/ThangTasksView')
|
||||||
'artisans/level-concepts': go('artisans/LevelConceptMap')
|
'artisans/level-concepts': go('artisans/LevelConceptMap')
|
||||||
|
'artisans/level-guides': go('artisans/LevelGuidesView')
|
||||||
|
|
||||||
'beta': go('HomeView')
|
'beta': go('HomeView')
|
||||||
|
|
||||||
|
|
|
@ -579,17 +579,17 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese",
|
||||||
tip_programming_not_about_computers: "天文学が望遠鏡に関する学問でないのと同様に、計算機科学はコンピュータに関する学問ではない。 - エドガー・ダイクストラ"
|
tip_programming_not_about_computers: "天文学が望遠鏡に関する学問でないのと同様に、計算機科学はコンピュータに関する学問ではない。 - エドガー・ダイクストラ"
|
||||||
tip_mulan: "できると信じていれば、できる。 - ムーラン"
|
tip_mulan: "できると信じていれば、できる。 - ムーラン"
|
||||||
|
|
||||||
# play_game_dev_level:
|
play_game_dev_level:
|
||||||
# created_by: "Created by {{name}}"
|
created_by: "作成者:{{name}}"
|
||||||
# how_to_play_title: "How to play:"
|
how_to_play_title: "遊び方:"
|
||||||
# how_to_play_1: "Use the mouse to control the hero!"
|
how_to_play_1: "マウスでヒーローを操作しましょう!"
|
||||||
# how_to_play_2: "Click anywhere on the map to move to that location."
|
how_to_play_2: "マップの動きたい場所をどこでもクリックしましょう."
|
||||||
# how_to_play_3: "Click on the ogres to attack them."
|
how_to_play_3: "オーガをクリックして攻撃しましょう."
|
||||||
# restart: "Restart Level"
|
restart: "レベルをリセット"
|
||||||
# play: "Play Level"
|
play: "プレイレベル"
|
||||||
# play_more_codecombat: "Play More CodeCombat"
|
play_more_codecombat: "もっとCodeCombatで遊ぶ"
|
||||||
# default_student_instructions: "Click to control your hero and win your game!"
|
default_student_instructions: "ヒーローをクリックしてゲームに勝ちましょう!"
|
||||||
# back_to_coding: "Back to Coding"
|
back_to_coding: "コーディングに戻る"
|
||||||
|
|
||||||
game_menu:
|
game_menu:
|
||||||
inventory_tab: "インベントリー"
|
inventory_tab: "インベントリー"
|
||||||
|
@ -767,8 +767,8 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese",
|
||||||
current_value: "現在値"
|
current_value: "現在値"
|
||||||
default_value: "デフォルト値"
|
default_value: "デフォルト値"
|
||||||
parameters: "パラメータ"
|
parameters: "パラメータ"
|
||||||
# required_parameters: "Required Parameters"
|
required_parameters: "必須パラメーター"
|
||||||
# optional_parameters: "Optional Parameters"
|
optional_parameters: "任意パラメーター"
|
||||||
returns: "リターン"
|
returns: "リターン"
|
||||||
granted_by: "スキルを与えてくれるアイテム:"
|
granted_by: "スキルを与えてくれるアイテム:"
|
||||||
|
|
||||||
|
@ -823,9 +823,9 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese",
|
||||||
phoenix_title: "ソフトウェアエンジニア"
|
phoenix_title: "ソフトウェアエンジニア"
|
||||||
nolan_title: "地区担当マネージャー"
|
nolan_title: "地区担当マネージャー"
|
||||||
elliot_title: "パートナーシップマネージャー"
|
elliot_title: "パートナーシップマネージャー"
|
||||||
# elliot_blurb: "Mindreader"
|
elliot_blurb: "読心術者"
|
||||||
# lisa_title: "Market Development Rep"
|
lisa_title: "市場開発代表"
|
||||||
# sean_title: "Territory Manager"
|
sean_title: "地域部長"
|
||||||
retrostyle_title: "イラスト"
|
retrostyle_title: "イラスト"
|
||||||
retrostyle_blurb: "レトロスタイルのゲーム"
|
retrostyle_blurb: "レトロスタイルのゲーム"
|
||||||
jose_title: "ミュージック"
|
jose_title: "ミュージック"
|
||||||
|
@ -1564,7 +1564,7 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese",
|
||||||
article_title: "アーティクル エディター"
|
article_title: "アーティクル エディター"
|
||||||
thang_title: "サングエディター"
|
thang_title: "サングエディター"
|
||||||
level_title: "レベルエディター"
|
level_title: "レベルエディター"
|
||||||
# course_title: "Course Editor"
|
course_title: "コースエディター"
|
||||||
achievement_title: "実績エディター"
|
achievement_title: "実績エディター"
|
||||||
poll_title: "投票エディター"
|
poll_title: "投票エディター"
|
||||||
back: "バック"
|
back: "バック"
|
||||||
|
@ -1750,7 +1750,7 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese",
|
||||||
rank_failed: "ランキングに送信できませんでした。"
|
rank_failed: "ランキングに送信できませんでした。"
|
||||||
rank_being_ranked: "ランキングにのっています"
|
rank_being_ranked: "ランキングにのっています"
|
||||||
rank_last_submitted: "送信"
|
rank_last_submitted: "送信"
|
||||||
# help_simulate: "Help simulate games?"
|
help_simulate: "試合のシミュレートのヘルプ?"
|
||||||
# code_being_simulated: "Your new code is being simulated by other players for ranking. This will refresh as new matches come in."
|
# code_being_simulated: "Your new code is being simulated by other players for ranking. This will refresh as new matches come in."
|
||||||
# no_ranked_matches_pre: "No ranked matches for the "
|
# no_ranked_matches_pre: "No ranked matches for the "
|
||||||
# no_ranked_matches_post: " team! Play against some competitors and then come back here to get your game ranked."
|
# no_ranked_matches_post: " team! Play against some competitors and then come back here to get your game ranked."
|
||||||
|
@ -2039,19 +2039,19 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese",
|
||||||
# license: "license"
|
# license: "license"
|
||||||
# oreilly: "ebook of your choice"
|
# oreilly: "ebook of your choice"
|
||||||
|
|
||||||
# calendar:
|
calendar:
|
||||||
# year: "Year"
|
year: "年"
|
||||||
# day: "Day"
|
day: "日"
|
||||||
# month: "Month"
|
month: "月"
|
||||||
# january: "January"
|
january: "1月"
|
||||||
# february: "February"
|
february: "2月"
|
||||||
# march: "March"
|
march: "3月"
|
||||||
# april: "April"
|
april: "4月"
|
||||||
# may: "May"
|
may: "5月"
|
||||||
# june: "June"
|
june: "6月"
|
||||||
# july: "July"
|
july: "7月"
|
||||||
# august: "August"
|
august: "8月"
|
||||||
# september: "September"
|
september: "9月"
|
||||||
# october: "October"
|
october: "10月"
|
||||||
# november: "November"
|
november: "11月"
|
||||||
# december: "December"
|
december: "12月"
|
||||||
|
|
|
@ -305,22 +305,22 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription:
|
||||||
enter_class_code: "Coloque seu código de classe"
|
enter_class_code: "Coloque seu código de classe"
|
||||||
enter_birthdate: "Coloque sua data de aniversário:"
|
enter_birthdate: "Coloque sua data de aniversário:"
|
||||||
parent_use_birthdate: "Responsáveis, usem o seu propio dia de nascimento."
|
parent_use_birthdate: "Responsáveis, usem o seu propio dia de nascimento."
|
||||||
# ask_teacher_1: "Ask your teacher for your Class Code."
|
ask_teacher_1: "Pergunte ao seu professor qual o código da sua turma."
|
||||||
# ask_teacher_2: "Not part of a class? Create an "
|
ask_teacher_2: "Não faz parte da turma? Crie uma "
|
||||||
# ask_teacher_3: "Individual Account"
|
ask_teacher_3: "Conta Pessoal"
|
||||||
# ask_teacher_4: " instead."
|
ask_teacher_4: " como alternativa."
|
||||||
# about_to_join: "You're about to join:"
|
about_to_join: "Sobre juntar-se:"
|
||||||
# enter_parent_email: "Enter your parent’s email address:"
|
enter_parent_email: "Informe o endereço de e-mail de seus pais:"
|
||||||
# parent_email_error: "Something went wrong when trying to send the email. Check the email address and try again."
|
parent_email_error: "Algo de errado aconteceu ao tentar enviar o email. Verifique o endereço de email e tente novamente."
|
||||||
# parent_email_sent: "We’ve sent an email with further instructions on how to create an account. Ask your parent to check their inbox."
|
parent_email_sent: "Nós enviamos um email com mais instruções de como criar uma conta. Solicite aos seus pais para que verifiquem suas caixas de emails."
|
||||||
# account_created: "Account Created!"
|
account_created: "Conta criada!"
|
||||||
# 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_student_blurb: "Anote suas informações para que você não esqueça. Seu professor também pode ajudá-lo reiniciando sua senha a qualquer momento."
|
||||||
# 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!"
|
confirm_individual_blurb: "Anote suas informações de acesso no caso de você dela depois. Verifique seu e-mail para que você possa recuperar sua senha caso a tenha esquecid. Verifique sua caixa de entrada de emails!"
|
||||||
# write_this_down: "Write this down:"
|
write_this_down: "Escreva isso:"
|
||||||
# start_playing: "Start Playing!"
|
start_playing: "Comece jogando!"
|
||||||
# sso_connected: "Successfully connected with:"
|
sso_connected: "Conectado com sucesso como:"
|
||||||
# select_your_starting_hero: "Select Your Starting Hero:"
|
select_your_starting_hero: "Selecione um herói para começar:"
|
||||||
# you_can_always_change_your_hero_later: "You can always change your hero later."
|
you_can_always_change_your_hero_later: "Você poderá mudar seu herói depois."
|
||||||
|
|
||||||
recover:
|
recover:
|
||||||
recover_account_title: "Recuperar conta"
|
recover_account_title: "Recuperar conta"
|
||||||
|
@ -337,18 +337,18 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription:
|
||||||
|
|
||||||
common:
|
common:
|
||||||
back: "Voltar" # When used as an action verb, like "Navigate backward"
|
back: "Voltar" # When used as an action verb, like "Navigate backward"
|
||||||
# coming_soon: "Coming soon!"
|
coming_soon: "Em breve!"
|
||||||
continue: "Continuar" # When used as an action verb, like "Continue forward"
|
continue: "Continuar" # When used as an action verb, like "Continue forward"
|
||||||
# default_code: "Default Code"
|
default_code: "Código padrão"
|
||||||
loading: "Carregando..."
|
loading: "Carregando..."
|
||||||
# overview: "Overview"
|
overview: "Visão geral"
|
||||||
# solution: "Solution"
|
solution: "Solução"
|
||||||
# intro: "Intro"
|
intro: "Introdução"
|
||||||
saving: "Salvando..."
|
saving: "Salvando..."
|
||||||
sending: "Enviando..."
|
sending: "Enviando..."
|
||||||
send: "Enviar"
|
send: "Enviar"
|
||||||
# sent: "Sent"
|
sent: "Enviado"
|
||||||
# type: "Type"
|
type: "Tipo"
|
||||||
cancel: "Cancelar"
|
cancel: "Cancelar"
|
||||||
save: "Salvar"
|
save: "Salvar"
|
||||||
publish: "Publicar"
|
publish: "Publicar"
|
||||||
|
@ -364,7 +364,7 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription:
|
||||||
submit_patch: "Enviar arranjo"
|
submit_patch: "Enviar arranjo"
|
||||||
submit_changes: "Enviar mudanças"
|
submit_changes: "Enviar mudanças"
|
||||||
save_changes: "Salvar mudanças"
|
save_changes: "Salvar mudanças"
|
||||||
# required_field: "required"
|
required_field: "obrigatório"
|
||||||
|
|
||||||
general:
|
general:
|
||||||
and: "e"
|
and: "e"
|
||||||
|
@ -418,7 +418,7 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription:
|
||||||
wizard: "Feiticeiro"
|
wizard: "Feiticeiro"
|
||||||
first_name: "Primeiro Nome"
|
first_name: "Primeiro Nome"
|
||||||
last_name: "Último Nome"
|
last_name: "Último Nome"
|
||||||
# last_initial: "Last Initial"
|
last_initial: "Última Inicial"
|
||||||
username: "Nome de Usuário"
|
username: "Nome de Usuário"
|
||||||
|
|
||||||
units:
|
units:
|
||||||
|
@ -438,15 +438,15 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription:
|
||||||
years: "anos"
|
years: "anos"
|
||||||
|
|
||||||
play_level:
|
play_level:
|
||||||
# level_complete: "Level Complete"
|
level_complete: "Nível Completo"
|
||||||
completed_level: "Nivel Completo:"
|
completed_level: "Nivel Completo:"
|
||||||
course: "Curso:"
|
course: "Curso:"
|
||||||
done: "Pronto"
|
done: "Pronto"
|
||||||
next_level: "Proximo Nivel"
|
next_level: "Proximo Nivel"
|
||||||
next_game: "Próximo jogo"
|
next_game: "Próximo jogo"
|
||||||
# language: "Language"
|
language: "Linguagem"
|
||||||
# languages: "Languages"
|
languages: "Linguagens"
|
||||||
# programming_language: "Programming language"
|
programming_language: "Linguagem de programação"
|
||||||
show_menu: "Mostrar menu do jogo"
|
show_menu: "Mostrar menu do jogo"
|
||||||
home: "Início" # Not used any more, will be removed soon.
|
home: "Início" # Not used any more, will be removed soon.
|
||||||
level: "Fase" # Like "Level: Dungeons of Kithgard"
|
level: "Fase" # Like "Level: Dungeons of Kithgard"
|
||||||
|
@ -481,10 +481,10 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription:
|
||||||
victory_experience_gained: "XP ganho"
|
victory_experience_gained: "XP ganho"
|
||||||
victory_gems_gained: "Gemas ganhas"
|
victory_gems_gained: "Gemas ganhas"
|
||||||
victory_new_item: "Novo item"
|
victory_new_item: "Novo item"
|
||||||
# victory_new_hero: "New Hero"
|
victory_new_hero: "Novo herói"
|
||||||
victory_viking_code_school: "Pelas barbas do profeta, esse foi um nível difícil! Se você ainda não é um desenvolvedor de software, você deveria ser. Você acaba de ser priorizado para aceitação na Viking Code School, onde você pode aprender mais e se tornar um desenvolvedor web profissional em 14 semanas."
|
victory_viking_code_school: "Pelas barbas do profeta, esse foi um nível difícil! Se você ainda não é um desenvolvedor de software, você deveria ser. Você acaba de ser priorizado para aceitação na Viking Code School, onde você pode aprender mais e se tornar um desenvolvedor web profissional em 14 semanas."
|
||||||
victory_become_a_viking: "Torne-se um viking"
|
victory_become_a_viking: "Torne-se um viking"
|
||||||
# victory_no_progress_for_teachers: "Progress is not saved for teachers. But, you can add a student account to your classroom for yourself."
|
victory_no_progress_for_teachers: "O progresso não é salvo para o professores. Mas, você mesmo pode adicionar um conta de aluno na sua turma."
|
||||||
guide_title: "Guia"
|
guide_title: "Guia"
|
||||||
tome_cast_button_run: "Rodar"
|
tome_cast_button_run: "Rodar"
|
||||||
tome_cast_button_running: "Rodando"
|
tome_cast_button_running: "Rodando"
|
||||||
|
@ -494,8 +494,8 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription:
|
||||||
tome_available_spells: "Feitiços Disponíveis"
|
tome_available_spells: "Feitiços Disponíveis"
|
||||||
tome_your_skills: "Suas habilidades"
|
tome_your_skills: "Suas habilidades"
|
||||||
tome_current_method: "Método Atual"
|
tome_current_method: "Método Atual"
|
||||||
# hints: "Hints"
|
hints: "Sugestões"
|
||||||
# hints_title: "Hint {{number}}"
|
hints_title: "Sugestão {{number}}"
|
||||||
code_saved: "Código Salvo"
|
code_saved: "Código Salvo"
|
||||||
skip_tutorial: "Pular (esc)"
|
skip_tutorial: "Pular (esc)"
|
||||||
keyboard_shortcuts: "Teclas de atalho"
|
keyboard_shortcuts: "Teclas de atalho"
|
||||||
|
|
5
app/styles/artisans/level-guides-view.sass
Normal file
5
app/styles/artisans/level-guides-view.sass
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
#level-guides-view
|
||||||
|
.problem
|
||||||
|
color: red
|
||||||
|
.level-details
|
||||||
|
width: 15%
|
|
@ -247,10 +247,9 @@ block content
|
||||||
#story-languages
|
#story-languages
|
||||||
.text-center
|
.text-center
|
||||||
.text-h5(data-i18n="about.story_statistic_3c")
|
.text-h5(data-i18n="about.story_statistic_3c")
|
||||||
#language-icons.text-center(title="CoffeeScript, JavaScript, Python, Java, Lua")
|
#language-icons.text-center(title="JavaScript, Python, HTML, CSS, jQuery, Bootstrap")
|
||||||
img.hidden-xs(src="/images/pages/about/languages.png")
|
img.hidden-xs(src="/images/pages/about/new_languages.png")
|
||||||
img.hidden-sm.hidden-md.hidden-lg(src="/images/pages/about/languages_group1.png")
|
img.hidden-sm.hidden-md.hidden-lg(src="/images/pages/about/new_languages_xs.png")
|
||||||
img.hidden-sm.hidden-md.hidden-lg(src="/images/pages/about/languages_group2.png")
|
|
||||||
|
|
||||||
#story-graphic-4.text-center
|
#story-graphic-4.text-center
|
||||||
p
|
p
|
||||||
|
|
|
@ -6,11 +6,14 @@ block content
|
||||||
a(href='/artisans/thang-tasks')
|
a(href='/artisans/thang-tasks')
|
||||||
|Thang Tasks
|
|Thang Tasks
|
||||||
div
|
div
|
||||||
a(href="/artisans/level-tasks")
|
a(href='/artisans/level-tasks')
|
||||||
|Level Tasks
|
|Level Tasks
|
||||||
div
|
div
|
||||||
a(href="/artisans/solution-problems")
|
a(href='/artisans/solution-problems')
|
||||||
|Solution Problems
|
|Solution Problems
|
||||||
div
|
div
|
||||||
a(href="/artisans/level-concepts")
|
a(href='/artisans/level-concepts')
|
||||||
|Level Concept Map
|
|Level Concept Map
|
||||||
|
div
|
||||||
|
a(href='/artisans/level-guides')
|
||||||
|
|Level Guides Overview
|
||||||
|
|
36
app/templates/artisans/level-guides-view.jade
Normal file
36
app/templates/artisans/level-guides-view.jade
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// DNT
|
||||||
|
extends /templates/base
|
||||||
|
|
||||||
|
block content
|
||||||
|
div
|
||||||
|
a(href='/artisans')
|
||||||
|
span.glyphicon.glyphicon-chevron-left
|
||||||
|
span Artisans Home
|
||||||
|
button#overview-button Show Overviews
|
||||||
|
br
|
||||||
|
button#intro-button Show Intros
|
||||||
|
table.table#level-table
|
||||||
|
for levelObj in (view.levels || [])
|
||||||
|
- var level = levelObj.level
|
||||||
|
tr
|
||||||
|
td.level-details
|
||||||
|
a(href='/editor/level/'+level.get('slug') target="_blank")=level.get('name')
|
||||||
|
div
|
||||||
|
ul
|
||||||
|
for problem in levelObj.problems
|
||||||
|
li.problem=problem
|
||||||
|
td(style='width:90%')
|
||||||
|
if levelObj.overview
|
||||||
|
.panel.panel-default
|
||||||
|
.panel-heading
|
||||||
|
h2.panel-title
|
||||||
|
a(data-toggle='collapse' href='#'+level.get('slug')+'-overview-collapse') Overview
|
||||||
|
.panel-collapse.collapse.overview(id=level.get('slug')+'-overview-collapse')
|
||||||
|
pre=levelObj.overview.body
|
||||||
|
if levelObj.intro
|
||||||
|
.panel.panel-default
|
||||||
|
.panel-heading
|
||||||
|
h2.panel-title
|
||||||
|
a(data-toggle='collapse' href='#'+level.get('slug')+'-intro-collapse') Intro
|
||||||
|
.panel-collapse.collapse.intro(id=level.get('slug')+'-intro-collapse')
|
||||||
|
pre=levelObj.intro.body
|
|
@ -41,7 +41,7 @@ if view.showAds()
|
||||||
a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.slug}", disabled=level.disabled, data-level-slug=level.slug, data-level-path=level.levelPath || 'level', data-level-name=level.name)
|
a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.slug}", disabled=level.disabled, data-level-slug=level.slug, data-level-path=level.levelPath || 'level', data-level-name=level.name)
|
||||||
if level.slug == 'lost-viking'
|
if level.slug == 'lost-viking'
|
||||||
img.star(src="/file/db/thang.type/5441c3144e9aeb727cc97111/portrait.png")
|
img.star(src="/file/db/thang.type/5441c3144e9aeb727cc97111/portrait.png")
|
||||||
else if level.requiresSubscription
|
else if level.requiresSubscription && !level.adventurer
|
||||||
img.star(src="/images/pages/play/star.png")
|
img.star(src="/images/pages/play/star.png")
|
||||||
if levelStatusMap[level.slug] === 'complete'
|
if levelStatusMap[level.slug] === 'complete'
|
||||||
img.banner(src="/images/pages/play/level-banner-complete.png")
|
img.banner(src="/images/pages/play/level-banner-complete.png")
|
||||||
|
|
98
app/views/artisans/LevelGuidesView.coffee
Normal file
98
app/views/artisans/LevelGuidesView.coffee
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
RootView = require 'views/core/RootView'
|
||||||
|
template = require 'templates/artisans/level-guides-view'
|
||||||
|
|
||||||
|
Campaigns = require 'collections/Campaigns'
|
||||||
|
Campaign = require 'models/Campaign'
|
||||||
|
|
||||||
|
Levels = require 'collections/Levels'
|
||||||
|
Level = require 'models/Level'
|
||||||
|
|
||||||
|
module.exports = class LevelGuidesView extends RootView
|
||||||
|
template: template
|
||||||
|
id: 'level-guides-view'
|
||||||
|
events:
|
||||||
|
'click #overview-button': 'onOverviewButtonClicked'
|
||||||
|
'click #intro-button': 'onIntroButtonClicked'
|
||||||
|
|
||||||
|
excludedCampaigns = [
|
||||||
|
'pico-ctf', 'auditions'
|
||||||
|
]
|
||||||
|
includedCampaigns = [
|
||||||
|
'intro', 'course-2', 'course-3', 'course-4', 'course-5', 'course-6',
|
||||||
|
'web-dev-1', 'web-dev-2',
|
||||||
|
'game-dev-1', 'game-dev-2'
|
||||||
|
]
|
||||||
|
levels: []
|
||||||
|
|
||||||
|
onOverviewButtonClicked: (e) ->
|
||||||
|
@$('.overview').toggleClass('in')
|
||||||
|
onIntroButtonClicked: (e) ->
|
||||||
|
@$('.intro').toggleClass('in')
|
||||||
|
|
||||||
|
initialize: () ->
|
||||||
|
|
||||||
|
@campaigns = new Campaigns()
|
||||||
|
|
||||||
|
@listenTo(@campaigns, 'sync', @onCampaignsLoaded)
|
||||||
|
@supermodel.trackRequest(@campaigns.fetch(
|
||||||
|
data:
|
||||||
|
project: 'name,slug,levels'
|
||||||
|
))
|
||||||
|
onCampaignsLoaded: (campCollection) ->
|
||||||
|
for camp in campCollection.models
|
||||||
|
campaignSlug = camp.get 'slug'
|
||||||
|
continue if campaignSlug in excludedCampaigns
|
||||||
|
continue unless campaignSlug in includedCampaigns
|
||||||
|
levels = camp.get 'levels'
|
||||||
|
|
||||||
|
levels = new Levels()
|
||||||
|
@listenTo(levels, 'sync', @onLevelsLoaded)
|
||||||
|
levels.fetchForCampaign(campaignSlug)
|
||||||
|
#for key, level of levels
|
||||||
|
|
||||||
|
onLevelsLoaded: (lvlCollection) ->
|
||||||
|
lvlCollection.models.reverse()
|
||||||
|
#console.log lvlCollection
|
||||||
|
for level in lvlCollection.models
|
||||||
|
#console.log level
|
||||||
|
levelSlug = level.get 'slug'
|
||||||
|
overview = _.find(level.get('documentation').specificArticles, name:'Overview')
|
||||||
|
intro = _.find(level.get('documentation').specificArticles, name:'Intro')
|
||||||
|
#if intro and overview
|
||||||
|
problems = []
|
||||||
|
if not overview
|
||||||
|
problems.push 'No Overview'
|
||||||
|
else
|
||||||
|
if not overview.i18n
|
||||||
|
problems.push 'Overview doesn\'t have i18n field'
|
||||||
|
if not overview.body
|
||||||
|
problems.push 'Overview doesn\'t have a body'
|
||||||
|
else
|
||||||
|
if level.get('campaign')?.indexOf('web') is -1
|
||||||
|
jsIndex = overview.body.indexOf('```javascript')
|
||||||
|
pyIndex = overview.body.indexOf('```python')
|
||||||
|
if jsIndex is -1 and pyIndex isnt -1 or jsIndex isnt -1 and pyIndex is -1
|
||||||
|
problems.push 'Overview is missing a language example.'
|
||||||
|
if not intro
|
||||||
|
problems.push 'No Intro'
|
||||||
|
else
|
||||||
|
if not intro.i18n
|
||||||
|
problems.push 'Intro doesn\'t have i18n field'
|
||||||
|
if not intro.body
|
||||||
|
problems.push 'Intro doesn\'t have a body'
|
||||||
|
else
|
||||||
|
if intro.body.indexOf('file/db') is -1
|
||||||
|
problems.push 'Intro is missing image'
|
||||||
|
if level.get('campaign')?.indexOf('web') is -1
|
||||||
|
jsIndex = intro.body.indexOf('```javascript')
|
||||||
|
pyIndex = intro.body.indexOf('```python')
|
||||||
|
if jsIndex is -1 and pyIndex isnt -1 or jsIndex isnt -1 and pyIndex is -1
|
||||||
|
problems.push 'Intro is missing a language example.'
|
||||||
|
@levels.push
|
||||||
|
level: level
|
||||||
|
overview: overview
|
||||||
|
intro: intro
|
||||||
|
problems: problems
|
||||||
|
@levels.sort (a, b) ->
|
||||||
|
return b.problems.length - a.problems.length
|
||||||
|
@renderSelectors '#level-table'
|
|
@ -178,7 +178,7 @@ module.exports = class CampaignView extends RootView
|
||||||
context.levels = _.reject context.levels, slug: reject
|
context.levels = _.reject context.levels, slug: reject
|
||||||
if me.isOnFreeOnlyServer()
|
if me.isOnFreeOnlyServer()
|
||||||
context.levels = _.reject context.levels, 'requiresSubscription'
|
context.levels = _.reject context.levels, 'requiresSubscription'
|
||||||
@annotateLevel level for level in context.levels
|
@annotateLevels(context.levels)
|
||||||
count = @countLevels context.levels
|
count = @countLevels context.levels
|
||||||
context.levelsCompleted = count.completed
|
context.levelsCompleted = count.completed
|
||||||
context.levelsTotal = count.total
|
context.levelsTotal = count.total
|
||||||
|
@ -198,7 +198,7 @@ module.exports = class CampaignView extends RootView
|
||||||
context.adjacentCampaigns = _.filter _.values(_.cloneDeep(@campaign?.get('adjacentCampaigns') or {})), (ac) =>
|
context.adjacentCampaigns = _.filter _.values(_.cloneDeep(@campaign?.get('adjacentCampaigns') or {})), (ac) =>
|
||||||
if ac.showIfUnlocked and not @editorMode
|
if ac.showIfUnlocked and not @editorMode
|
||||||
return false if _.isString(ac.showIfUnlocked) and ac.showIfUnlocked not in me.levels()
|
return false if _.isString(ac.showIfUnlocked) and ac.showIfUnlocked not in me.levels()
|
||||||
return false if _.isArray(ac.showIfUnlocked) and _.intersection(ac.showIfUnlocked, me.levels()).length < 0
|
return false if _.isArray(ac.showIfUnlocked) and _.intersection(ac.showIfUnlocked, me.levels()).length <= 0
|
||||||
ac.name = utils.i18n ac, 'name'
|
ac.name = utils.i18n ac, 'name'
|
||||||
styles = []
|
styles = []
|
||||||
styles.push "color: #{ac.color}" if ac.color
|
styles.push "color: #{ac.color}" if ac.color
|
||||||
|
@ -231,7 +231,7 @@ module.exports = class CampaignView extends RootView
|
||||||
if _.isString(ac.showIfUnlocked)
|
if _.isString(ac.showIfUnlocked)
|
||||||
_.find(@campaigns.models, id: acID)?.locked = false if ac.showIfUnlocked in me.levels()
|
_.find(@campaigns.models, id: acID)?.locked = false if ac.showIfUnlocked in me.levels()
|
||||||
else if _.isArray(ac.showIfUnlocked)
|
else if _.isArray(ac.showIfUnlocked)
|
||||||
_.find(@campaigns.models, id: acID)?.locked = false if _.intersection(ac.showIfUnlocked, me.levels()).length
|
_.find(@campaigns.models, id: acID)?.locked = false if _.intersection(ac.showIfUnlocked, me.levels()).length > 0
|
||||||
|
|
||||||
context
|
context
|
||||||
|
|
||||||
|
@ -278,9 +278,11 @@ module.exports = class CampaignView extends RootView
|
||||||
return me.getCampaignAdsGroup() is 'leaderboard-ads'
|
return me.getCampaignAdsGroup() is 'leaderboard-ads'
|
||||||
false
|
false
|
||||||
|
|
||||||
annotateLevel: (level) ->
|
annotateLevels: (orderedLevels) ->
|
||||||
|
previousIncompletePracticeLevel = false # Lock owned levels if there's a earlier incomplete practice level to play
|
||||||
|
for level in orderedLevels
|
||||||
level.position ?= { x: 10, y: 10 }
|
level.position ?= { x: 10, y: 10 }
|
||||||
level.locked = not me.ownsLevel level.original
|
level.locked = not me.ownsLevel(level.original) or previousIncompletePracticeLevel
|
||||||
level.locked = true if level.slug is 'kithgard-mastery' and @calculateExperienceScore() is 0
|
level.locked = true if level.slug is 'kithgard-mastery' and @calculateExperienceScore() is 0
|
||||||
level.locked = false if @levelStatusMap[level.slug] in ['started', 'complete']
|
level.locked = false if @levelStatusMap[level.slug] in ['started', 'complete']
|
||||||
level.locked = false if @editorMode
|
level.locked = false if @editorMode
|
||||||
|
@ -290,8 +292,8 @@ module.exports = class CampaignView extends RootView
|
||||||
level.disabled = true if level.adminOnly and @levelStatusMap[level.slug] not in ['started', 'complete']
|
level.disabled = true if level.adminOnly and @levelStatusMap[level.slug] not in ['started', 'complete']
|
||||||
level.disabled = false if me.isInGodMode()
|
level.disabled = false if me.isInGodMode()
|
||||||
level.color = 'rgb(255, 80, 60)'
|
level.color = 'rgb(255, 80, 60)'
|
||||||
if level.requiresSubscription
|
level.color = 'rgb(80, 130, 200)' if level.requiresSubscription
|
||||||
level.color = 'rgb(80, 130, 200)'
|
level.color = 'rgb(200, 80, 200)' if level.adventurer
|
||||||
if unlocksHero = _.find(level.rewards, 'hero')?.hero
|
if unlocksHero = _.find(level.rewards, 'hero')?.hero
|
||||||
level.unlocksHero = unlocksHero
|
level.unlocksHero = unlocksHero
|
||||||
if level.unlocksHero
|
if level.unlocksHero
|
||||||
|
@ -309,18 +311,22 @@ module.exports = class CampaignView extends RootView
|
||||||
"""
|
"""
|
||||||
level.color = 'rgb(80, 130, 200)' if problem.solved
|
level.color = 'rgb(80, 130, 200)' if problem.solved
|
||||||
|
|
||||||
|
if level.practice and not level.locked and @levelStatusMap[level.slug] isnt 'complete' and
|
||||||
|
(not level.requiresSubscription or level.adventurer or not @requiresSubscription)
|
||||||
|
previousIncompletePracticeLevel = true
|
||||||
|
|
||||||
level.hidden = level.locked
|
level.hidden = level.locked
|
||||||
if level.concepts?.length
|
if level.concepts?.length
|
||||||
level.displayConcepts = level.concepts
|
level.displayConcepts = level.concepts
|
||||||
maxConcepts = 6
|
maxConcepts = 6
|
||||||
if level.displayConcepts.length > maxConcepts
|
if level.displayConcepts.length > maxConcepts
|
||||||
level.displayConcepts = level.displayConcepts.slice -maxConcepts
|
level.displayConcepts = level.displayConcepts.slice -maxConcepts
|
||||||
level
|
|
||||||
|
|
||||||
countLevels: (levels) ->
|
countLevels: (levels) ->
|
||||||
count = total: 0, completed: 0
|
count = total: 0, completed: 0
|
||||||
for level, levelIndex in levels
|
for level, levelIndex in levels
|
||||||
@annotateLevel level unless level.locked? # Annotate if we haven't already.
|
continue if level.practice
|
||||||
|
@annotateLevels(levels) unless level.locked? # Annotate if we haven't already.
|
||||||
unless level.disabled
|
unless level.disabled
|
||||||
unlockedInSameCampaign = levelIndex < 5 # First few are always counted (probably unlocked in previous campaign)
|
unlockedInSameCampaign = levelIndex < 5 # First few are always counted (probably unlocked in previous campaign)
|
||||||
for otherLevel in levels when not unlockedInSameCampaign and otherLevel isnt level
|
for otherLevel in levels when not unlockedInSameCampaign and otherLevel isnt level
|
||||||
|
@ -334,34 +340,42 @@ module.exports = class CampaignView extends RootView
|
||||||
leaderboardModal = new LeaderboardModal supermodel: @supermodel, levelSlug: levelSlug
|
leaderboardModal = new LeaderboardModal supermodel: @supermodel, levelSlug: levelSlug
|
||||||
@openModalView leaderboardModal
|
@openModalView leaderboardModal
|
||||||
|
|
||||||
determineNextLevel: (levels) ->
|
determineNextLevel: (orderedLevels) ->
|
||||||
foundNext = false
|
|
||||||
dontPointTo = ['lost-viking', 'kithgard-mastery'] # Challenge levels we don't want most players bashing heads against
|
dontPointTo = ['lost-viking', 'kithgard-mastery'] # Challenge levels we don't want most players bashing heads against
|
||||||
subscriptionPrompts = [{slug: 'boom-and-bust', unless: 'defense-of-plainswood'}]
|
subscriptionPrompts = [{slug: 'boom-and-bust', unless: 'defense-of-plainswood'}]
|
||||||
for level in levels
|
|
||||||
# Iterate through all levels in order and look to find the first unlocked one that meets all our criteria for being pointed out as the next level.
|
findNextLevel = (nextLevels, practiceOnly) =>
|
||||||
level.nextLevels = (reward.level for reward in level.rewards ? [] when reward.level)
|
for nextLevelOriginal in nextLevels
|
||||||
unless foundNext
|
nextLevel = _.find orderedLevels, original: nextLevelOriginal
|
||||||
for nextLevelOriginal in level.nextLevels
|
continue if not nextLevel or nextLevel.locked
|
||||||
nextLevel = _.find levels, original: nextLevelOriginal
|
continue if practiceOnly and not nextLevel.practice
|
||||||
|
|
||||||
# If it's a challenge level, we efficiently determine whether we actually do want to point it out.
|
# If it's a challenge level, we efficiently determine whether we actually do want to point it out.
|
||||||
if nextLevel and nextLevel.slug is 'kithgard-mastery' and not nextLevel.locked and not @levelStatusMap[nextLevel.slug] and @calculateExperienceScore() >= 3
|
if nextLevel.slug is 'kithgard-mastery' and not @levelStatusMap[nextLevel.slug] and @calculateExperienceScore() >= 3
|
||||||
unless (timesPointedOut = storage.load("pointed-out-#{nextLevel.slug}") or 0) > 3
|
unless (timesPointedOut = storage.load("pointed-out-#{nextLevel.slug}") or 0) > 3
|
||||||
# We may determineNextLevel more than once per render, so we can't just do this once. But we do give up after a couple highlights.
|
# We may determineNextLevel more than once per render, so we can't just do this once. But we do give up after a couple highlights.
|
||||||
dontPointTo = _.without dontPointTo, nextLevel.slug
|
dontPointTo = _.without dontPointTo, nextLevel.slug
|
||||||
storage.save "pointed-out-#{nextLevel.slug}", timesPointedOut + 1
|
storage.save "pointed-out-#{nextLevel.slug}", timesPointedOut + 1
|
||||||
|
|
||||||
# Should we point this level out?
|
# Should we point this level out?
|
||||||
if nextLevel and not nextLevel.locked and not nextLevel.disabled and @levelStatusMap[nextLevel.slug] isnt 'complete' and nextLevel.slug not in dontPointTo and not nextLevel.replayable and (
|
if not nextLevel.disabled and @levelStatusMap[nextLevel.slug] isnt 'complete' and nextLevel.slug not in dontPointTo and
|
||||||
me.isPremium() or not nextLevel.requiresSubscription or
|
not nextLevel.replayable and (
|
||||||
|
me.isPremium() or not nextLevel.requiresSubscription or nextLevel.adventurer or
|
||||||
_.any(subscriptionPrompts, (prompt) => nextLevel.slug is prompt.slug and not @levelStatusMap[prompt.unless])
|
_.any(subscriptionPrompts, (prompt) => nextLevel.slug is prompt.slug and not @levelStatusMap[prompt.unless])
|
||||||
)
|
)
|
||||||
nextLevel.next = true
|
nextLevel.next = true
|
||||||
foundNext = true
|
return true
|
||||||
break
|
false
|
||||||
if not foundNext and levels[0] and not levels[0].locked and @levelStatusMap[levels[0].slug] isnt 'complete'
|
|
||||||
levels[0].next = true
|
foundNext = false
|
||||||
|
for level in orderedLevels
|
||||||
|
# Iterate through all levels in order and look to find the first unlocked one that meets all our criteria for being pointed out as the next level.
|
||||||
|
level.nextLevels = (reward.level for reward in level.rewards ? [] when reward.level)
|
||||||
|
break if foundNext = findNextLevel(level.nextLevels, true) # Check practice levels first
|
||||||
|
break if foundNext = findNextLevel(level.nextLevels, false)
|
||||||
|
|
||||||
|
if not foundNext and orderedLevels[0] and not orderedLevels[0].locked and @levelStatusMap[orderedLevels[0].slug] isnt 'complete'
|
||||||
|
orderedLevels[0].next = true
|
||||||
|
|
||||||
calculateExperienceScore: ->
|
calculateExperienceScore: ->
|
||||||
adultPoint = me.get('ageRange') in ['18-24', '25-34', '35-44', '45-100'] # They have to have answered the poll for this, likely after Shadow Guard.
|
adultPoint = me.get('ageRange') in ['18-24', '25-34', '35-44', '45-100'] # They have to have answered the poll for this, likely after Shadow Guard.
|
||||||
|
@ -409,6 +423,7 @@ module.exports = class CampaignView extends RootView
|
||||||
@particleMan.removeEmitters()
|
@particleMan.removeEmitters()
|
||||||
@particleMan.attach @$el.find('.map')
|
@particleMan.attach @$el.find('.map')
|
||||||
for level in @campaign.renderedLevels ? {}
|
for level in @campaign.renderedLevels ? {}
|
||||||
|
continue if level.practice
|
||||||
terrain = @terrain.replace('-branching-test', '').replace(/(campaign-)?(game|web)-dev-\d/, 'forest').replace('intro', 'dungeon')
|
terrain = @terrain.replace('-branching-test', '').replace(/(campaign-)?(game|web)-dev-\d/, 'forest').replace('intro', 'dungeon')
|
||||||
particleKey = ['level', terrain]
|
particleKey = ['level', terrain]
|
||||||
particleKey.push level.type if level.type and not (level.type in ['hero', 'course']) # Would use isType, but it's not a Level model
|
particleKey.push level.type if level.type and not (level.type in ['hero', 'course']) # Would use isType, but it's not a Level model
|
||||||
|
|
|
@ -321,14 +321,12 @@ module.exports = class Autocomplete
|
||||||
attackEntry.content = attackEntry.content.replace '${1:enemy}', '"${1:Enemy Name}"'
|
attackEntry.content = attackEntry.content.replace '${1:enemy}', '"${1:Enemy Name}"'
|
||||||
snippetEntries.push attackEntry
|
snippetEntries.push attackEntry
|
||||||
|
|
||||||
# Add copied hero. entries for most important ones that start with hero.
|
# Update 'hero.' and 'game.' entries to include their prefixes
|
||||||
sortedEntries = _.sortBy snippetEntries, (entry) -> -1 * parseInt(entry.importance ? 0)
|
for entry in snippetEntries
|
||||||
for entry in sortedEntries
|
if entry.content?.indexOf('hero.') is 0 and entry.name?.indexOf('hero.') < 0
|
||||||
if entry.content?.indexOf('hero.') is 0
|
entry.name = "hero.#{entry.name}"
|
||||||
newEntry = _.cloneDeep(entry)
|
else if entry.content?.indexOf('game.') is 0 and entry.name?.indexOf('game.') < 0
|
||||||
entry.name = "hero.#{newEntry.name}"
|
entry.name = "game.#{entry.name}"
|
||||||
snippetEntries.push(newEntry)
|
|
||||||
break if snippetEntries.length - sortedEntries.length >= 10
|
|
||||||
|
|
||||||
if haveFindNearest and not haveFindNearestEnemy
|
if haveFindNearest and not haveFindNearestEnemy
|
||||||
spellView.translateFindNearest()
|
spellView.translateFindNearest()
|
||||||
|
|
|
@ -136,7 +136,7 @@ module.exports = (SnippetManager, autoLineEndings) ->
|
||||||
beginningOfLine = session.getLine(pos.row).substring(0,pos.column - prefix.length)
|
beginningOfLine = session.getLine(pos.row).substring(0,pos.column - prefix.length)
|
||||||
|
|
||||||
unless (fullPrefixParts.length < 3 and /^(hero|self|this|@)$/.test(fullPrefixParts[0]) ) or /^\s*$/.test(beginningOfLine)
|
unless (fullPrefixParts.length < 3 and /^(hero|self|this|@)$/.test(fullPrefixParts[0]) ) or /^\s*$/.test(beginningOfLine)
|
||||||
console.log "Bailing", fullPrefixParts, '|', prefix, '|', beginningOfLine, '|', pos.column - prefix.length
|
# console.log "DEBUG: autocomplete bailing", fullPrefixParts, '|', prefix, '|', beginningOfLine, '|', pos.column - prefix.length
|
||||||
@completions = completions
|
@completions = completions
|
||||||
return callback null, completions
|
return callback null, completions
|
||||||
|
|
||||||
|
@ -195,10 +195,10 @@ getFullIdentifier = (doc, pos) ->
|
||||||
scrubSnippet = (snippet, caption, line, input, pos, lang, autoLineEndings, captureReturn) ->
|
scrubSnippet = (snippet, caption, line, input, pos, lang, autoLineEndings, captureReturn) ->
|
||||||
# console.log "Snippets snippet=#{snippet} caption=#{caption} line=#{line} input=#{input} pos.column=#{pos.column} lang=#{lang}"
|
# console.log "Snippets snippet=#{snippet} caption=#{caption} line=#{line} input=#{input} pos.column=#{pos.column} lang=#{lang}"
|
||||||
fuzzScore = 0.1
|
fuzzScore = 0.1
|
||||||
|
snippetLineBreaks = (snippet.match(lineBreak) || []).length
|
||||||
# input will be replaced by snippet
|
# input will be replaced by snippet
|
||||||
# trim snippet prefix and suffix if already in the document (line)
|
# trim snippet prefix and suffix if already in the document (line)
|
||||||
if prefixStart = snippet.toLowerCase().indexOf(input.toLowerCase()) > -1
|
if prefixStart = snippet.toLowerCase().indexOf(input.toLowerCase()) > -1
|
||||||
snippetLines = (snippet.match(lineBreak) || []).length
|
|
||||||
captionStart = snippet.indexOf caption
|
captionStart = snippet.indexOf caption
|
||||||
|
|
||||||
# Calculate snippet prefixes and suffixes. E.g. full snippet might be: "self." + "moveLeft" + "()"
|
# Calculate snippet prefixes and suffixes. E.g. full snippet might be: "self." + "moveLeft" + "()"
|
||||||
|
@ -243,14 +243,18 @@ scrubSnippet = (snippet, caption, line, input, pos, lang, autoLineEndings, captu
|
||||||
# console.log 'Snippets atLineEnd', pos.column, lineSuffix.length, line.slice(pos.column + lineSuffix.length), line
|
# console.log 'Snippets atLineEnd', pos.column, lineSuffix.length, line.slice(pos.column + lineSuffix.length), line
|
||||||
toLinePrefix = line.substring 0, linePrefixIndex
|
toLinePrefix = line.substring 0, linePrefixIndex
|
||||||
if linePrefixIndex < 0 or linePrefixIndex >= 0 and not /[\(\)]/.test(toLinePrefix) and not /^[ \t]*(?:if\b|elif\b)/.test(toLinePrefix)
|
if linePrefixIndex < 0 or linePrefixIndex >= 0 and not /[\(\)]/.test(toLinePrefix) and not /^[ \t]*(?:if\b|elif\b)/.test(toLinePrefix)
|
||||||
snippet += autoLineEndings[lang] if snippetLines is 0 and autoLineEndings[lang]
|
snippet += autoLineEndings[lang] if snippetLineBreaks is 0 and autoLineEndings[lang]
|
||||||
snippet += "\n" if snippetLines is 0 and not /\$\{/.test(snippet)
|
snippet += "\n" if snippetLineBreaks is 0 and not /\$\{/.test(snippet)
|
||||||
|
|
||||||
if captureReturn and /^\s*$/.test(toLinePrefix)
|
if captureReturn and /^\s*$/.test(toLinePrefix)
|
||||||
snippet = captureReturn + linePrefix + snippet
|
snippet = captureReturn + linePrefix + snippet
|
||||||
|
|
||||||
# console.log "Snippets snippetPrefix=#{snippetPrefix} linePrefix=#{linePrefix} snippetSuffix=#{snippetSuffix} lineSuffix=#{lineSuffix} snippet=#{snippet} score=#{fuzzScore}"
|
# console.log "Snippets snippetPrefix=#{snippetPrefix} linePrefix=#{linePrefix} snippetSuffix=#{snippetSuffix} lineSuffix=#{lineSuffix} snippet=#{snippet} score=#{fuzzScore}"
|
||||||
else
|
else
|
||||||
|
# Append automatic line ending and newline for simple scenario
|
||||||
|
if line.trim() is input
|
||||||
|
snippet += autoLineEndings[lang] if snippetLineBreaks is 0 and autoLineEndings[lang]
|
||||||
|
snippet += "\n" if snippetLineBreaks is 0 and not /\$\{/.test(snippet)
|
||||||
fuzzScore += score snippet, input
|
fuzzScore += score snippet, input
|
||||||
|
|
||||||
startsWith = (string, searchString, position) ->
|
startsWith = (string, searchString, position) ->
|
||||||
|
|
|
@ -23,6 +23,8 @@ let zpMinActivityDate = new Date();
|
||||||
zpMinActivityDate.setUTCDate(zpMinActivityDate.getUTCDate() - 30);
|
zpMinActivityDate.setUTCDate(zpMinActivityDate.getUTCDate() - 30);
|
||||||
zpMinActivityDate = zpMinActivityDate.toISOString().substring(0, 10);
|
zpMinActivityDate = zpMinActivityDate.toISOString().substring(0, 10);
|
||||||
|
|
||||||
|
const closeParallelLimit = 100;
|
||||||
|
|
||||||
getZPContacts((err, emailContactMap) => {
|
getZPContacts((err, emailContactMap) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
@ -33,7 +35,7 @@ getZPContacts((err, emailContactMap) => {
|
||||||
const contact = emailContactMap[email];
|
const contact = emailContactMap[email];
|
||||||
tasks.push(createUpsertCloseLeadFn(contact));
|
tasks.push(createUpsertCloseLeadFn(contact));
|
||||||
}
|
}
|
||||||
async.parallel(tasks, (err, results) => {
|
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
|
||||||
if (err) console.log(err);
|
if (err) console.log(err);
|
||||||
log("Script runtime: " + (new Date() - scriptStartTime));
|
log("Script runtime: " + (new Date() - scriptStartTime));
|
||||||
});
|
});
|
||||||
|
@ -127,26 +129,39 @@ function updateCloseLead(zpContact, existingLead, done) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function createUpsertCloseLeadFn(zpContact) {
|
function createUpsertCloseLeadFn(zpContact) {
|
||||||
|
// New contact lead matching algorithm:
|
||||||
|
// 1. New contact email exists
|
||||||
|
// 2. New contact NCES school id exists
|
||||||
|
// 3. New contact NCES district id and no NCES school id
|
||||||
|
// 4. New contact school name and no NCES data
|
||||||
|
// 5. New contact district name and no NCES data
|
||||||
return (done) => {
|
return (done) => {
|
||||||
// console.log(`DEBUG: createUpsertCloseLeadFn ${zpContact.organization} ${zpContact.email}`);
|
// console.log(`DEBUG: createUpsertCloseLeadFn ${zpContact.organization} ${zpContact.email}`);
|
||||||
const query = `email:${zpContact.email}`;
|
let query = `email:${zpContact.email}`;
|
||||||
const url = `https://${closeIoApiKey}:X@app.close.io/api/v1/lead/?query=${encodeURIComponent(query)}`;
|
let url = `https://${closeIoApiKey}:X@app.close.io/api/v1/lead/?query=${encodeURIComponent(query)}`;
|
||||||
request.get(url, (error, response, body) => {
|
request.get(url, (error, response, body) => {
|
||||||
if (error) return done(error);
|
if (error) return done(error);
|
||||||
const data = JSON.parse(body);
|
const data = JSON.parse(body);
|
||||||
if (data.total_results != 0) return done();
|
if (data.total_results != 0) return done();
|
||||||
const query = `name:${zpContact.organization}`;
|
|
||||||
const url = `https://${closeIoApiKey}:X@app.close.io/api/v1/lead/?query=${encodeURIComponent(query)}`;
|
query = `name:${zpContact.organization}`;
|
||||||
|
if (zpContact.nces_school_id) {
|
||||||
|
query = `custom.demo_nces_id:"${zpContact.nces_school_id}"`;
|
||||||
|
}
|
||||||
|
else if (zpContact.nces_district_id) {
|
||||||
|
query = `custom.demo_nces_district_id:"${zpContact.nces_district_id}" custom.demo_nces_id:""`;
|
||||||
|
}
|
||||||
|
url = `https://${closeIoApiKey}:X@app.close.io/api/v1/lead/?query=${encodeURIComponent(query)}`;
|
||||||
request.get(url, (error, response, body) => {
|
request.get(url, (error, response, body) => {
|
||||||
if (error) return done(error);
|
if (error) return done(error);
|
||||||
const data = JSON.parse(body);
|
const data = JSON.parse(body);
|
||||||
if (data.total_results === 0) {
|
if (data.total_results === 0) {
|
||||||
console.log(`DEBUG: Creating lead for ${zpContact.organization} ${zpContact.email}`);
|
console.log(`DEBUG: Creating lead for ${zpContact.organization} ${zpContact.email} nces_district_id=${zpContact.nces_district_id} nces_school_id=${zpContact.nces_school_id}`);
|
||||||
return createCloseLead(zpContact, done);
|
return createCloseLead(zpContact, done);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const existingLead = data.data[0];
|
const existingLead = data.data[0];
|
||||||
console.log(`DEBUG: Adding ${zpContact.organization} ${zpContact.email} to ${existingLead.id}`);
|
console.log(`DEBUG: Adding to ${existingLead.id} ${zpContact.organization} ${zpContact.email} nces_district_id=${zpContact.nces_district_id} nces_school_id=${zpContact.nces_school_id}`);
|
||||||
return updateCloseLead(zpContact, existingLead, done);
|
return updateCloseLead(zpContact, existingLead, done);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,7 +14,7 @@ if (process.argv.length !== 10) {
|
||||||
// TODO: Reduce response data via _fields param
|
// TODO: Reduce response data via _fields param
|
||||||
// TODO: Assumes 1:1 contact:email relationship (Close.io supports multiple emails for a single contact)
|
// TODO: Assumes 1:1 contact:email relationship (Close.io supports multiple emails for a single contact)
|
||||||
// TODO: Cleanup country/status lookup code
|
// TODO: Cleanup country/status lookup code
|
||||||
// TODO: parallelize update leads
|
// TODO: Handle trial requests as individual contacts to be imported, instead of batching them into leads immediately via CocoLead objects
|
||||||
|
|
||||||
// Save as custom fields instead of user-specific lead notes (also saving nces_ props)
|
// Save as custom fields instead of user-specific lead notes (also saving nces_ props)
|
||||||
const commonTrialProperties = ['organization', 'district', 'city', 'state', 'country'];
|
const commonTrialProperties = ['organization', 'district', 'city', 'state', 'country'];
|
||||||
|
@ -59,6 +59,8 @@ const usSchoolStatuses = ['Auto Attempt 1', 'New US Schools Auto Attempt 1', 'Ne
|
||||||
|
|
||||||
const emailDelayMinutes = 27;
|
const emailDelayMinutes = 27;
|
||||||
|
|
||||||
|
const closeParallelLimit = 100;
|
||||||
|
|
||||||
const scriptStartTime = new Date();
|
const scriptStartTime = new Date();
|
||||||
const closeIoApiKey = process.argv[2];
|
const closeIoApiKey = process.argv[2];
|
||||||
// Automatic mails sent as API owners, first key assumed to be primary and gets 50% of the leads
|
// Automatic mails sent as API owners, first key assumed to be primary and gets 50% of the leads
|
||||||
|
@ -677,7 +679,7 @@ function updateExistingLead(lead, existingLead, userApiKeyMap, done) {
|
||||||
newContact.lead_id = existingLead.id;
|
newContact.lead_id = existingLead.id;
|
||||||
tasks.push(createAddContactFn(newContact, lead, existingLead, userApiKeyMap));
|
tasks.push(createAddContactFn(newContact, lead, existingLead, userApiKeyMap));
|
||||||
}
|
}
|
||||||
async.parallel(tasks, (err, results) => {
|
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
|
|
||||||
// Add notes
|
// Add notes
|
||||||
|
@ -690,7 +692,7 @@ function updateExistingLead(lead, existingLead, userApiKeyMap, done) {
|
||||||
for (const newNote of newNotes) {
|
for (const newNote of newNotes) {
|
||||||
tasks.push(createAddNoteFn(existingLead.id, newNote));
|
tasks.push(createAddNoteFn(existingLead.id, newNote));
|
||||||
}
|
}
|
||||||
async.parallel(tasks, (err, results) => {
|
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
|
||||||
return done(err);
|
return done(err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -721,7 +723,7 @@ function saveNewLead(lead, done) {
|
||||||
for (const newNote of newNotes) {
|
for (const newNote of newNotes) {
|
||||||
tasks.push(createAddNoteFn(existingLead.id, newNote));
|
tasks.push(createAddNoteFn(existingLead.id, newNote));
|
||||||
}
|
}
|
||||||
async.parallel(tasks, (err, results) => {
|
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
|
|
||||||
// Send emails to new contacts
|
// Send emails to new contacts
|
||||||
|
@ -733,7 +735,7 @@ function saveNewLead(lead, done) {
|
||||||
tasks.push(createSendEmailFn(email.email, existingLead.id, contact.id, emailTemplate, postData.status));
|
tasks.push(createSendEmailFn(email.email, existingLead.id, contact.id, emailTemplate, postData.status));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async.parallel(tasks, (err, results) => {
|
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
|
||||||
return done(err);
|
return done(err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -767,15 +769,15 @@ function createFindExistingLeadFn(email, name, existingLeads) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function createUpdateLeadFn(lead, existingLeads, userApiKeyMap) {
|
function createUpdateLeadFn(lead, existingLeads, userApiKeyMap) {
|
||||||
|
// New contact lead matching algorithm:
|
||||||
|
// 1. New contact email exists
|
||||||
|
// 2. New contact NCES school id exists
|
||||||
|
// 3. New contact NCES district id and no NCES school id
|
||||||
|
// 4. New contact school name and no NCES data
|
||||||
|
// 5. New contact district name and no NCES data
|
||||||
return (done) => {
|
return (done) => {
|
||||||
// console.log('DEBUG: updateLead', lead.name);
|
// console.log('DEBUG: updateLead', lead.name);
|
||||||
const query = `name:"${lead.name}"`;
|
|
||||||
const url = `https://${closeIoApiKey}:X@app.close.io/api/v1/lead/?query=${encodeURIComponent(query)}`;
|
|
||||||
request.get(url, (error, response, body) => {
|
|
||||||
if (error) return done(error);
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(body);
|
|
||||||
if (data.total_results === 0) {
|
|
||||||
if (existingLeads[lead.name.toLowerCase()]) {
|
if (existingLeads[lead.name.toLowerCase()]) {
|
||||||
if (existingLeads[lead.name.toLowerCase()].length === 1) {
|
if (existingLeads[lead.name.toLowerCase()].length === 1) {
|
||||||
// console.log(`DEBUG: Using lead from email lookup: ${lead.name}`);
|
// console.log(`DEBUG: Using lead from email lookup: ${lead.name}`);
|
||||||
|
@ -784,17 +786,44 @@ function createUpdateLeadFn(lead, existingLeads, userApiKeyMap) {
|
||||||
console.error(`ERROR: ${existingLeads[lead.name.toLowerCase()].length} email leads found for ${lead.name}`);
|
console.error(`ERROR: ${existingLeads[lead.name.toLowerCase()].length} email leads found for ${lead.name}`);
|
||||||
return done();
|
return done();
|
||||||
}
|
}
|
||||||
return saveNewLead(lead, done);
|
|
||||||
|
let nces_district_id;
|
||||||
|
let nces_school_id;
|
||||||
|
for (const trial of lead.trialRequests) {
|
||||||
|
if (!trial.properties) continue;
|
||||||
|
if (trial.properties.nces_district_id) {
|
||||||
|
nces_district_id = trial.properties.nces_district_id;
|
||||||
|
if (trial.properties.nces_id) {
|
||||||
|
nces_district_id = trial.properties.nces_district_id;
|
||||||
|
nces_school_id = trial.properties.nces_id;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// console.log(`DEBUG: updateLead district ${nces_district_id} school ${nces_school_id}`);
|
||||||
|
|
||||||
|
let query = `name:"${lead.name}"`;
|
||||||
|
if (nces_school_id) {
|
||||||
|
query = `custom.demo_nces_id:"${nces_school_id}"`;
|
||||||
|
}
|
||||||
|
else if (nces_district_id) {
|
||||||
|
query = `custom.demo_nces_district_id:"${nces_district_id}" custom.demo_nces_id:""`;
|
||||||
|
}
|
||||||
|
const url = `https://${closeIoApiKey}:X@app.close.io/api/v1/lead/?query=${encodeURIComponent(query)}`;
|
||||||
|
request.get(url, (error, response, body) => {
|
||||||
|
if (error) return done(error);
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(body);
|
||||||
if (data.total_results > 1) {
|
if (data.total_results > 1) {
|
||||||
console.error(`ERROR: ${data.total_results} leads found for ${lead.name}`);
|
console.error(`ERROR: ${data.total_results} leads found for ${lead.name} nces_district_id=${nces_district_id} nces_school_id=${nces_school_id}`);
|
||||||
return done();
|
return done();
|
||||||
}
|
}
|
||||||
|
if (data.total_results === 1) {
|
||||||
return updateExistingLead(lead, data.data[0], userApiKeyMap, done);
|
return updateExistingLead(lead, data.data[0], userApiKeyMap, done);
|
||||||
|
}
|
||||||
|
return saveNewLead(lead, done);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.log(url);
|
|
||||||
console.log(`ERROR: updateLead ${error}`);
|
console.log(`ERROR: updateLead ${error}`);
|
||||||
// console.log(body);
|
|
||||||
return done();
|
return done();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -939,7 +968,7 @@ function updateLeads(leads, done) {
|
||||||
for (const closeIoMailApiKey of closeIoMailApiKeys) {
|
for (const closeIoMailApiKey of closeIoMailApiKeys) {
|
||||||
tasks.push(createGetUserFn(closeIoMailApiKey.apiKey));
|
tasks.push(createGetUserFn(closeIoMailApiKey.apiKey));
|
||||||
}
|
}
|
||||||
async.parallel(tasks, (err, results) => {
|
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
|
||||||
if (err) console.log(err);
|
if (err) console.log(err);
|
||||||
// Lookup existing leads via email to protect against direct lead name querying later
|
// Lookup existing leads via email to protect against direct lead name querying later
|
||||||
// Querying via lead name is unreliable
|
// Querying via lead name is unreliable
|
||||||
|
@ -951,14 +980,14 @@ function updateLeads(leads, done) {
|
||||||
tasks.push(createFindExistingLeadFn(email.toLowerCase(), name.toLowerCase(), existingLeads));
|
tasks.push(createFindExistingLeadFn(email.toLowerCase(), name.toLowerCase(), existingLeads));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async.parallel(tasks, (err, results) => {
|
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
const tasks = [];
|
const tasks = [];
|
||||||
for (const name in leads) {
|
for (const name in leads) {
|
||||||
if (leadsToSkip.indexOf(name) >= 0) continue;
|
if (leadsToSkip.indexOf(name) >= 0) continue;
|
||||||
tasks.push(createUpdateLeadFn(leads[name], existingLeads, userApiKeyMap));
|
tasks.push(createUpdateLeadFn(leads[name], existingLeads, userApiKeyMap));
|
||||||
}
|
}
|
||||||
async.series(tasks, (err, results) => {
|
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
|
||||||
return done(err);
|
return done(err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -123,10 +123,14 @@ module.exports =
|
||||||
if _.isEmpty(req.body)
|
if _.isEmpty(req.body)
|
||||||
throw new errors.UnprocessableEntity('No input')
|
throw new errors.UnprocessableEntity('No input')
|
||||||
|
|
||||||
props = doc.schema.statics.editableProperties.slice()
|
if not doc.schema.statics.editableProperties
|
||||||
|
console.warn 'No editableProperties set for', doc.constructor.modelName
|
||||||
|
props = (doc.schema.statics.editableProperties or []).slice()
|
||||||
|
|
||||||
if doc.isNew
|
if doc.isNew
|
||||||
props = props.concat doc.schema.statics.postEditableProperties
|
props = props.concat(doc.schema.statics.postEditableProperties or [])
|
||||||
|
if not doc.schema.statics.postEditableProperties
|
||||||
|
console.warn 'No postEditableProperties set for', doc.constructor.modelName
|
||||||
|
|
||||||
if doc.schema.uses_coco_permissions and req.user
|
if doc.schema.uses_coco_permissions and req.user
|
||||||
isOwner = doc.getAccessForUserObjectId(req.user._id) is 'owner'
|
isOwner = doc.getAccessForUserObjectId(req.user._id) is 'owner'
|
||||||
|
|
|
@ -16,64 +16,8 @@ Classroom = require '../models/Classroom'
|
||||||
LevelHandler = class LevelHandler extends Handler
|
LevelHandler = class LevelHandler extends Handler
|
||||||
modelClass: Level
|
modelClass: Level
|
||||||
jsonSchema: require '../../app/schemas/models/level'
|
jsonSchema: require '../../app/schemas/models/level'
|
||||||
editableProperties: [
|
editableProperties: Level.editableProperties
|
||||||
'description'
|
postEditableProperties: Level.postEditableProperties
|
||||||
'documentation'
|
|
||||||
'background'
|
|
||||||
'nextLevel'
|
|
||||||
'scripts'
|
|
||||||
'thangs'
|
|
||||||
'systems'
|
|
||||||
'victory'
|
|
||||||
'name'
|
|
||||||
'i18n'
|
|
||||||
'icon'
|
|
||||||
'goals'
|
|
||||||
'type'
|
|
||||||
'showsGuide'
|
|
||||||
'banner'
|
|
||||||
'employerDescription'
|
|
||||||
'terrain'
|
|
||||||
'i18nCoverage'
|
|
||||||
'loadingTip'
|
|
||||||
'requiresSubscription'
|
|
||||||
'adventurer'
|
|
||||||
'practice'
|
|
||||||
'shareable'
|
|
||||||
'adminOnly'
|
|
||||||
'disableSpaces'
|
|
||||||
'hidesSubmitUntilRun'
|
|
||||||
'hidesPlayButton'
|
|
||||||
'hidesRunShortcut'
|
|
||||||
'hidesHUD'
|
|
||||||
'hidesSay'
|
|
||||||
'hidesCodeToolbar'
|
|
||||||
'hidesRealTimePlayback'
|
|
||||||
'backspaceThrottle'
|
|
||||||
'lockDefaultCode'
|
|
||||||
'moveRightLoopSnippet'
|
|
||||||
'realTimeSpeedFactor'
|
|
||||||
'autocompleteFontSizePx'
|
|
||||||
'requiredCode'
|
|
||||||
'suspectCode'
|
|
||||||
'requiredGear'
|
|
||||||
'restrictedGear'
|
|
||||||
'allowedHeroes'
|
|
||||||
'tasks'
|
|
||||||
'helpVideos'
|
|
||||||
'campaign'
|
|
||||||
'campaignIndex'
|
|
||||||
'replayable'
|
|
||||||
'buildTime'
|
|
||||||
'scoreTypes'
|
|
||||||
'concepts'
|
|
||||||
'picoCTFProblem'
|
|
||||||
'practiceThresholdMinutes',
|
|
||||||
'primerLanguage'
|
|
||||||
'studentPlayInstructions'
|
|
||||||
]
|
|
||||||
|
|
||||||
postEditableProperties: ['name']
|
|
||||||
|
|
||||||
getByRelationship: (req, res, args...) ->
|
getByRelationship: (req, res, args...) ->
|
||||||
return @getSession(req, res, args[0]) if args[1] is 'session'
|
return @getSession(req, res, args[0]) if args[1] is 'session'
|
||||||
|
|
|
@ -9,62 +9,66 @@ mongoose = require 'mongoose'
|
||||||
database = require '../commons/database'
|
database = require '../commons/database'
|
||||||
parse = require '../commons/parse'
|
parse = require '../commons/parse'
|
||||||
|
|
||||||
|
# More info on database versioning: https://github.com/codecombat/codecombat/wiki/Versioning
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
postNewVersion: (Model, options={}) -> wrap (req, res) ->
|
postNewVersion: (Model, options={}) -> wrap (req, res) ->
|
||||||
|
# Find the document which is getting a new version
|
||||||
parent = yield database.getDocFromHandle(req, Model)
|
parent = yield database.getDocFromHandle(req, Model)
|
||||||
if not parent
|
if not parent
|
||||||
throw new errors.NotFound('Parent not found.')
|
throw new errors.NotFound('Parent not found.')
|
||||||
|
|
||||||
# TODO: Figure out a better way to do this
|
# Check permissions
|
||||||
|
# TODO: Figure out an encapsulated way to do this; it's more permissions than versioning
|
||||||
if options.hasPermissionsOrTranslations
|
if options.hasPermissionsOrTranslations
|
||||||
permissions = options.hasPermissionsOrTranslations
|
permissions = options.hasPermissionsOrTranslations
|
||||||
permissions = [permissions] if _.isString(permissions)
|
permissions = [permissions] if _.isString(permissions)
|
||||||
permissions = ['admin'] if not _.isArray(permissions)
|
permissions = ['admin'] if not _.isArray(permissions)
|
||||||
hasPermission = _.any(req.user?.hasPermission(permission) for permission in permissions)
|
hasPermission = _.any(req.user?.hasPermission(permission) for permission in permissions)
|
||||||
|
if Model.schema.uses_coco_permissions and not hasPermission
|
||||||
|
hasPermission = parent.hasPermissionsForMethod(req.user, req.method)
|
||||||
if not (hasPermission or database.isJustFillingTranslations(req, parent))
|
if not (hasPermission or database.isJustFillingTranslations(req, parent))
|
||||||
throw new errors.Forbidden()
|
throw new errors.Forbidden()
|
||||||
|
|
||||||
|
# Create the new version, a clone of the parent with POST data applied
|
||||||
doc = database.initDoc(req, Model)
|
doc = database.initDoc(req, Model)
|
||||||
ATTRIBUTES_NOT_INHERITED = ['_id', 'version', 'created', 'creator']
|
ATTRIBUTES_NOT_INHERITED = ['_id', 'version', 'created', 'creator']
|
||||||
doc.set(_.omit(parent.toObject(), ATTRIBUTES_NOT_INHERITED))
|
doc.set(_.omit(parent.toObject(), ATTRIBUTES_NOT_INHERITED))
|
||||||
|
|
||||||
database.assignBody(req, doc, { unsetMissing: true })
|
database.assignBody(req, doc, { unsetMissing: true })
|
||||||
|
|
||||||
# Get latest version
|
# Get latest (minor or major) version. This may not be the same document (or same major version) as parent.
|
||||||
|
latestSelect = 'version index slug'
|
||||||
major = req.body.version?.major
|
major = req.body.version?.major
|
||||||
original = parent.get('original')
|
original = parent.get('original')
|
||||||
if _.isNumber(major)
|
if _.isNumber(major)
|
||||||
q1 = Model.findOne({original: original, 'version.isLatestMinor': true, 'version.major': major})
|
q1 = Model.findOne({original: original, 'version.isLatestMinor': true, 'version.major': major})
|
||||||
else
|
else
|
||||||
q1 = Model.findOne({original: original, 'version.isLatestMajor': true})
|
q1 = Model.findOne({original: original, 'version.isLatestMajor': true})
|
||||||
q1.select 'version'
|
q1.select latestSelect
|
||||||
latest = yield q1.exec()
|
latest = yield q1.exec()
|
||||||
|
|
||||||
if not latest
|
# Handle the case where no version is marked as latest, since making new
|
||||||
# handle the case where no version is marked as latest, since making new
|
|
||||||
# versions is not atomic
|
# versions is not atomic
|
||||||
|
if not latest
|
||||||
if _.isNumber(major)
|
if _.isNumber(major)
|
||||||
q2 = Model.findOne({original: original, 'version.major': major})
|
q2 = Model.findOne({original: original, 'version.major': major})
|
||||||
q2.sort({'version.minor': -1})
|
q2.sort({'version.minor': -1})
|
||||||
else
|
else
|
||||||
q2 = Model.findOne()
|
q2 = Model.findOne()
|
||||||
q2.sort({'version.major': -1, 'version.minor': -1})
|
q2.sort({'version.major': -1, 'version.minor': -1})
|
||||||
q2.select 'version'
|
q2.select(latestSelect)
|
||||||
latest = yield q2.exec()
|
latest = yield q2.exec()
|
||||||
if not latest
|
if not latest
|
||||||
throw new errors.NotFound('Previous version not found.')
|
throw new errors.NotFound('Previous version not found.')
|
||||||
|
|
||||||
# Transfer latest version
|
# Update the latest version, making it no longer the latest. This includes
|
||||||
major = req.body.version?.major
|
major = req.body.version?.major
|
||||||
version = _.clone(latest.get('version'))
|
version = _.clone(latest.get('version'))
|
||||||
wasLatestMajor = version.isLatestMajor
|
wasLatestMajor = version.isLatestMajor
|
||||||
version.isLatestMajor = false
|
version.isLatestMajor = false
|
||||||
if _.isNumber(major)
|
if _.isNumber(major)
|
||||||
version.isLatestMinor = false
|
version.isLatestMinor = false
|
||||||
|
raw = yield latest.update({$set: {version: version}, $unset: {index: 1, slug: 1}})
|
||||||
conditions = {_id: latest._id}
|
|
||||||
|
|
||||||
raw = yield Model.update(conditions, {version: version, $unset: {index: 1, slug: 1}})
|
|
||||||
if not raw.nModified
|
if not raw.nModified
|
||||||
console.error('Conditions', conditions)
|
console.error('Conditions', conditions)
|
||||||
console.error('Doc', doc)
|
console.error('Doc', doc)
|
||||||
|
@ -89,7 +93,12 @@ module.exports =
|
||||||
|
|
||||||
doc.set('parent', latest._id)
|
doc.set('parent', latest._id)
|
||||||
|
|
||||||
|
try
|
||||||
doc = yield doc.save()
|
doc = yield doc.save()
|
||||||
|
catch e
|
||||||
|
# Revert changes to latest doc made earlier, should set everything back to normal
|
||||||
|
yield latest.update({$set: _.pick(latest.toObject(), 'version', 'index', 'slug')})
|
||||||
|
throw e
|
||||||
|
|
||||||
editPath = req.headers['x-current-path']
|
editPath = req.headers['x-current-path']
|
||||||
docLink = "http://codecombat.com#{editPath}"
|
docLink = "http://codecombat.com#{editPath}"
|
||||||
|
|
|
@ -93,6 +93,7 @@ AchievementSchema.statics.editableProperties = [
|
||||||
'i18nCoverage'
|
'i18nCoverage'
|
||||||
'hidden'
|
'hidden'
|
||||||
]
|
]
|
||||||
|
AchievementSchema.statics.postEditableProperties = []
|
||||||
|
|
||||||
AchievementSchema.statics.jsonSchema = require '../../app/schemas/models/achievement'
|
AchievementSchema.statics.jsonSchema = require '../../app/schemas/models/achievement'
|
||||||
|
|
||||||
|
|
|
@ -56,5 +56,6 @@ CampaignSchema.statics.editableProperties = [
|
||||||
'adjacentCampaigns'
|
'adjacentCampaigns'
|
||||||
'levels'
|
'levels'
|
||||||
]
|
]
|
||||||
|
CampaignSchema.statics.postEditableProperties = []
|
||||||
|
|
||||||
module.exports = mongoose.model('campaign', CampaignSchema)
|
module.exports = mongoose.model('campaign', CampaignSchema)
|
||||||
|
|
|
@ -23,6 +23,7 @@ ClassroomSchema.statics.editableProperties = [
|
||||||
'ageRangeMax'
|
'ageRangeMax'
|
||||||
'archived'
|
'archived'
|
||||||
]
|
]
|
||||||
|
ClassroomSchema.statics.postEditableProperties = []
|
||||||
|
|
||||||
ClassroomSchema.statics.generateNewCode = (done) ->
|
ClassroomSchema.statics.generateNewCode = (done) ->
|
||||||
tryCode = ->
|
tryCode = ->
|
||||||
|
|
|
@ -28,6 +28,7 @@ CodeLogSchema.statics.editableProperties = [
|
||||||
'log'
|
'log'
|
||||||
'created'
|
'created'
|
||||||
]
|
]
|
||||||
|
CodeLogSchema.statics.postEditableProperties = []
|
||||||
|
|
||||||
CodeLogSchema.statics.jsonSchema = require '../../app/schemas/models/codelog.schema'
|
CodeLogSchema.statics.jsonSchema = require '../../app/schemas/models/codelog.schema'
|
||||||
|
|
||||||
|
|
|
@ -46,4 +46,64 @@ LevelSchema.post 'init', (doc) ->
|
||||||
if _.isString(doc.get('nextLevel'))
|
if _.isString(doc.get('nextLevel'))
|
||||||
doc.set('nextLevel', undefined)
|
doc.set('nextLevel', undefined)
|
||||||
|
|
||||||
|
LevelSchema.statics.postEditableProperties = ['name']
|
||||||
|
|
||||||
|
LevelSchema.statics.editableProperties = [
|
||||||
|
'description'
|
||||||
|
'documentation'
|
||||||
|
'background'
|
||||||
|
'nextLevel'
|
||||||
|
'scripts'
|
||||||
|
'thangs'
|
||||||
|
'systems'
|
||||||
|
'victory'
|
||||||
|
'name'
|
||||||
|
'i18n'
|
||||||
|
'icon'
|
||||||
|
'goals'
|
||||||
|
'type'
|
||||||
|
'showsGuide'
|
||||||
|
'banner'
|
||||||
|
'employerDescription'
|
||||||
|
'terrain'
|
||||||
|
'i18nCoverage'
|
||||||
|
'loadingTip'
|
||||||
|
'requiresSubscription'
|
||||||
|
'adventurer'
|
||||||
|
'practice'
|
||||||
|
'shareable'
|
||||||
|
'adminOnly'
|
||||||
|
'disableSpaces'
|
||||||
|
'hidesSubmitUntilRun'
|
||||||
|
'hidesPlayButton'
|
||||||
|
'hidesRunShortcut'
|
||||||
|
'hidesHUD'
|
||||||
|
'hidesSay'
|
||||||
|
'hidesCodeToolbar'
|
||||||
|
'hidesRealTimePlayback'
|
||||||
|
'backspaceThrottle'
|
||||||
|
'lockDefaultCode'
|
||||||
|
'moveRightLoopSnippet'
|
||||||
|
'realTimeSpeedFactor'
|
||||||
|
'autocompleteFontSizePx'
|
||||||
|
'requiredCode'
|
||||||
|
'suspectCode'
|
||||||
|
'requiredGear'
|
||||||
|
'restrictedGear'
|
||||||
|
'allowedHeroes'
|
||||||
|
'tasks'
|
||||||
|
'helpVideos'
|
||||||
|
'campaign'
|
||||||
|
'campaignIndex'
|
||||||
|
'replayable'
|
||||||
|
'buildTime'
|
||||||
|
'scoreTypes'
|
||||||
|
'concepts'
|
||||||
|
'picoCTFProblem'
|
||||||
|
'practiceThresholdMinutes',
|
||||||
|
'primerLanguage'
|
||||||
|
'studentPlayInstructions'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
module.exports = Level = mongoose.model('level', LevelSchema)
|
module.exports = Level = mongoose.model('level', LevelSchema)
|
||||||
|
|
|
@ -97,6 +97,10 @@ module.exports.setup = (app) ->
|
||||||
app.get('/db/course_instance/:handle/classroom', mw.auth.checkLoggedIn(), mw.courseInstances.fetchClassroom)
|
app.get('/db/course_instance/:handle/classroom', mw.auth.checkLoggedIn(), mw.courseInstances.fetchClassroom)
|
||||||
app.get('/db/course_instance/:handle/course', mw.auth.checkLoggedIn(), mw.courseInstances.fetchCourse)
|
app.get('/db/course_instance/:handle/course', mw.auth.checkLoggedIn(), mw.courseInstances.fetchCourse)
|
||||||
|
|
||||||
|
Level = require '../models/Level'
|
||||||
|
app.post('/db/level/:handle', mw.auth.checkLoggedIn(), mw.versions.postNewVersion(Level, { hasPermissionsOrTranslations: 'artisan' })) # TODO: add /new-version to route like Article has
|
||||||
|
app.get('/db/level/:handle/session', mw.auth.checkHasUser(), mw.levels.upsertSession)
|
||||||
|
|
||||||
app.put('/db/user/:handle', mw.users.resetEmailVerifiedFlag)
|
app.put('/db/user/:handle', mw.users.resetEmailVerifiedFlag)
|
||||||
app.delete('/db/user/:handle', mw.users.removeFromClassrooms)
|
app.delete('/db/user/:handle', mw.users.removeFromClassrooms)
|
||||||
app.get('/db/user', mw.users.fetchByGPlusID, mw.users.fetchByFacebookID)
|
app.get('/db/user', mw.users.fetchByGPlusID, mw.users.fetchByFacebookID)
|
||||||
|
@ -104,7 +108,6 @@ module.exports.setup = (app) ->
|
||||||
app.put('/db/user/-/remain-teacher', mw.users.remainTeacher)
|
app.put('/db/user/-/remain-teacher', mw.users.remainTeacher)
|
||||||
app.post('/db/user/:userID/request-verify-email', mw.users.sendVerificationEmail)
|
app.post('/db/user/:userID/request-verify-email', mw.users.sendVerificationEmail)
|
||||||
app.post('/db/user/:userID/verify/:verificationCode', mw.users.verifyEmailAddress) # TODO: Finalize URL scheme
|
app.post('/db/user/:userID/verify/:verificationCode', mw.users.verifyEmailAddress) # TODO: Finalize URL scheme
|
||||||
app.get('/db/level/:handle/session', mw.auth.checkHasUser(), mw.levels.upsertSession)
|
|
||||||
app.get('/db/user/-/students', mw.auth.checkHasPermission(['admin']), mw.users.getStudents)
|
app.get('/db/user/-/students', mw.auth.checkHasPermission(['admin']), mw.users.getStudents)
|
||||||
app.get('/db/user/-/teachers', mw.auth.checkHasPermission(['admin']), mw.users.getTeachers)
|
app.get('/db/user/-/teachers', mw.auth.checkHasPermission(['admin']), mw.users.getTeachers)
|
||||||
app.post('/db/user/:handle/signup-with-facebook', mw.users.signupWithFacebook)
|
app.post('/db/user/:handle/signup-with-facebook', mw.users.signupWithFacebook)
|
||||||
|
|
|
@ -52,9 +52,40 @@ describe 'POST /db/level/:handle', ->
|
||||||
|
|
||||||
url = getURL("/db/level/#{@level.id}")
|
url = getURL("/db/level/#{@level.id}")
|
||||||
[res, body] = yield request.postAsync({url: url, json: levelJSON})
|
[res, body] = yield request.postAsync({url: url, json: levelJSON})
|
||||||
expect(res.statusCode).toBe(200)
|
expect(res.statusCode).toBe(201)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
it 'does not break the target level if a name change would conflict with another level', utils.wrap (done) ->
|
||||||
|
yield utils.clearModels([Level, User])
|
||||||
|
user = yield utils.initUser()
|
||||||
|
yield utils.loginUser(user)
|
||||||
|
yield utils.makeLevel({name: 'Taken Name'})
|
||||||
|
level = yield utils.makeLevel({name: 'Another Level'})
|
||||||
|
json = _.extend({}, level.toObject(), {name: 'Taken Name'})
|
||||||
|
[res, body] = yield request.postAsync({url: utils.getURL("/db/level/#{level.id}"), json})
|
||||||
|
expect(res.statusCode).toBe(409)
|
||||||
|
level = yield Level.findById(level.id)
|
||||||
|
# should be unchanged
|
||||||
|
expect(level.get('slug')).toBe('another-level')
|
||||||
|
expect(level.get('version').isLatestMinor).toBe(true)
|
||||||
|
expect(level.get('version').isLatestMajor).toBe(true)
|
||||||
|
expect(level.get('index')).toBeDefined()
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'enforces permissions', ->
|
||||||
|
yield utils.clearModels([Level, User])
|
||||||
|
user = yield utils.initUser()
|
||||||
|
yield utils.loginUser(user)
|
||||||
|
level = yield utils.makeLevel({description:'Original desc'})
|
||||||
|
|
||||||
|
otherUser = yield utils.initUser()
|
||||||
|
yield utils.loginUser(otherUser)
|
||||||
|
json = _.extend({}, level.toObject(), {description: 'Trollin'})
|
||||||
|
[res, body] = yield request.postAsync({url: utils.getURL("/db/level/#{level.id}"), json})
|
||||||
|
expect(res.statusCode).toBe(403)
|
||||||
|
level = yield Level.findById(level.id)
|
||||||
|
expect(level.get('description')).toBe('Original desc')
|
||||||
|
done()
|
||||||
|
|
||||||
describe 'GET /db/level/:handle/session', ->
|
describe 'GET /db/level/:handle/session', ->
|
||||||
|
|
||||||
|
|
37
test/app/views/play/CampaignView.spec.coffee
Normal file
37
test/app/views/play/CampaignView.spec.coffee
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
factories = require 'test/app/factories'
|
||||||
|
CampaignView = require 'views/play/CampaignView'
|
||||||
|
Levels = require 'collections/Levels'
|
||||||
|
|
||||||
|
describe 'CampaignView', ->
|
||||||
|
|
||||||
|
describe 'when 4 earned levels', ->
|
||||||
|
beforeEach ->
|
||||||
|
@campaignView = new CampaignView()
|
||||||
|
@campaignView.levelStatusMap = {}
|
||||||
|
levels = new Levels(_.times(4, -> factories.makeLevel()))
|
||||||
|
@campaignView.campaign = factories.makeCampaign({}, {levels})
|
||||||
|
@levels = (level.toJSON() for level in levels.models)
|
||||||
|
earned = me.get('earned') or {}
|
||||||
|
earned.levels ?= []
|
||||||
|
earned.levels.push(level.original) for level in @levels
|
||||||
|
me.set('earned', earned)
|
||||||
|
|
||||||
|
describe 'and 3rd one is practice', ->
|
||||||
|
beforeEach ->
|
||||||
|
@levels[2].practice = true
|
||||||
|
@campaignView.annotateLevels(@levels)
|
||||||
|
it 'hides next levels if there are practice levels to do', ->
|
||||||
|
expect(@levels[2].hidden).toEqual(false)
|
||||||
|
expect(@levels[3].hidden).toEqual(true)
|
||||||
|
|
||||||
|
describe 'and 2nd rewards a practice a non-practice level', ->
|
||||||
|
beforeEach ->
|
||||||
|
@campaignView.levelStatusMap[@levels[0].slug] = 'complete'
|
||||||
|
@campaignView.levelStatusMap[@levels[1].slug] = 'complete'
|
||||||
|
@levels[1].rewards = [{level: @levels[2].original}, {level: @levels[3].original}]
|
||||||
|
@levels[2].practice = true
|
||||||
|
@campaignView.annotateLevels(@levels)
|
||||||
|
@campaignView.determineNextLevel(@levels)
|
||||||
|
it 'points at practice level first', ->
|
||||||
|
expect(@levels[2].next).toEqual(true)
|
||||||
|
expect(@levels[3].next).not.toBeDefined(true)
|
8
vendor/scripts/jquery.minicolors.min.js
vendored
8
vendor/scripts/jquery.minicolors.min.js
vendored
File diff suppressed because one or more lines are too long
245
vendor/styles/jquery.minicolors.css
vendored
245
vendor/styles/jquery.minicolors.css
vendored
|
@ -1,245 +0,0 @@
|
||||||
.minicolors {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors-swatch {
|
|
||||||
position: absolute;
|
|
||||||
vertical-align: middle;
|
|
||||||
background: url(/images/jquery.minicolors.png) -80px 0;
|
|
||||||
border: solid 1px #ccc;
|
|
||||||
cursor: text;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors-swatch-color {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors input[type=hidden] + .minicolors-swatch {
|
|
||||||
width: 28px;
|
|
||||||
position: static;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Panel */
|
|
||||||
.minicolors-panel {
|
|
||||||
position: absolute;
|
|
||||||
width: 173px;
|
|
||||||
height: 152px;
|
|
||||||
background: white;
|
|
||||||
border: solid 1px #CCC;
|
|
||||||
box-shadow: 0 0 20px rgba(0, 0, 0, .2);
|
|
||||||
z-index: 99999;
|
|
||||||
-moz-box-sizing: content-box;
|
|
||||||
-webkit-box-sizing: content-box;
|
|
||||||
box-sizing: content-box;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors-panel.minicolors-visible {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Panel positioning */
|
|
||||||
.minicolors-position-top .minicolors-panel {
|
|
||||||
top: -154px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors-position-right .minicolors-panel {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors-position-bottom .minicolors-panel {
|
|
||||||
top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors-position-left .minicolors-panel {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors-with-opacity .minicolors-panel {
|
|
||||||
width: 194px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors .minicolors-grid {
|
|
||||||
position: absolute;
|
|
||||||
top: 1px;
|
|
||||||
left: 1px;
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
background: url(/images/jquery.minicolors.png) -120px 0;
|
|
||||||
cursor: crosshair;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors .minicolors-grid-inner {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors-slider-saturation .minicolors-grid {
|
|
||||||
background-position: -420px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors-slider-saturation .minicolors-grid-inner {
|
|
||||||
background: url(/images/jquery.minicolors.png) -270px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors-slider-brightness .minicolors-grid {
|
|
||||||
background-position: -570px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors-slider-brightness .minicolors-grid-inner {
|
|
||||||
background: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors-slider-wheel .minicolors-grid {
|
|
||||||
background-position: -720px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors-slider,
|
|
||||||
.minicolors-opacity-slider {
|
|
||||||
position: absolute;
|
|
||||||
top: 1px;
|
|
||||||
left: 152px;
|
|
||||||
width: 20px;
|
|
||||||
height: 150px;
|
|
||||||
background: white url(/images/jquery.minicolors.png) 0 0;
|
|
||||||
cursor: row-resize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors-slider-saturation .minicolors-slider {
|
|
||||||
background-position: -60px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors-slider-brightness .minicolors-slider {
|
|
||||||
background-position: -20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors-slider-wheel .minicolors-slider {
|
|
||||||
background-position: -20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors-opacity-slider {
|
|
||||||
left: 173px;
|
|
||||||
background-position: -40px 0;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors-with-opacity .minicolors-opacity-slider {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pickers */
|
|
||||||
.minicolors-grid .minicolors-picker {
|
|
||||||
position: absolute;
|
|
||||||
top: 70px;
|
|
||||||
left: 70px;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border: solid 1px black;
|
|
||||||
border-radius: 10px;
|
|
||||||
margin-top: -6px;
|
|
||||||
margin-left: -6px;
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors-grid .minicolors-picker > div {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: solid 2px white;
|
|
||||||
-moz-box-sizing: content-box;
|
|
||||||
-webkit-box-sizing: content-box;
|
|
||||||
box-sizing: content-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors-picker {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 18px;
|
|
||||||
height: 2px;
|
|
||||||
background: white;
|
|
||||||
border: solid 1px black;
|
|
||||||
margin-top: -2px;
|
|
||||||
-moz-box-sizing: content-box;
|
|
||||||
-webkit-box-sizing: content-box;
|
|
||||||
box-sizing: content-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Inline controls */
|
|
||||||
.minicolors-inline {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors-inline .minicolors-input {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minicolors-inline .minicolors-panel {
|
|
||||||
position: relative;
|
|
||||||
top: auto;
|
|
||||||
left: auto;
|
|
||||||
box-shadow: none;
|
|
||||||
z-index: auto;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Default theme */
|
|
||||||
.minicolors-theme-default .minicolors-swatch {
|
|
||||||
top: 5px;
|
|
||||||
left: 5px;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
.minicolors-theme-default.minicolors-position-right .minicolors-swatch {
|
|
||||||
left: auto;
|
|
||||||
right: 5px;
|
|
||||||
}
|
|
||||||
.minicolors-theme-default.minicolors {
|
|
||||||
width: auto;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.minicolors-theme-default .minicolors-input {
|
|
||||||
height: 20px;
|
|
||||||
width: auto;
|
|
||||||
display: inline-block;
|
|
||||||
padding-left: 26px;
|
|
||||||
}
|
|
||||||
.minicolors-theme-default.minicolors-position-right .minicolors-input {
|
|
||||||
padding-right: 26px;
|
|
||||||
padding-left: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bootstrap theme */
|
|
||||||
.minicolors-theme-bootstrap .minicolors-swatch {
|
|
||||||
top: 3px;
|
|
||||||
left: 3px;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
.minicolors-theme-bootstrap.minicolors-position-right .minicolors-swatch {
|
|
||||||
left: auto;
|
|
||||||
right: 3px;
|
|
||||||
}
|
|
||||||
.minicolors-theme-bootstrap .minicolors-input {
|
|
||||||
padding-left: 44px;
|
|
||||||
}
|
|
||||||
.minicolors-theme-bootstrap.minicolors-position-right .minicolors-input {
|
|
||||||
padding-right: 44px;
|
|
||||||
padding-left: 12px;
|
|
||||||
}
|
|
Loading…
Reference in a new issue