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