Merge branch 'master' into production

This commit is contained in:
Nick Winter 2016-08-29 09:46:26 -07:00
commit 07bf0238b4
31 changed files with 606 additions and 564 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View file

@ -493,7 +493,7 @@ What was challenging:
Circulate to assist. Draw students attention to the instructions and tips. Flags can be tricky for some students, so allow them to pair up to beat the levels. Each student should write their own code, but its ok for another student to place the flags for them.
###Written Reflection (5 mins)
### Written Reflection (5 mins)
**How did you use properties today?**
>I had to see where the flag was and the flag has a property called pos. Then inside that it has two more properties, x and y. You use a dot to get inside the object, or inside the property.
@ -502,7 +502,7 @@ Circulate to assist. Draw students attention to the instructions and tips. Fl
##### Module 10
## Review and Synthesis
###Summary
### Summary
Read the instructions! Remember the hints! Sit and think about how to solve the problem and how youll be able to tell its solved. All the habits of mind of a good programmer come to bear on these levels: defining the problem, breaking the problem down into parts, making a plan, syntax and debugging, sticking to it, and asking for help.
### Transfer Goals

View file

@ -52,6 +52,7 @@ module.exports = class CocoRouter extends Backbone.Router
'artisans/solution-problems': go('artisans/SolutionProblemsView')
'artisans/thang-tasks': go('artisans/ThangTasksView')
'artisans/level-concepts': go('artisans/LevelConceptMap')
'artisans/level-guides': go('artisans/LevelGuidesView')
'beta': go('HomeView')

View file

@ -579,17 +579,17 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese",
tip_programming_not_about_computers: "天文学が望遠鏡に関する学問でないのと同様に、計算機科学はコンピュータに関する学問ではない。 - エドガー・ダイクストラ"
tip_mulan: "できると信じていれば、できる。 - ムーラン"
# play_game_dev_level:
# created_by: "Created by {{name}}"
# how_to_play_title: "How to play:"
# how_to_play_1: "Use the mouse to control the hero!"
# how_to_play_2: "Click anywhere on the map to move to that location."
# how_to_play_3: "Click on the ogres to attack them."
# restart: "Restart Level"
# play: "Play Level"
# play_more_codecombat: "Play More CodeCombat"
# default_student_instructions: "Click to control your hero and win your game!"
# back_to_coding: "Back to Coding"
play_game_dev_level:
created_by: "作成者:{{name}}"
how_to_play_title: "遊び方:"
how_to_play_1: "マウスでヒーローを操作しましょう!"
how_to_play_2: "マップの動きたい場所をどこでもクリックしましょう."
how_to_play_3: "オーガをクリックして攻撃しましょう."
restart: "レベルをリセット"
play: "プレイレベル"
play_more_codecombat: "もっとCodeCombatで遊ぶ"
default_student_instructions: "ヒーローをクリックしてゲームに勝ちましょう!"
back_to_coding: "コーディングに戻る"
game_menu:
inventory_tab: "インベントリー"
@ -767,8 +767,8 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese",
current_value: "現在値"
default_value: "デフォルト値"
parameters: "パラメータ"
# required_parameters: "Required Parameters"
# optional_parameters: "Optional Parameters"
required_parameters: "必須パラメーター"
optional_parameters: "任意パラメーター"
returns: "リターン"
granted_by: "スキルを与えてくれるアイテム:"
@ -823,9 +823,9 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese",
phoenix_title: "ソフトウェアエンジニア"
nolan_title: "地区担当マネージャー"
elliot_title: "パートナーシップマネージャー"
# elliot_blurb: "Mindreader"
# lisa_title: "Market Development Rep"
# sean_title: "Territory Manager"
elliot_blurb: "読心術者"
lisa_title: "市場開発代表"
sean_title: "地域部長"
retrostyle_title: "イラスト"
retrostyle_blurb: "レトロスタイルのゲーム"
jose_title: "ミュージック"
@ -1564,7 +1564,7 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese",
article_title: "アーティクル エディター"
thang_title: "サングエディター"
level_title: "レベルエディター"
# course_title: "Course Editor"
course_title: "コースエディター"
achievement_title: "実績エディター"
poll_title: "投票エディター"
back: "バック"
@ -1750,7 +1750,7 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese",
rank_failed: "ランキングに送信できませんでした。"
rank_being_ranked: "ランキングにのっています"
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."
# 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."
@ -2039,19 +2039,19 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese",
# license: "license"
# oreilly: "ebook of your choice"
# calendar:
# year: "Year"
# day: "Day"
# month: "Month"
# january: "January"
# february: "February"
# march: "March"
# april: "April"
# may: "May"
# june: "June"
# july: "July"
# august: "August"
# september: "September"
# october: "October"
# november: "November"
# december: "December"
calendar:
year: ""
day: ""
month: ""
january: "1月"
february: "2月"
march: "3月"
april: "4月"
may: "5月"
june: "6月"
july: "7月"
august: "8月"
september: "9月"
october: "10月"
november: "11月"
december: "12月"

