From 6786f89a47487f8a50a7e9a27c9a3e729dc2fd19 Mon Sep 17 00:00:00 2001 From: bencodeorg Date: Tue, 20 Feb 2018 10:11:40 -0800 Subject: [PATCH 01/50] Restore ability to execute Redshift queries from production --- lib/cdo/redshift.rb | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 lib/cdo/redshift.rb diff --git a/lib/cdo/redshift.rb b/lib/cdo/redshift.rb new file mode 100644 index 0000000000000..fc222f516969b --- /dev/null +++ b/lib/cdo/redshift.rb @@ -0,0 +1,31 @@ +# WARNING: This is explicitly not included in our root Gemfile for the reasons discussed in the PR +# https://github.com/code-dot-org/code-dot-org/pull/14056. A separate gemfile should be created for +# any scripts needing usage of this client and loaded via RakeUtils.with_bundle_dir. +# @example: +# RakeUtils.with_bundle_dir(File.dirname(__FILE__)) do +# require 'cdo/redshift' +# end + +RakeUtils.with_bundle_dir(File.expand_path('../../../bin/cron/with_pg', __FILE__)) do + require 'pg' +end +require 'singleton' + +# A thin wrapper around PG, providing a mechanism to execute SQL commands on our AWS Redshift +# instance. +class RedshiftClient + include Singleton + + def initialize + port = 5439 + options = '' + tty = '' + dbname = 'dashboard' + login = 'dev' + @conn = PGconn.new(CDO.redshift_host, port, options, tty, dbname, login, CDO.redshift_password) + end + + def exec(sql_query) + @conn.exec(sql) + end +end From 03dd91f3bf2d91c567b8d3c07dc5ef7303ac32b4 Mon Sep 17 00:00:00 2001 From: Ram Kandasamy Date: Mon, 19 Feb 2018 11:46:13 -0800 Subject: [PATCH 02/50] Add whenTouching and displace blocks --- apps/src/gamelab/blocks.js | 29 ++++++++++++++++--- dashboard/app/models/levels/gamelab_jr.rb | 2 ++ .../levels/New Game Lab Jr Project.level | 24 +++++++++++++++ 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/apps/src/gamelab/blocks.js b/apps/src/gamelab/blocks.js index 8009e41ce28ef..57d1d1ae9b28b 100644 --- a/apps/src/gamelab/blocks.js +++ b/apps/src/gamelab/blocks.js @@ -30,7 +30,7 @@ export default { ORDER_FUNCTION_CALL, ORDER_MEMBER, } = Blockly.JavaScript; - const SPRITE_TYPE = blockly.BlockValueType.NUMBER; + const SPRITE_TYPE = blockly.BlockValueType.NONE; const generator = blockly.Generator.get('JavaScript'); const createJsWrapperBlock = ({ @@ -118,9 +118,9 @@ export default { func: 'makeNewSprite', blockText: 'Make a new {ANIMATION} sprite at {X} {Y}', args: [ - { name: 'ANIMATION', options: SPRITES}, - { name: 'X', type: blockly.BlockValueType.NUMBER}, - { name: 'Y', type: blockly.BlockValueType.NUMBER}, + { name: 'ANIMATION', options: SPRITES }, + { name: 'X', type: blockly.BlockValueType.NUMBER }, + { name: 'Y', type: blockly.BlockValueType.NUMBER }, ], returnType: SPRITE_TYPE, }); @@ -220,5 +220,26 @@ export default { args: [], eventLoopBlock: true, }); + + createJsWrapperBlock({ + category: EVENT_CATEGORY, + func: 'whenTouching', + blockText: 'when {SPRITE1} is touching {SPRITE2}', + args: [ + { name: 'SPRITE1', type: SPRITE_TYPE }, + { name: 'SPRITE2', type: SPRITE_TYPE }, + ], + eventBlock: true, + }); + + createJsWrapperBlock({ + category: SPRITE_CATEGORY, + func: 'displace', + blockText: '{THIS} blocks {SPRITE} from moving', + args: [ + {name: 'SPRITE', type: SPRITE_TYPE }, + ], + methodCall: true, + }); }, }; diff --git a/dashboard/app/models/levels/gamelab_jr.rb b/dashboard/app/models/levels/gamelab_jr.rb index 2c79da35f14ab..f5c07ec213d5c 100644 --- a/dashboard/app/models/levels/gamelab_jr.rb +++ b/dashboard/app/models/levels/gamelab_jr.rb @@ -58,6 +58,7 @@ def common_blocks(type) + @@ -68,6 +69,7 @@ def common_blocks(type) + diff --git a/dashboard/config/scripts/levels/New Game Lab Jr Project.level b/dashboard/config/scripts/levels/New Game Lab Jr Project.level index bc1a47db290ca..55e46ba8b9d0a 100644 --- a/dashboard/config/scripts/levels/New Game Lab Jr Project.level +++ b/dashboard/config/scripts/levels/New Game Lab Jr Project.level @@ -285,6 +285,18 @@ + + + + sprite + + + + + other + + + @@ -295,6 +307,18 @@ + + + + sprite + + + + + other + + + From 0d59424d3f1ad817ce4f6665649b83b4a6395f47 Mon Sep 17 00:00:00 2001 From: Ram Kandasamy Date: Tue, 20 Feb 2018 12:22:43 -0800 Subject: [PATCH 03/50] Add group blocks --- apps/src/gamelab/blocks.js | 20 +++++- dashboard/app/models/levels/gamelab_jr.rb | 39 +++++++++++ .../levels/New Game Lab Jr Project.level | 66 ++++++++++++++++++- 3 files changed, 123 insertions(+), 2 deletions(-) diff --git a/apps/src/gamelab/blocks.js b/apps/src/gamelab/blocks.js index 57d1d1ae9b28b..193e8874dd0c7 100644 --- a/apps/src/gamelab/blocks.js +++ b/apps/src/gamelab/blocks.js @@ -116,7 +116,7 @@ export default { createJsWrapperBlock({ category: SPRITE_CATEGORY, func: 'makeNewSprite', - blockText: 'Make a new {ANIMATION} sprite at {X} {Y}', + blockText: 'make a new {ANIMATION} sprite at {X} {Y}', args: [ { name: 'ANIMATION', options: SPRITES }, { name: 'X', type: blockly.BlockValueType.NUMBER }, @@ -125,6 +125,24 @@ export default { returnType: SPRITE_TYPE, }); + createJsWrapperBlock({ + category: SPRITE_CATEGORY, + func: 'makeNewGroup', + blockText: 'make a new group', + args: [], + returnType: SPRITE_TYPE, + }); + + createJsWrapperBlock({ + category: SPRITE_CATEGORY, + func: 'add', + blockText: 'add {SPRITE} to group {THIS}', + args: [ + { name: 'SPRITE', type: SPRITE_TYPE }, + ], + methodCall: true, + }); + createJsWrapperBlock({ category: SPRITE_CATEGORY, func: 'moveUp', diff --git a/dashboard/app/models/levels/gamelab_jr.rb b/dashboard/app/models/levels/gamelab_jr.rb index f5c07ec213d5c..92e49db8fce93 100644 --- a/dashboard/app/models/levels/gamelab_jr.rb +++ b/dashboard/app/models/levels/gamelab_jr.rb @@ -60,6 +60,10 @@ def common_blocks(type) + + + + @@ -93,6 +97,41 @@ def common_blocks(type) + + + + + + + + + + + + + 10 + + + + + + + + 1 + + + + + 10 + + + + + 1 + + + + XML end diff --git a/dashboard/config/scripts/levels/New Game Lab Jr Project.level b/dashboard/config/scripts/levels/New Game Lab Jr Project.level index 55e46ba8b9d0a..2f48b72816e94 100644 --- a/dashboard/config/scripts/levels/New Game Lab Jr Project.level +++ b/dashboard/config/scripts/levels/New Game Lab Jr Project.level @@ -228,7 +228,7 @@ }, "published": true, "notes": "", - "audit_log": "", + "audit_log": "[]", "level_concept_difficulty": { } }]]> @@ -298,6 +298,26 @@ + + + group + + + + + + + + sprite + + + + + group + + + + @@ -321,6 +341,50 @@ + + + + + 10 + + + + + WHILE + + + i + + + 1 + + + + + 10 + + + + + 1 + + + + + + + + + 1 + + + + + 100 + + + + From 38f04928424e689ef496ecd1a1b3a703baf29cbf Mon Sep 17 00:00:00 2001 From: Caley Brock Date: Tue, 20 Feb 2018 14:21:50 -0800 Subject: [PATCH 04/50] Single student row pinned to top --- .../ManageStudentsActionsCell.jsx | 13 +++++++- .../manageStudents/ManageStudentsTable.jsx | 19 +++++++++++- .../manageStudents/manageStudentsRedux.js | 30 +++++++++++++++++-- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/apps/src/templates/manageStudents/ManageStudentsActionsCell.jsx b/apps/src/templates/manageStudents/ManageStudentsActionsCell.jsx index cd33c84df3d91..cb40fff099f89 100644 --- a/apps/src/templates/manageStudents/ManageStudentsActionsCell.jsx +++ b/apps/src/templates/manageStudents/ManageStudentsActionsCell.jsx @@ -23,6 +23,7 @@ class ManageStudentActionsCell extends Component { isEditing: PropTypes.bool, isSaving: PropTypes.bool, disableSaving: PropTypes.bool, + isAddRow: PropTypes.bool, // Provided by redux startEditingStudent: PropTypes.func, cancelEditingStudent: PropTypes.func, @@ -91,7 +92,7 @@ class ManageStudentActionsCell extends Component { } - {this.props.isEditing && + {(this.props.isEditing && !this.props.isAddRow) &&
} + {this.props.isAddRow && +
+
+ } { ); }; +const sortRows = (data, columnIndexList, orderList) => { + let addRows = []; + let studentRows = []; + for (let i = 0; i ); }; @@ -279,7 +296,7 @@ class ManageStudentsTable extends Component { const sortedRows = sort.sorter({ columns, sortingColumns, - sort: orderBy, + sort: sortRows, })(this.props.studentData); return ( diff --git a/apps/src/templates/manageStudents/manageStudentsRedux.js b/apps/src/templates/manageStudents/manageStudentsRedux.js index 1f3480ba9ac8c..5f1557a3eed12 100644 --- a/apps/src/templates/manageStudents/manageStudentsRedux.js +++ b/apps/src/templates/manageStudents/manageStudentsRedux.js @@ -37,10 +37,31 @@ export const saveStudent = (studentId) => { }; }; + const initialState = { loginType: '', - studentData: {}, - editingData: {}, + studentData: { + 0: { + id: 0, + name: '', + username: '', + sectionId: 0, + loginType: '', + isEditing: true, + isAddRow: true, + } + }, + editingData: { + 0: { + id: 0, + name: '', + username: '', + sectionId: 0, + loginType: '', + isEditing: true, + isAddRow: true, + } + }, sectionId: null, }; @@ -60,7 +81,10 @@ export default function manageStudents(state=initialState, action) { if (action.type === SET_STUDENTS) { return { ...state, - studentData: action.studentData, + studentData: { + ...state.studentData, + ...action.studentData + }, }; } if (action.type === START_EDITING_STUDENT) { From f57d198fbc80bf365048d064f4a41f1ec4376582 Mon Sep 17 00:00:00 2001 From: Andrew Oberhardt Date: Tue, 20 Feb 2018 16:58:58 -0800 Subject: [PATCH 05/50] Fix summer workshop enrollment emails to display actual workshop subject instead of always CSP --- .../workshop_mailer/_teacher_enrollment_details.html.haml | 3 ++- .../workshop_mailer/teacher_enrollment_receipt.html.haml | 5 ++++- .../workshop_mailer/teacher_enrollment_reminder.html.haml | 3 ++- .../test/mailers/previews/pd_workshop_mailer_preview.rb | 8 ++++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/dashboard/app/views/pd/workshop_mailer/_teacher_enrollment_details.html.haml b/dashboard/app/views/pd/workshop_mailer/_teacher_enrollment_details.html.haml index 619cb484ccd39..03753bfc2ee9b 100644 --- a/dashboard/app/views/pd/workshop_mailer/_teacher_enrollment_details.html.haml +++ b/dashboard/app/views/pd/workshop_mailer/_teacher_enrollment_details.html.haml @@ -33,7 +33,8 @@ %ul %li It helps us understand who the population of teachers involved in our - Computer Science Principles (CSP) PD program are. + = @workshop.course + PD program are. %li Your students will be asked to take a similar survey as part of the course. This gives you a bit of a preview of the kinds of questions we diff --git a/dashboard/app/views/pd/workshop_mailer/teacher_enrollment_receipt.html.haml b/dashboard/app/views/pd/workshop_mailer/teacher_enrollment_receipt.html.haml index 1883291320556..fc184d69fef9b 100644 --- a/dashboard/app/views/pd/workshop_mailer/teacher_enrollment_receipt.html.haml +++ b/dashboard/app/views/pd/workshop_mailer/teacher_enrollment_receipt.html.haml @@ -10,7 +10,10 @@ - if @workshop.local_summer? %p Thanks for enrolling in - = "#{@workshop.organizer.name}’#{@workshop.organizer.name.ends_with?('s') ? '' : 's'} 5-day Summer workshop on the Code.org CS Principles curriculum." + = "#{@workshop.organizer.name}’#{@workshop.organizer.name.ends_with?('s') ? '' : 's'} " + 5-day Summer workshop on the Code.org + = @workshop.course + curriculum. - else %p Thanks for enrolling in Code.org’s diff --git a/dashboard/app/views/pd/workshop_mailer/teacher_enrollment_reminder.html.haml b/dashboard/app/views/pd/workshop_mailer/teacher_enrollment_reminder.html.haml index 4d7d96cc3d0a7..b95fe23b78ab3 100644 --- a/dashboard/app/views/pd/workshop_mailer/teacher_enrollment_reminder.html.haml +++ b/dashboard/app/views/pd/workshop_mailer/teacher_enrollment_reminder.html.haml @@ -10,7 +10,8 @@ - if @workshop.local_summer? %p This is a reminder about your upcoming 5-day Summer workshop on the Code.org - CS Principles curriculum. + = @workshop.course + curriculum. - else %p This is a reminder about your upcoming Code.org diff --git a/dashboard/test/mailers/previews/pd_workshop_mailer_preview.rb b/dashboard/test/mailers/previews/pd_workshop_mailer_preview.rb index 53be4fa0c3723..d2d9462ec5a04 100644 --- a/dashboard/test/mailers/previews/pd_workshop_mailer_preview.rb +++ b/dashboard/test/mailers/previews/pd_workshop_mailer_preview.rb @@ -18,6 +18,10 @@ def teacher_enrollment_receipt__csf mail :teacher_enrollment_receipt, Pd::Workshop::COURSE_CSF end + def teacher_enrollment_receipt__csd_summer_workshop + mail :teacher_enrollment_receipt, Pd::Workshop::COURSE_CSD, Pd::Workshop::SUBJECT_CSD_SUMMER_WORKSHOP + end + def teacher_enrollment_receipt__csp_summer_workshop mail :teacher_enrollment_receipt, Pd::Workshop::COURSE_CSP, Pd::Workshop::SUBJECT_CSP_SUMMER_WORKSHOP end @@ -38,6 +42,10 @@ def teacher_enrollment_reminder__csf mail :teacher_enrollment_reminder, Pd::Workshop::COURSE_CSF end + def teacher_enrollment_reminder__csd_summer_workshop + mail :teacher_enrollment_reminder, Pd::Workshop::COURSE_CSD, Pd::Workshop::SUBJECT_CSD_SUMMER_WORKSHOP + end + def teacher_enrollment_reminder__csp_summer_workshop mail :teacher_enrollment_reminder, Pd::Workshop::COURSE_CSP, Pd::Workshop::SUBJECT_CSP_SUMMER_WORKSHOP end From 76b203e454cbf7d359a6458594e2e6a63afd27c3 Mon Sep 17 00:00:00 2001 From: Josh Lory Date: Tue, 20 Feb 2018 17:10:09 -0800 Subject: [PATCH 06/50] Ensure assets (especially the timer sprite) are loaded before running --- apps/.eslintrc.js | 1 + apps/src/craft/designer/craft.js | 17 ++++++++++++++++- apps/test/integration/levelTests.js | 5 +++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/.eslintrc.js b/apps/.eslintrc.js index 76049b43aa7a8..16500bd14ec25 100644 --- a/apps/.eslintrc.js +++ b/apps/.eslintrc.js @@ -25,6 +25,7 @@ module.exports = { "Flappy": true, "Applab": true, "Calc": true, + "Craft": true, "Jigsaw": true, "$": true, "jQuery": true, diff --git a/apps/src/craft/designer/craft.js b/apps/src/craft/designer/craft.js index c4b6b5b7e4af9..880db89b590bd 100644 --- a/apps/src/craft/designer/craft.js +++ b/apps/src/craft/designer/craft.js @@ -146,6 +146,18 @@ function trySetLocalStorageItem(key, value) { } } +Craft.onAssetsLoaded = () => {}; + +function ensureAssetsLoaded() { + return new Promise(resolve => { + if (Craft.assetsLoaded) { + resolve(); + } else { + Craft.onAssetsLoaded = () => resolve(); + } + }); +} + /** * Initialize Blockly and the Craft app. Called on page load. */ @@ -283,6 +295,9 @@ Craft.init = function (config) { */ earlyLoadAssetPacks: Craft.earlyLoadAssetsForLevel(config.level.puzzle_number), afterAssetsLoaded: function () { + Craft.assetsLoaded = true; + Craft.onAssetsLoaded(); + // preload music after essential game asset downloads completely finished Craft.musicController.preload(); }, @@ -646,7 +661,7 @@ Craft.runButtonClick = function () { Blockly.mainBlockSpace.traceOn(true); studioApp().attempts++; - Craft.executeUserCode(); + ensureAssetsLoaded().then(() => Craft.executeUserCode()); if (Craft.level.freePlay && !studioApp().hideSource) { var finishBtnContainer = $('#right-button-cell'); diff --git a/apps/test/integration/levelTests.js b/apps/test/integration/levelTests.js index 33b7431475f11..977c700422af6 100644 --- a/apps/test/integration/levelTests.js +++ b/apps/test/integration/levelTests.js @@ -153,6 +153,11 @@ describe('Level tests', function () { if (window.Calc) { Calc.resetButtonClick(); } + + if (window.Craft) { + Craft.assetsLoaded = false; + Craft.onAssetsLoaded = function () {}; + } }); testCollectionUtils.getCollections().forEach(runTestCollection); From 470ef7ae24f1fa59ef7758c22e21caaf85c5fc30 Mon Sep 17 00:00:00 2001 From: Josh Lory Date: Tue, 20 Feb 2018 17:10:31 -0800 Subject: [PATCH 07/50] Add integration test for Designer level 1 --- .../levelSolutions/craft/designer1.js | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 apps/test/integration/levelSolutions/craft/designer1.js diff --git a/apps/test/integration/levelSolutions/craft/designer1.js b/apps/test/integration/levelSolutions/craft/designer1.js new file mode 100644 index 0000000000000..571663c644562 --- /dev/null +++ b/apps/test/integration/levelSolutions/craft/designer1.js @@ -0,0 +1,65 @@ +import { TestResults } from '@cdo/apps/constants'; + +const levelDef = { + isTestLevel: true, + isEventLevel: true, + groundPlane: ["grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "dirt", "dirtCoarse", "dirt", "dirtCoarse", "dirt", "grass", "grass", "grass", "grass", "grass", "dirt", "dirt", "dirt", "dirt", "dirt", "dirt", "dirtCoarse", "dirtCoarse", "grass", "grass", "grass", "dirt", "dirt", "dirt", "dirt", "dirt", "dirt", "dirt", "dirt", "dirt", "dirtCoarse", "dirtCoarse", "dirtCoarse", "dirt", "dirt", "dirt", "dirt", "dirt", "dirt", "dirt", "dirt", "grass", "grass", "grass", "dirtCoarse", "dirt", "dirtCoarse", "dirtCoarse", "dirt", "dirt", "dirt", "grass", "grass", "grass", "grass", "grass", "grass", "dirtCoarse", "dirtCoarse", "dirt", "dirtCoarse", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "dirt"], + groundDecorationPlane: ["", "", "tallGrass", "", "", "", "", "", "", "", "", "tallGrass", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "tallGrass", "", "", "", "", "", "", "", "", "", "", "tallGrass", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "flowerDandelion", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "tallGrass", "", "", "", "", "tallGrass", ""], + actionPlane: ["grass", "grass", "", "", "", "", "", "", "grass", "grass", "grass", "", "", "", "", "", "", "grass", "grass", "grass", "", "", "", "", "", "", "", "", "", "grass", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "grass", "", "", "", "", "", "", "", "", "", "grass", "grass", "grass", "", "", "", "", "", "", "grass"], + entities: [["chicken", 4, 4, 1]], + usePlayer: false, + levelVerificationTimeout: 5000, + timeoutResult: verificationAPI => ( + verificationAPI.getCommandExecutedCount("moveForward") >= 1 && + verificationAPI.getCommandExecutedCount("turn") >= 1 + ), + verificationFunction: verificationAPI => ( + !verificationAPI.isEntityTypeRunning("chicken") && + verificationAPI.getCommandExecutedCount("moveForward") >= 1 && + verificationAPI.getCommandExecutedCount("turn") >= 1 + ), +}; + +export default { + app: 'craft', + skinId: 'craft', + levelDefinition: levelDef, + tests: [ + { + description: 'Craft Designer 1 fail', + xml: ` + + + + + + + `, + expected: { + result: false, + testResult: TestResults.APP_SPECIFIC_FAIL, + }, + }, + { + description: 'Craft Designer 1 pass', + xml: ` + + + + + + + left + + + + + + `, + expected: { + result: true, + testResult: TestResults.ALL_PASS, + }, + }, + ], +}; From 933dcdab307fe97f9dba6fb123e67cd44b7e0433 Mon Sep 17 00:00:00 2001 From: Josh Lory Date: Tue, 20 Feb 2018 17:36:59 -0800 Subject: [PATCH 08/50] Add integration test for Designer level 2 --- .../levelSolutions/craft/designer1.js | 2 +- .../levelSolutions/craft/designer2.js | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 apps/test/integration/levelSolutions/craft/designer2.js diff --git a/apps/test/integration/levelSolutions/craft/designer1.js b/apps/test/integration/levelSolutions/craft/designer1.js index 571663c644562..6c352018d53bd 100644 --- a/apps/test/integration/levelSolutions/craft/designer1.js +++ b/apps/test/integration/levelSolutions/craft/designer1.js @@ -9,7 +9,7 @@ const levelDef = { entities: [["chicken", 4, 4, 1]], usePlayer: false, levelVerificationTimeout: 5000, - timeoutResult: verificationAPI => ( + timeoutVerificationFunction: verificationAPI => ( verificationAPI.getCommandExecutedCount("moveForward") >= 1 && verificationAPI.getCommandExecutedCount("turn") >= 1 ), diff --git a/apps/test/integration/levelSolutions/craft/designer2.js b/apps/test/integration/levelSolutions/craft/designer2.js new file mode 100644 index 0000000000000..ce3659fef95bd --- /dev/null +++ b/apps/test/integration/levelSolutions/craft/designer2.js @@ -0,0 +1,49 @@ +import { TestResults } from '@cdo/apps/constants'; + +const levelDef = { + isTestLevel: true, + isEventLevel: true, + groundPlane: ["grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "dirt", "dirtCoarse", "dirt", "dirtCoarse", "dirt", "grass", "grass", "grass", "grass", "grass", "dirt", "dirt", "dirt", "dirt", "dirt", "dirt", "dirtCoarse", "dirtCoarse", "grass", "grass", "grass", "dirt", "dirt", "dirt", "dirt", "dirt", "dirt", "dirt", "dirt", "dirt", "dirtCoarse", "dirtCoarse", "dirtCoarse", "dirt", "dirt", "dirt", "dirt", "dirt", "dirt", "dirt", "dirt", "grass", "grass", "grass", "dirtCoarse", "dirt", "dirtCoarse", "dirtCoarse", "dirt", "dirt", "dirt", "grass", "grass", "grass", "grass", "grass", "grass", "dirtCoarse", "dirtCoarse", "dirt", "dirtCoarse", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "grass", "dirt"], + groundDecorationPlane: ["", "", "tallGrass", "", "", "", "", "", "", "", "", "tallGrass", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "tallGrass", "", "", "", "", "", "", "", "", "", "", "tallGrass", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "flowerDandelion", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "tallGrass", "", "", "", "", "tallGrass", ""], + actionPlane: ["grass", "grass", "", "", "", "", "", "", "grass", "grass", "grass", "", "", "", "", "", "", "grass", "grass", "grass", "", "", "", "", "", "", "", "", "", "grass", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "grass", "", "", "", "", "", "", "", "", "", "grass", "grass", "grass", "", "", "", "", "", "", "grass"], + entities: [["chicken", 3, 3, 1], ["chicken", 6, 3, 1], ["chicken", 3, 6, 1], ["chicken", 6, 6, 1]], + usePlayer: false, + levelVerificationTimeout: 5000, + timeoutVerificationFunction: verificationAPI => ( + verificationAPI.getRepeatCommandExecutedCount("moveForward") > 0 + ), + verificationFunction: () => false, +}; + +export default { + app: 'craft', + skinId: 'craft', + levelDefinition: levelDef, + tests: [ + { + description: 'Craft Designer 2', + xml: ` + + + + + + + + + left + + + + + + + + `, + expected: { + result: true, + testResult: TestResults.ALL_PASS, + }, + }, + ], +}; From f35d300c6256d37195035dd32b46cec40d13657e Mon Sep 17 00:00:00 2001 From: Brad Buchanan Date: Tue, 20 Feb 2018 17:38:46 -0800 Subject: [PATCH 09/50] Ignore order when asserting levelgroup results match These two tests were consistently failing on my local machine, because the order of the inner results list of the levelgroup results was different, even though the tests attempt to prevent randomness from being a factor. I'm not sure what the root cause of that is, but the right solution is not to assert a particular order if we don't care what the resulting order is. I've extracted a couple of helper methods (addressing a TODO left by Asher) that assert the result sets match without enforcing a particular order. --- .../test/controllers/api_controller_test.rb | 58 ++++++++++++++----- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/dashboard/test/controllers/api_controller_test.rb b/dashboard/test/controllers/api_controller_test.rb index dff9d16b7ea50..978ad53eb2c3b 100644 --- a/dashboard/test/controllers/api_controller_test.rb +++ b/dashboard/test/controllers/api_controller_test.rb @@ -402,21 +402,14 @@ class ApiControllerTest < ActionController::TestCase } ] - response_body = JSON.parse(@response.body) - # Since the order of the levelgroup_results with the response isn't defined, we manually - # compare the actual and expected responses. - # TODO(asher): Generalize this to somewhere where it can be reused. - assert_equal 1, response_body.length - assert_equal ['stage', 'levelgroup_results'], response_body[0].keys - assert_equal expected_response[0]['stage'], response_body[0]['stage'] - assert_equal expected_response[0]['levelgroup_results'].count, - response_body[0]['levelgroup_results'].count - expected_response[0]['levelgroup_results'].each do |result| - assert response_body[0]['levelgroup_results'].include? result - end - response_body[0]['levelgroup_results'].each do |result| - assert expected_response[0]['levelgroup_results'].include? result - end + actual_response = JSON.parse(@response.body) + assert_equal 1, actual_response.length + assert_equal ['stage', 'levelgroup_results'], actual_response[0].keys + assert_equal expected_response[0]['stage'], actual_response[0]['stage'] + assert_levelgroup_results_match( + expected_response[0]['levelgroup_results'], + actual_response[0]['levelgroup_results'] + ) end test "should get surveys for section with script with single page anonymous level_group assessment" do @@ -541,7 +534,14 @@ class ApiControllerTest < ActionController::TestCase } ] - assert_equal expected_response, JSON.parse(@response.body) + actual_response = JSON.parse(@response.body) + assert_equal 1, actual_response.length + assert_equal ['stage', 'levelgroup_results'], actual_response[0].keys + assert_equal expected_response[0]['stage'], actual_response[0]['stage'] + assert_levelgroup_results_match( + expected_response[0]['levelgroup_results'], + actual_response[0]['levelgroup_results'] + ) end test "no anonymous survey data via assessment call" do @@ -1600,4 +1600,30 @@ class ApiControllerTest < ActionController::TestCase {method: "get", path: "/api/student_progress/2/15"} ) end + + def assert_levelgroup_results_match(expected_results, actual_results) + # Results may be in any order, so... + # Assert sets of equal length + assert_equal expected_results.count, actual_results.count + # Assert every expected result is found in the actual results + expected_results.each do |expected_result| + assert_contains_matching_result expected_result, actual_results + end + end + + def assert_contains_matching_result(expected_result, actual_results) + result_found = actual_results.any? do |actual| + actual['type'] == expected_result['type'] && + actual['question'] == expected_result['question'] && + actual['answer_texts'] == expected_result['answer_texts'] && + # Particular results may be in any order so... + # Assert sets of equal length + actual['results'].count == expected_result['results'].count && + # Assert every actual result shows up in expected results + actual['results'].all? do |actual_result| + expected_result['results'].include? actual_result + end + end + assert result_found, "Could not find result\n\n#{expected_result}\n\nin results\n\n#{actual_results.join("\n")}\n\n" + end end From 692debe504cfa19a6b0951a191d215c4db971264 Mon Sep 17 00:00:00 2001 From: Caley Brock Date: Tue, 20 Feb 2018 17:48:02 -0800 Subject: [PATCH 10/50] Add student saves to server --- .../ManageStudentsActionsCell.jsx | 14 ++- .../ManageStudentsGenderCell.jsx | 1 + .../manageStudents/manageStudentsRedux.js | 101 +++++++++++++++--- 3 files changed, 99 insertions(+), 17 deletions(-) diff --git a/apps/src/templates/manageStudents/ManageStudentsActionsCell.jsx b/apps/src/templates/manageStudents/ManageStudentsActionsCell.jsx index cb40fff099f89..f8181d2618ca7 100644 --- a/apps/src/templates/manageStudents/ManageStudentsActionsCell.jsx +++ b/apps/src/templates/manageStudents/ManageStudentsActionsCell.jsx @@ -4,7 +4,7 @@ import PopUpMenu, {MenuBreak} from "@cdo/apps/lib/ui/PopUpMenu"; import color from "../../util/color"; import FontAwesome from '../FontAwesome'; import Button from '../Button'; -import {startEditingStudent, cancelEditingStudent, removeStudent, saveStudent} from './manageStudentsRedux'; +import {startEditingStudent, cancelEditingStudent, removeStudent, saveStudent, addStudent} from './manageStudentsRedux'; import {connect} from 'react-redux'; import BaseDialog from '../BaseDialog'; import DialogFooter from "../teacherDashboard/DialogFooter"; @@ -19,7 +19,7 @@ const styles = { class ManageStudentActionsCell extends Component { static propTypes = { id: PropTypes.number.isRequired, - sectionId: PropTypes.number.isRequired, + sectionId: PropTypes.number, isEditing: PropTypes.bool, isSaving: PropTypes.bool, disableSaving: PropTypes.bool, @@ -29,6 +29,7 @@ class ManageStudentActionsCell extends Component { cancelEditingStudent: PropTypes.func, removeStudent: PropTypes.func, saveStudent: PropTypes.func, + addStudent: PropTypes.func, }; state = { @@ -72,6 +73,10 @@ class ManageStudentActionsCell extends Component { this.props.saveStudent(this.props.id); }; + onAdd = () => { + this.props.addStudent(this.props.id); + }; + render() { return (
@@ -110,7 +115,7 @@ class ManageStudentActionsCell extends Component { {this.props.isAddRow &&
diff --git a/apps/src/templates/manageStudents/ManageStudentsNameCell.jsx b/apps/src/templates/manageStudents/ManageStudentsNameCell.jsx index af845efeb5757..32696384b0ae5 100644 --- a/apps/src/templates/manageStudents/ManageStudentsNameCell.jsx +++ b/apps/src/templates/manageStudents/ManageStudentsNameCell.jsx @@ -43,7 +43,9 @@ class ManageStudentNameCell extends Component { {this.props.isEditing &&
-
{this.props.editedValue.length === 0 ? 'required' : ''}
+
+ {this.props.editedValue.length === 0 ? i18n.required() : ''} +
}
diff --git a/apps/src/templates/manageStudents/ManageStudentsTable.jsx b/apps/src/templates/manageStudents/ManageStudentsTable.jsx index ccc7b42ccfa43..958fed5a83c54 100644 --- a/apps/src/templates/manageStudents/ManageStudentsTable.jsx +++ b/apps/src/templates/manageStudents/ManageStudentsTable.jsx @@ -62,7 +62,7 @@ const passwordFormatter = (loginType, {rowData}) => { } {rowData.isEditing &&
- Auto-generated + {i18n.autoGenerated()}
} From fc21ca89bd7de9b5d7f80c9400b3a77c14e6f67f Mon Sep 17 00:00:00 2001 From: David Bailey Date: Wed, 21 Feb 2018 16:57:08 -0800 Subject: [PATCH 36/50] bump study_group to animation_no_load_v3 [ci skip] --- apps/src/gamelab/ErrorDialogStack.jsx | 4 ++-- apps/src/gamelab/animationListModule.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/src/gamelab/ErrorDialogStack.jsx b/apps/src/gamelab/ErrorDialogStack.jsx index 785466e2368d1..85c93279abed9 100644 --- a/apps/src/gamelab/ErrorDialogStack.jsx +++ b/apps/src/gamelab/ErrorDialogStack.jsx @@ -30,7 +30,7 @@ class ErrorDialogStack extends React.Component { 'analysis-events', { study: 'animation_no_load', - study_group: 'animation_no_load_v2', + study_group: 'animation_no_load_v3', event: 'delete_selected', project_id: getCurrentId(), data_json: JSON.stringify({'version': this.props.animationList.propsByKey[key].version, @@ -47,7 +47,7 @@ class ErrorDialogStack extends React.Component { 'analysis-events', { study: 'animation_no_load', - study_group: 'animation_no_load_v2', + study_group: 'animation_no_load_v3', event: 'reload_selected', project_id: getCurrentId(), data_json: JSON.stringify({'version': this.props.animationList.propsByKey[key].version, diff --git a/apps/src/gamelab/animationListModule.js b/apps/src/gamelab/animationListModule.js index dc5ad0aaf34ba..e3655bf6dc6fd 100644 --- a/apps/src/gamelab/animationListModule.js +++ b/apps/src/gamelab/animationListModule.js @@ -570,7 +570,7 @@ function loadAnimationFromSource(key, callback) { 'analysis-events', { study: 'animation_no_load', - study_group: 'animation_no_load_v2', + study_group: 'animation_no_load_v3', event: isOwner() ? 'animation_not_loaded_owner' : 'animation_not_loaded_viewer', project_id: getCurrentId(), data_json: JSON.stringify({'sourceUrl': sourceUrl, 'version': state.propsByKey[key].version, From 4e531dba1eff615450eacb9fa871ad0fd9c7ba4b Mon Sep 17 00:00:00 2001 From: Caley Brock Date: Wed, 21 Feb 2018 17:15:23 -0800 Subject: [PATCH 37/50] Update text based on new spec --- apps/i18n/common/en_us.json | 5 ++++- .../manageStudents/ManageStudentsActionsCell.jsx | 10 ++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/i18n/common/en_us.json b/apps/i18n/common/en_us.json index 8612275934d6a..6c40e41be0d68 100644 --- a/apps/i18n/common/en_us.json +++ b/apps/i18n/common/en_us.json @@ -798,7 +798,10 @@ "removeFromClassGallery": "Remove from Class Gallery", "removeFromPublicGallery": "Remove from Public Gallery", "removeStudent": "Remove student", - "removeStudentConfirm": "Are you sure you want to remove this student?", + "removeStudentConfirm1": "If your student currently logs in through a secret picture or secret pair of words, the student may no longer be able to log into their account if you remove them from your section. If this is the case, please give your student a chance to keep using their Code.org account by letting them create a personal login.", + "removeStudentConfirm2": "Send home these instructions on how to create a personal login.", + "removeStudentConfirm3": "Give them at least a few days to follow these instructions before you remove them.", + "removeStudentHeader": "Are you sure you want to remove this student?", "rename": "Rename", "repeat": "repeat", "replayButton": "Replay", diff --git a/apps/src/templates/manageStudents/ManageStudentsActionsCell.jsx b/apps/src/templates/manageStudents/ManageStudentsActionsCell.jsx index b4aafef6254d7..af99419b445fc 100644 --- a/apps/src/templates/manageStudents/ManageStudentsActionsCell.jsx +++ b/apps/src/templates/manageStudents/ManageStudentsActionsCell.jsx @@ -128,8 +128,14 @@ class ManageStudentActionsCell extends Component { isOpen={this.state.deleting} style={{paddingLeft: 20, paddingRight: 20, paddingBottom: 20}} > -

{i18n.removeStudent()}

-
{i18n.removeStudentConfirm()}
+

{i18n.removeStudentHeader()}

+
+ {i18n.removeStudentConfirm1() + ' '} + + {i18n.removeStudentConfirm2()} + + {' ' + i18n.removeStudentConfirm3()} +