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

@ -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,9 +278,11 @@ module.exports = class CampaignView extends RootView
return me.getCampaignAdsGroup() is 'leaderboard-ads'
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.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 = false if @levelStatusMap[level.slug] in ['started', 'complete']
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 = false if me.isInGodMode()
level.color = 'rgb(255, 80, 60)'
if level.requiresSubscription
level.color = 'rgb(80, 130, 200)'
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
@ -309,18 +311,22 @@ module.exports = class CampaignView extends RootView
"""
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
if level.concepts?.length
level.displayConcepts = level.concepts
maxConcepts = 6
if level.displayConcepts.length > maxConcepts
level.displayConcepts = level.displayConcepts.slice -maxConcepts
level
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
# 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
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 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
# 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
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
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
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)
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: ->
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,15 +769,15 @@ 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}"`;
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}`);
@ -784,17 +786,44 @@ function createUpdateLeadFn(lead, existingLeads, userApiKeyMap) {
console.error(`ERROR: ${existingLeads[lead.name.toLowerCase()].length} email leads found for ${lead.name}`);
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) {
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();
}
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()
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
if not latest
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)
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;
}