View file

@ -305,22 +305,22 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription:
enter_class_code: "Coloque seu código de classe"
enter_birthdate: "Coloque sua data de aniversário:"
parent_use_birthdate: "Responsáveis, usem o seu propio dia de nascimento."
# 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 parents email address:"
# parent_email_error: "Something went wrong when trying to send the email. Check the email address and try again."
# parent_email_sent: "Weve 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:"
# select_your_starting_hero: "Select Your Starting Hero:"
# you_can_always_change_your_hero_later: "You can always change your hero later."
ask_teacher_1: "Pergunte ao seu professor qual o código da sua turma."
ask_teacher_2: "Não faz parte da turma? Crie uma "
ask_teacher_3: "Conta Pessoal"
ask_teacher_4: " como alternativa."
about_to_join: "Sobre juntar-se:"
enter_parent_email: "Informe o endereço de e-mail de seus pais:"
parent_email_error: "Algo de errado aconteceu ao tentar enviar o email. Verifique o endereço de email e tente novamente."
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: "Conta criada!"
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: "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: "Escreva isso:"
start_playing: "Comece jogando!"
sso_connected: "Conectado com sucesso como:"
select_your_starting_hero: "Selecione um herói para começar:"
you_can_always_change_your_hero_later: "Você poderá mudar seu herói depois."
recover:
recover_account_title: "Recuperar conta"
@ -337,18 +337,18 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription:
common:
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"
# default_code: "Default Code"
default_code: "Código padrão"
loading: "Carregando..."
# overview: "Overview"
# solution: "Solution"
# intro: "Intro"
overview: "Visão geral"
solution: "Solução"
intro: "Introdução"
saving: "Salvando..."
sending: "Enviando..."
send: "Enviar"
# sent: "Sent"
# type: "Type"
sent: "Enviado"
type: "Tipo"
cancel: "Cancelar"
save: "Salvar"
publish: "Publicar"
@ -364,7 +364,7 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription:
submit_patch: "Enviar arranjo"
submit_changes: "Enviar mudanças"
save_changes: "Salvar mudanças"
# required_field: "required"
required_field: "obrigatório"
general:
and: "e"
@ -418,7 +418,7 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription:
wizard: "Feiticeiro"
first_name: "Primeiro Nome"
last_name: "Último Nome"
# last_initial: "Last Initial"
last_initial: "Última Inicial"
username: "Nome de Usuário"
units:
@ -438,15 +438,15 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription:
years: "anos"
play_level:
# level_complete: "Level Complete"
level_complete: "Nível Completo"
completed_level: "Nivel Completo:"
course: "Curso:"
done: "Pronto"
next_level: "Proximo Nivel"
next_game: "Próximo jogo"
# language: "Language"
# languages: "Languages"
# programming_language: "Programming language"
language: "Linguagem"
languages: "Linguagens"
programming_language: "Linguagem de programação"
show_menu: "Mostrar menu do jogo"
home: "Início" # Not used any more, will be removed soon.
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_gems_gained: "Gemas ganhas"
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_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"
tome_cast_button_run: "Rodar"
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_your_skills: "Suas habilidades"
tome_current_method: "Método Atual"
# hints: "Hints"
# hints_title: "Hint {{number}}"
hints: "Sugestões"
hints_title: "Sugestão {{number}}"
code_saved: "Código Salvo"
skip_tutorial: "Pular (esc)"
keyboard_shortcuts: "Teclas de atalho"

View file

@ -0,0 +1,5 @@
#level-guides-view
.problem
color: red
.level-details
width: 15%

View file

@ -247,10 +247,9 @@ block content
#story-languages
.text-center
.text-h5(data-i18n="about.story_statistic_3c")
#language-icons.text-center(title="CoffeeScript, JavaScript, Python, Java, Lua")
img.hidden-xs(src="/images/pages/about/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/languages_group2.png")
#language-icons.text-center(title="JavaScript, Python, HTML, CSS, jQuery, Bootstrap")
img.hidden-xs(src="/images/pages/about/new_languages.png")
img.hidden-sm.hidden-md.hidden-lg(src="/images/pages/about/new_languages_xs.png")
#story-graphic-4.text-center
p

View file

@ -6,11 +6,14 @@ block content
a(href='/artisans/thang-tasks')
|Thang Tasks
div
a(href="/artisans/level-tasks")
a(href='/artisans/level-tasks')
|Level Tasks
div
a(href="/artisans/solution-problems")
a(href='/artisans/solution-problems')
|Solution Problems
div
a(href="/artisans/level-concepts")
a(href='/artisans/level-concepts')
|Level Concept Map
div
a(href='/artisans/level-guides')
|Level Guides Overview

View 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

View file

@ -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)
if level.slug == 'lost-viking'
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")
if levelStatusMap[level.slug] === 'complete'
img.banner(src="/images/pages/play/level-banner-complete.png")

View 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'

View file

@ -178,7 +178,7 @@ module.exports = class CampaignView extends RootView
context.levels = _.reject context.levels, slug: reject
if me.isOnFreeOnlyServer()
context.levels = _.reject context.levels, 'requiresSubscription'
@annotateLevel level for level in context.levels
@annotateLevels(context.levels)
count = @countLevels context.levels
context.levelsCompleted = count.completed
context.levelsTotal = count.total
@ -198,7 +198,7 @@ module.exports = class CampaignView extends RootView
context.adjacentCampaigns = _.filter _.values(_.cloneDeep(@campaign?.get('adjacentCampaigns') or {})), (ac) =>
if ac.showIfUnlocked and not @editorMode
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'
styles = []
styles.push "color: #{ac.color}" if ac.color
@ -231,7 +231,7 @@ module.exports = class CampaignView extends RootView
if _.isString(ac.showIfUnlocked)
_.find(@campaigns.models, id: acID)?.locked = false if ac.showIfUnlocked in me.levels()
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
@ -278,49 +278,55 @@ module.exports = class CampaignView extends RootView
return me.getCampaignAdsGroup() is 'leaderboard-ads'
false
annotateLevel: (level) ->
level.position ?= { x: 10, y: 10 }
level.locked = not me.ownsLevel level.original
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 @editorMode
level.locked = false if @campaign?.get('name') in ['Auditions', 'Intro']
level.locked = false if me.isInGodMode()
#level.locked = false if level.slug is 'robot-ragnarok'
level.disabled = true if level.adminOnly and @levelStatusMap[level.slug] not in ['started', 'complete']
level.disabled = false if me.isInGodMode()
level.color = 'rgb(255, 80, 60)'
if level.requiresSubscription
level.color = 'rgb(80, 130, 200)'
if unlocksHero = _.find(level.rewards, 'hero')?.hero
level.unlocksHero = unlocksHero
if level.unlocksHero
level.purchasedHero = level.unlocksHero in (me.get('purchased')?.heroes or [])
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.locked = not me.ownsLevel(level.original) or previousIncompletePracticeLevel
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 @editorMode
level.locked = false if @campaign?.get('name') in ['Auditions', 'Intro']
level.locked = false if me.isInGodMode()
#level.locked = false if level.slug is 'robot-ragnarok'
level.disabled = true if level.adminOnly and @levelStatusMap[level.slug] not in ['started', 'complete']
level.disabled = false if me.isInGodMode()
level.color = 'rgb(255, 80, 60)'
level.color = 'rgb(80, 130, 200)' if level.requiresSubscription
level.color = 'rgb(200, 80, 200)' if level.adventurer
if unlocksHero = _.find(level.rewards, 'hero')?.hero
level.unlocksHero = unlocksHero
if level.unlocksHero
level.purchasedHero = level.unlocksHero in (me.get('purchased')?.heroes or [])
if window.serverConfig.picoCTF
if problem = _.find(@picoCTFProblems or [], pid: level.picoCTFProblem)
level.locked = false if problem.unlocked or level.slug is 'digital-graffiti'
#level.locked = false # Testing to see all levels
level.description = """
### #{problem.name}
#{level.description or problem.description}
if window.serverConfig.picoCTF
if problem = _.find(@picoCTFProblems or [], pid: level.picoCTFProblem)
level.locked = false if problem.unlocked or level.slug is 'digital-graffiti'
#level.locked = false # Testing to see all levels
level.description = """
### #{problem.name}
#{level.description or problem.description}
#{problem.category} - #{problem.score} points
"""
level.color = 'rgb(80, 130, 200)' if problem.solved
#{problem.category} - #{problem.score} points
"""
level.color = 'rgb(80, 130, 200)' if problem.solved
level.hidden = level.locked
if level.concepts?.length
level.displayConcepts = level.concepts
maxConcepts = 6
if level.displayConcepts.length > maxConcepts
level.displayConcepts = level.displayConcepts.slice -maxConcepts
level
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
if level.concepts?.length
level.displayConcepts = level.concepts
maxConcepts = 6
if level.displayConcepts.length > maxConcepts
level.displayConcepts = level.displayConcepts.slice -maxConcepts
countLevels: (levels) ->
count = total: 0, completed: 0
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
unlockedInSameCampaign = levelIndex < 5 # First few are always counted (probably unlocked in previous campaign)
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
@openModalView leaderboardModal
determineNextLevel: (levels) ->
foundNext = false
determineNextLevel: (orderedLevels) ->
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'}]
for level in levels
findNextLevel = (nextLevels, practiceOnly) =>
for nextLevelOriginal in nextLevels
nextLevel = _.find orderedLevels, original: nextLevelOriginal
continue if not nextLevel or nextLevel.locked
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 nextLevel.slug is 'kithgard-mastery' and not @levelStatusMap[nextLevel.slug] and @calculateExperienceScore() >= 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.
dontPointTo = _.without dontPointTo, nextLevel.slug
storage.save "pointed-out-#{nextLevel.slug}", timesPointedOut + 1
# Should we point this level out?
if not nextLevel.disabled and @levelStatusMap[nextLevel.slug] isnt 'complete' and nextLevel.slug not in dontPointTo and
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])
)
nextLevel.next = true
return true
false
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)
unless foundNext
for nextLevelOriginal in level.nextLevels
nextLevel = _.find levels, original: nextLevelOriginal
break if foundNext = findNextLevel(level.nextLevels, true) # Check practice levels first
break if foundNext = findNextLevel(level.nextLevels, false)
# 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
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.
dontPointTo = _.without dontPointTo, nextLevel.slug
storage.save "pointed-out-#{nextLevel.slug}", timesPointedOut + 1
# 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 (
me.isPremium() or not nextLevel.requiresSubscription or
_.any(subscriptionPrompts, (prompt) => nextLevel.slug is prompt.slug and not @levelStatusMap[prompt.unless])
)
nextLevel.next = true
foundNext = true
break
if not foundNext and levels[0] and not levels[0].locked and @levelStatusMap[levels[0].slug] isnt 'complete'
levels[0].next = true
if not foundNext and orderedLevels[0] and not orderedLevels[0].locked and @levelStatusMap[orderedLevels[0].slug] isnt 'complete'
orderedLevels[0].next = true
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.
@ -409,6 +423,7 @@ module.exports = class CampaignView extends RootView
@particleMan.removeEmitters()
@particleMan.attach @$el.find('.map')
for level in @campaign.renderedLevels ? {}
continue if level.practice
terrain = @terrain.replace('-branching-test', '').replace(/(campaign-)?(game|web)-dev-\d/, 'forest').replace('intro', 'dungeon')
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

View file

@ -321,14 +321,12 @@ module.exports = class Autocomplete
attackEntry.content = attackEntry.content.replace '${1:enemy}', '"${1:Enemy Name}"'
snippetEntries.push attackEntry
# Add copied hero. entries for most important ones that start with hero.
sortedEntries = _.sortBy snippetEntries, (entry) -> -1 * parseInt(entry.importance ? 0)
for entry in sortedEntries
if entry.content?.indexOf('hero.') is 0
newEntry = _.cloneDeep(entry)
entry.name = "hero.#{newEntry.name}"
snippetEntries.push(newEntry)
break if snippetEntries.length - sortedEntries.length >= 10
# Update 'hero.' and 'game.' entries to include their prefixes
for entry in snippetEntries
if entry.content?.indexOf('hero.') is 0 and entry.name?.indexOf('hero.') < 0
entry.name = "hero.#{entry.name}"
else if entry.content?.indexOf('game.') is 0 and entry.name?.indexOf('game.') < 0
entry.name = "game.#{entry.name}"
if haveFindNearest and not haveFindNearestEnemy
spellView.translateFindNearest()

View file

@ -136,7 +136,7 @@ module.exports = (SnippetManager, autoLineEndings) ->
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)
console.log "Bailing", fullPrefixParts, '|', prefix, '|', beginningOfLine, '|', pos.column - prefix.length
# console.log "DEBUG: autocomplete bailing", fullPrefixParts, '|', prefix, '|', beginningOfLine, '|', pos.column - prefix.length
@completions = completions
return callback null, completions
@ -195,10 +195,10 @@ getFullIdentifier = (doc, pos) ->
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}"
fuzzScore = 0.1
snippetLineBreaks = (snippet.match(lineBreak) || []).length
# input will be replaced by snippet
# trim snippet prefix and suffix if already in the document (line)
if prefixStart = snippet.toLowerCase().indexOf(input.toLowerCase()) > -1
snippetLines = (snippet.match(lineBreak) || []).length
captionStart = snippet.indexOf caption
# 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
toLinePrefix = line.substring 0, linePrefixIndex
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 += "\n" if snippetLines is 0 and not /\$\{/.test(snippet)
snippet += autoLineEndings[lang] if snippetLineBreaks is 0 and autoLineEndings[lang]
snippet += "\n" if snippetLineBreaks is 0 and not /\$\{/.test(snippet)
if captureReturn and /^\s*$/.test(toLinePrefix)
snippet = captureReturn + linePrefix + snippet
# console.log "Snippets snippetPrefix=#{snippetPrefix} linePrefix=#{linePrefix} snippetSuffix=#{snippetSuffix} lineSuffix=#{lineSuffix} snippet=#{snippet} score=#{fuzzScore}"
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
startsWith = (string, searchString, position) ->

View file

@ -23,6 +23,8 @@ let zpMinActivityDate = new Date();
zpMinActivityDate.setUTCDate(zpMinActivityDate.getUTCDate() - 30);
zpMinActivityDate = zpMinActivityDate.toISOString().substring(0, 10);
const closeParallelLimit = 100;
getZPContacts((err, emailContactMap) => {
if (err) {
console.log(err);
@ -33,7 +35,7 @@ getZPContacts((err, emailContactMap) => {
const contact = emailContactMap[email];
tasks.push(createUpsertCloseLeadFn(contact));
}
async.parallel(tasks, (err, results) => {
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
if (err) console.log(err);
log("Script runtime: " + (new Date() - scriptStartTime));
});
@ -127,26 +129,39 @@ function updateCloseLead(zpContact, existingLead, done) {
}
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) => {
// console.log(`DEBUG: createUpsertCloseLeadFn ${zpContact.organization} ${zpContact.email}`);
const query = `email:${zpContact.email}`;
const url = `https://${closeIoApiKey}:X@app.close.io/api/v1/lead/?query=${encodeURIComponent(query)}`;
let query = `email:${zpContact.email}`;
let url = `https://${closeIoApiKey}:X@app.close.io/api/v1/lead/?query=${encodeURIComponent(query)}`;
request.get(url, (error, response, body) => {
if (error) return done(error);
const data = JSON.parse(body);
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) => {
if (error) return done(error);
const data = JSON.parse(body);
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);
}
else {
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);
}
});

View file

@ -14,7 +14,7 @@ if (process.argv.length !== 10) {
// TODO: Reduce response data via _fields param
// TODO: Assumes 1:1 contact:email relationship (Close.io supports multiple emails for a single contact)
// 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)
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 closeParallelLimit = 100;
const scriptStartTime = new Date();
const closeIoApiKey = process.argv[2];
// 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;
tasks.push(createAddContactFn(newContact, lead, existingLead, userApiKeyMap));
}
async.parallel(tasks, (err, results) => {
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
if (err) return done(err);
// Add notes
@ -690,7 +692,7 @@ function updateExistingLead(lead, existingLead, userApiKeyMap, done) {
for (const newNote of newNotes) {
tasks.push(createAddNoteFn(existingLead.id, newNote));
}
async.parallel(tasks, (err, results) => {
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
return done(err);
});
});
@ -721,7 +723,7 @@ function saveNewLead(lead, done) {
for (const newNote of newNotes) {
tasks.push(createAddNoteFn(existingLead.id, newNote));
}
async.parallel(tasks, (err, results) => {
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
if (err) return done(err);
// 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));
}
}
async.parallel(tasks, (err, results) => {
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
return done(err);
});
});
@ -767,34 +769,61 @@ function createFindExistingLeadFn(email, name, existingLeads) {
}
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) => {
// console.log('DEBUG: updateLead', lead.name);
const query = `name:"${lead.name}"`;
if (existingLeads[lead.name.toLowerCase()]) {
if (existingLeads[lead.name.toLowerCase()].length === 1) {
// console.log(`DEBUG: Using lead from email lookup: ${lead.name}`);
return updateExistingLead(lead, existingLeads[lead.name.toLowerCase()][0], userApiKeyMap, done);
}
console.error(`ERROR: ${existingLeads[lead.name.toLowerCase()].length} email leads found for ${lead.name}`);
return 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 === 0) {
if (existingLeads[lead.name.toLowerCase()]) {
if (existingLeads[lead.name.toLowerCase()].length === 1) {
// console.log(`DEBUG: Using lead from email lookup: ${lead.name}`);
return updateExistingLead(lead, existingLeads[lead.name.toLowerCase()][0], userApiKeyMap, done);
}
console.error(`ERROR: ${existingLeads[lead.name.toLowerCase()].length} email leads found for ${lead.name}`);
return done();
}
return saveNewLead(lead, done);
}
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 updateExistingLead(lead, data.data[0], userApiKeyMap, done);
if (data.total_results === 1) {
return updateExistingLead(lead, data.data[0], userApiKeyMap, done);
}
return saveNewLead(lead, done);
} catch (error) {
// console.log(url);
console.log(`ERROR: updateLead ${error}`);
// console.log(body);
return done();
}
});
@ -939,7 +968,7 @@ function updateLeads(leads, done) {
for (const closeIoMailApiKey of closeIoMailApiKeys) {
tasks.push(createGetUserFn(closeIoMailApiKey.apiKey));
}
async.parallel(tasks, (err, results) => {
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
if (err) console.log(err);
// Lookup existing leads via email to protect against direct lead name querying later
// Querying via lead name is unreliable
@ -951,14 +980,14 @@ function updateLeads(leads, done) {
tasks.push(createFindExistingLeadFn(email.toLowerCase(), name.toLowerCase(), existingLeads));
}
}
async.parallel(tasks, (err, results) => {
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
if (err) return done(err);
const tasks = [];
for (const name in leads) {
if (leadsToSkip.indexOf(name) >= 0) continue;
tasks.push(createUpdateLeadFn(leads[name], existingLeads, userApiKeyMap));
}
async.series(tasks, (err, results) => {
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
return done(err);
});
});

View file

@ -123,10 +123,14 @@ module.exports =
if _.isEmpty(req.body)
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
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
isOwner = doc.getAccessForUserObjectId(req.user._id) is 'owner'

View file

@ -16,64 +16,8 @@ Classroom = require '../models/Classroom'
LevelHandler = class LevelHandler extends Handler
modelClass: Level
jsonSchema: require '../../app/schemas/models/level'
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'
]
postEditableProperties: ['name']
editableProperties: Level.editableProperties
postEditableProperties: Level.postEditableProperties
getByRelationship: (req, res, args...) ->
return @getSession(req, res, args[0]) if args[1] is 'session'

View file

@ -9,62 +9,66 @@ mongoose = require 'mongoose'
database = require '../commons/database'
parse = require '../commons/parse'
# More info on database versioning: https://github.com/codecombat/codecombat/wiki/Versioning
module.exports =
postNewVersion: (Model, options={}) -> wrap (req, res) ->
# Find the document which is getting a new version
parent = yield database.getDocFromHandle(req, Model)
if not parent
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
permissions = options.hasPermissionsOrTranslations
permissions = [permissions] if _.isString(permissions)
permissions = ['admin'] if not _.isArray(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))
throw new errors.Forbidden()
# Create the new version, a clone of the parent with POST data applied
doc = database.initDoc(req, Model)
ATTRIBUTES_NOT_INHERITED = ['_id', 'version', 'created', 'creator']
doc.set(_.omit(parent.toObject(), ATTRIBUTES_NOT_INHERITED))
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
original = parent.get('original')
if _.isNumber(major)
q1 = Model.findOne({original: original, 'version.isLatestMinor': true, 'version.major': major})
else
q1 = Model.findOne({original: original, 'version.isLatestMajor': true})
q1.select 'version'
q1.select latestSelect
latest = yield q1.exec()
# Handle the case where no version is marked as latest, since making new
# versions is not atomic
if not latest
# handle the case where no version is marked as latest, since making new
# versions is not atomic
if _.isNumber(major)
q2 = Model.findOne({original: original, 'version.major': major})
q2.sort({'version.minor': -1})
else
q2 = Model.findOne()
q2.sort({'version.major': -1, 'version.minor': -1})
q2.select 'version'
q2.select(latestSelect)
latest = yield q2.exec()
if not latest
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
version = _.clone(latest.get('version'))
wasLatestMajor = version.isLatestMajor
version.isLatestMajor = false
if _.isNumber(major)
version.isLatestMinor = false
conditions = {_id: latest._id}
raw = yield Model.update(conditions, {version: version, $unset: {index: 1, slug: 1}})
raw = yield latest.update({$set: {version: version}, $unset: {index: 1, slug: 1}})
if not raw.nModified
console.error('Conditions', conditions)
console.error('Doc', doc)
@ -89,7 +93,12 @@ module.exports =
doc.set('parent', latest._id)
doc = yield doc.save()
try
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']
docLink = "http://codecombat.com#{editPath}"

View file

@ -93,6 +93,7 @@ AchievementSchema.statics.editableProperties = [
'i18nCoverage'
'hidden'
]
AchievementSchema.statics.postEditableProperties = []
AchievementSchema.statics.jsonSchema = require '../../app/schemas/models/achievement'

View file

@ -56,5 +56,6 @@ CampaignSchema.statics.editableProperties = [
'adjacentCampaigns'
'levels'
]
CampaignSchema.statics.postEditableProperties = []
module.exports = mongoose.model('campaign', CampaignSchema)

View file

@ -23,6 +23,7 @@ ClassroomSchema.statics.editableProperties = [
'ageRangeMax'
'archived'
]
ClassroomSchema.statics.postEditableProperties = []
ClassroomSchema.statics.generateNewCode = (done) ->
tryCode = ->

View file

@ -28,6 +28,7 @@ CodeLogSchema.statics.editableProperties = [
'log'
'created'
]
CodeLogSchema.statics.postEditableProperties = []
CodeLogSchema.statics.jsonSchema = require '../../app/schemas/models/codelog.schema'

View file

@ -46,4 +46,64 @@ LevelSchema.post 'init', (doc) ->
if _.isString(doc.get('nextLevel'))
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)

View file

@ -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/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.delete('/db/user/:handle', mw.users.removeFromClassrooms)
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.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.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/-/teachers', mw.auth.checkHasPermission(['admin']), mw.users.getTeachers)
app.post('/db/user/:handle/signup-with-facebook', mw.users.signupWithFacebook)

View file

@ -52,9 +52,40 @@ describe 'POST /db/level/:handle', ->
url = getURL("/db/level/#{@level.id}")
[res, body] = yield request.postAsync({url: url, json: levelJSON})
expect(res.statusCode).toBe(200)
expect(res.statusCode).toBe(201)
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', ->

View 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)

File diff suppressed because one or more lines are too long

View file

@ -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;
}