Skip to content

Commit e0873ef

Browse files
Merge pull request #73309 from code-dot-org/staging
2 parents f68964d + 536f5ac commit e0873ef

273 files changed

Lines changed: 8599 additions & 1726 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/script/generateSharedConstants.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ def main
8383
DEFAULT_LOCALE
8484
LOCALE_FALLBACKS
8585
LOCALIZE_TO_I18N_LOCALES
86+
GLOBAL_EDITION_DEFAULT_REGION
8687
GLOBAL_EDITION_EXCLUDED_PATHS
8788
ARTIST_AUTORUN_OPTIONS
8889
LEVEL_KIND

apps/src/levelbuilder/lesson-editor/AddLevelDialogTop.jsx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import SegmentedButtons from '@code-dot-org/component-library/segmentedButtons';
12
import $ from 'jquery';
23
import PropTypes from 'prop-types';
34
import queryString from 'query-string';
@@ -9,8 +10,6 @@ import FontAwesome from '@cdo/apps/legacySharedComponents/FontAwesome';
910
import AddLevelFilters from '@cdo/apps/levelbuilder/lesson-editor/AddLevelFilters';
1011
import AddLevelTable from '@cdo/apps/levelbuilder/lesson-editor/AddLevelTable';
1112
import CreateNewLevelInputs from '@cdo/apps/levelbuilder/lesson-editor/CreateNewLevelInputs';
12-
import ToggleGroup from '@cdo/apps/templates/ToggleGroup';
13-
1413
function AddLevelDialogTop(props) {
1514
const [methodOfAddingLevel, setMethodOfAddingLevel] = useState('Find Level');
1615
const [levels, setLevels] = useState([]);
@@ -69,17 +68,17 @@ function AddLevelDialogTop(props) {
6968
<div>
7069
{!loadingLevels && (
7170
<div style={styles.topArea}>
72-
<ToggleGroup
73-
selected={methodOfAddingLevel}
74-
onChange={value => setMethodOfAddingLevel(value)}
75-
>
76-
<button type="button" value={'Find Level'}>
77-
Find Level
78-
</button>
79-
<button type="button" value={'Create New Level'}>
80-
Create New Level
81-
</button>
82-
</ToggleGroup>
71+
<div style={styles.toggle}>
72+
<SegmentedButtons
73+
selectedButtonValue={methodOfAddingLevel}
74+
onChange={value => setMethodOfAddingLevel(value)}
75+
size="xs"
76+
buttons={[
77+
{value: 'Find Level', label: 'Find Level'},
78+
{value: 'Create New Level', label: 'Create New Level'},
79+
]}
80+
/>
81+
</div>
8382
{methodOfAddingLevel === 'Find Level' && (
8483
<div style={styles.filtersAndLevels}>
8584
<AddLevelFilters
@@ -146,6 +145,9 @@ const styles = {
146145
flexDirection: 'column',
147146
margin: 15,
148147
},
148+
toggle: {
149+
marginBottom: 5,
150+
},
149151
bottomArea: {
150152
display: 'flex',
151153
flexDirection: 'column',

apps/src/util/globalEdition.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {Regions} from '@cdo/generated-scripts/globalRegionConstants';
2+
import {GlobalEditionDefaultRegion} from '@cdo/generated-scripts/sharedConstants';
23

34
interface RegionConfigurationObject {
45
[key: string]: object | boolean;
@@ -11,7 +12,6 @@ export interface RegionConfigurationPageObject {
1112

1213
export interface RegionConfiguration {
1314
locales?: readonly string[];
14-
locale_lock?: boolean;
1515
countries?: readonly string[];
1616
header?: RegionConfigurationObject;
1717
footer?: RegionConfigurationObject;
@@ -28,4 +28,5 @@ export const getGlobalEditionRegion = () =>
2828
* This returns the current region's configuration data.
2929
*/
3030
export const currentGlobalConfiguration: () => RegionConfiguration = () =>
31-
Regions[getGlobalEditionRegion() as keyof typeof Regions] || Regions.root;
31+
Regions[getGlobalEditionRegion() as keyof typeof Regions] ||
32+
Regions[GlobalEditionDefaultRegion];

apps/test/unit/levelbuilder/lesson-editor/AddLevelDialogTopTest.jsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import SegmentedButtons from '@code-dot-org/component-library/segmentedButtons';
12
import {isolateComponent} from 'isolate-react';
23
import React from 'react';
34
import sinon from 'sinon'; // eslint-disable-line no-restricted-imports
@@ -24,7 +25,7 @@ describe('AddLevelDialogTop', () => {
2425
{
2526
conceptDifficulty: '',
2627
concepts: '',
27-
icon: 'fa-solid fa-list-ul',
28+
icon: 'fa fa-list-ul',
2829
id: 22300,
2930
is_concept_level: false,
3031
kind: 'puzzle',
@@ -51,7 +52,7 @@ describe('AddLevelDialogTop', () => {
5152
const wrapper = isolateComponent(<AddLevelDialogTop {...defaultProps} />);
5253
server.respond();
5354

54-
expect(wrapper.findOne('Connect(ToggleGroup)'));
55+
expect(wrapper.findOne(SegmentedButtons));
5556
expect(wrapper.findOne('Connect(AddLevelFilters)'));
5657
expect(wrapper.findOne('AddLevelTable'));
5758
expect(!wrapper.exists('FontAwesome')); // no spinner
@@ -64,7 +65,7 @@ describe('AddLevelDialogTop', () => {
6465

6566
// Without using setLevels this test has no level data
6667

67-
expect(!wrapper.exists('ToggleGroup'));
68+
expect(!wrapper.exists(SegmentedButtons));
6869
expect(!wrapper.exists('Connect(AddLevelFilters)'));
6970
expect(!wrapper.exists('AddLevelTable'));
7071
expect(wrapper.exists('FontAwesome'));

apps/test/unit/util/globalEditionTest.tsx

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,49 +2,61 @@ import {
22
getGlobalEditionRegion,
33
currentGlobalConfiguration,
44
} from '@cdo/apps/util/globalEdition';
5+
import {Regions} from '@cdo/generated-scripts/globalRegionConstants';
6+
import {GlobalEditionDefaultRegion} from '@cdo/generated-scripts/sharedConstants';
57

6-
const setGlobalEditionRegion = (region?: string) => {
8+
const setGlobalEditionRegion = (region: string) => {
79
document.documentElement.dataset.geRegion = region;
810
};
911

12+
const clearGlobalEditionRegion = () => {
13+
delete document.documentElement.dataset.geRegion;
14+
};
15+
1016
describe('globalEdition', () => {
17+
const defaultConfig = Regions[GlobalEditionDefaultRegion];
18+
19+
afterEach(() => {
20+
clearGlobalEditionRegion();
21+
});
22+
23+
describe('default configuration', () => {
24+
it('should be present', () => {
25+
expect(defaultConfig).toEqual(expect.any(Object));
26+
expect(defaultConfig.header).toEqual(expect.any(Object));
27+
expect(defaultConfig.footer).toEqual(expect.any(Object));
28+
});
29+
});
30+
1131
describe('getGlobalEditionRegion', () => {
1232
it('should return the region given in the embedded html data in spite of the location path', () => {
1333
setGlobalEditionRegion('narnia');
1434
expect(getGlobalEditionRegion()).toBe('narnia');
1535
});
36+
37+
it('should return null when no region is given in the embedded html data', () => {
38+
clearGlobalEditionRegion();
39+
expect(getGlobalEditionRegion()).toBeNull();
40+
});
1641
});
1742

1843
describe('currentGlobalConfiguration', () => {
19-
it('should return the root region configuration when the region is unknown', () => {
44+
it('should return the default region configuration when the region is unknown', () => {
2045
setGlobalEditionRegion('bogusweasel');
21-
// Should match config/global_editions/root.yml
22-
expect(currentGlobalConfiguration().locales).toEqual(['en-US']);
46+
// Should match config/global_editions/us.yml
47+
expect(currentGlobalConfiguration()).toEqual(defaultConfig);
2348
});
2449

25-
it('should return the root region configuration when the region is not in the location', () => {
26-
setGlobalEditionRegion();
27-
// Should match config/global_editions/root.yml
28-
expect(currentGlobalConfiguration().locales).toEqual(['en-US']);
50+
it('should return the default region configuration when no region is given in the embedded html data', () => {
51+
clearGlobalEditionRegion();
52+
// Should match config/global_editions/us.yml
53+
expect(currentGlobalConfiguration()).toEqual(defaultConfig);
2954
});
3055

3156
it('should return the region configuration for the current region', () => {
3257
setGlobalEditionRegion('fa');
3358
// Should match config/global_editions/fa.yml
3459
expect(currentGlobalConfiguration().locales).toEqual(['fa-IR']);
35-
36-
setGlobalEditionRegion('in');
37-
// Should match config/global_editions/in.yml
38-
expect(currentGlobalConfiguration().locales).toEqual([
39-
'en-IN',
40-
'hi-IN',
41-
'ta-IN',
42-
'te-IN',
43-
'mr-IN',
44-
'kn-IN',
45-
'or-IN',
46-
'gu-IN',
47-
]);
4860
});
4961
});
5062
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Zero ETL Materialized View templates
2+
3+
This directory holds one generated SQL ERB template per Redshift materialized view in the Zero ETL
4+
analytics export. For each model that declares `export_to_analytics`, there are up to two files:
5+
6+
- `<table>.sql.erb` — the **non-PII** view, created as `learning_platform_<env>.<table>`.
7+
- `<table>_pii.sql.erb` — the **PII** view, created as `learning_platform_<env>_pii.<table>`.
8+
9+
The `<%=environment_type%>` ERB placeholders are filled in at provision time (`test`, `production`),
10+
so one template serves every environment. The materialized views read the Learning Platform MySQL
11+
data that Zero ETL continuously replicates into Redshift.
12+
13+
## These files are generated — do not edit them by hand
14+
15+
They are produced by `Cdo::Aws::Redshift::MaterializedViewManager.generate_all_ddl_templates` from
16+
the current ActiveRecord models. Regeneration is triggered:
17+
18+
- automatically by `rake db:migrate` (development), so a migration that reshapes an exported table
19+
shows up here as a template diff;
20+
- on demand by `bundle exec rake analytics_export:generate_materialized_view_templates`;
21+
- and at the start of `analytics_export:provision_materialized_views`.
22+
23+
To change a view, change its source — a Rails migration (columns) or the model's `data_classification`
24+
declaration (which columns land in which view) — then regenerate. Editing a `.sql.erb` directly will
25+
be overwritten on the next regeneration.
26+
27+
Which columns appear is governed by each column's data classification:
28+
29+
- non-PII view (`learning_platform_<env>`): `:public` and `:confidential` columns.
30+
- PII view (`learning_platform_<env>_pii`): `:public`, `:confidential`, and `:restricted` columns.
31+
- `:highly_restricted` columns appear in neither view.
32+
33+
A model whose non-PII (or PII) projection has no columns produces no corresponding template — and a
34+
template that no longer maps to any view is pruned during regeneration, so a deleted file here means
35+
that view no longer exists.
36+
37+
## Committing these files does NOT change Redshift
38+
39+
Merging a change to this directory does **not** update the materialized views provisioned in the
40+
Redshift cluster. These templates only **record the pending change** so it is visible in code review
41+
and git history. Redshift cannot `ALTER` a materialized view, so any column or classification change
42+
requires a coordinated `DROP` + `CREATE` of the affected views — which drops and recomputes them and
43+
can briefly interrupt the data team's downstream dbt models and reports. We therefore never deploy
44+
these changes automatically.
45+
46+
## Deploying the change to Redshift
47+
48+
Before (or shortly after) merging a Pull Request that changes this directory, coordinate with the
49+
data analytics ("RED") team and the Infrastructure Engineering team to schedule the rebuild. An
50+
Infrastructure Engineer with admin AWS credentials then runs the provision task on their workstation:
51+
52+
VALIDATE (preview — submits nothing to Redshift):
53+
54+
code-dot-org $ export AWS_PROFILE=codeorg-admin
55+
code-dot-org/dashboard $ DRY_RUN=1 bundle exec rake 'analytics_export:provision_materialized_views[production]'
56+
# Review the Add / Update / Drop plan. It should list only the views changed in this branch.
57+
58+
EXECUTE (DROP + CREATE the changed views, drop orphans):
59+
60+
code-dot-org/dashboard $ bundle exec rake 'analytics_export:provision_materialized_views[production]'
61+
62+
`provision_materialized_views` rebuilds only the views whose DDL actually changed (it compares a hash
63+
stored on each view) and drops views no longer backed by a model. After it completes, the views are
64+
empty until refreshed; `analytics_export:refresh_materialized_views[production]` (or the daily export
65+
job) populates them. Use `analytics_export:materialized_view_status[production]` to check state.
66+
67+
See `dashboard/lib/tasks/analytics_exportable.rake` for the full task list and
68+
`lib/cdo/aws/redshift/materialized_view_manager.rb` for the generator and provisioner.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
-- GENERATED FILE -- do not edit by hand.
2+
-- Regenerated from the ActiveRecord model by `rake db:migrate` and by
3+
-- `rake analytics_export:generate_materialized_view_templates`; edits here are overwritten.
4+
-- Committing a change to this file does NOT update the materialized view in Redshift. That
5+
-- needs a coordinated DROP/CREATE -- see aws/redshift/zeroetl_materialized_views/README.md
6+
-- and coordinate with the data (RED) and Infrastructure Engineering teams.
7+
CREATE MATERIALIZED VIEW learning_platform_<%=environment_type%>_pii.ai_interaction_feedbacks
8+
BACKUP NO
9+
DISTSTYLE KEY DISTKEY ("id")
10+
AUTO REFRESH NO
11+
AS SELECT
12+
"id",
13+
"user_id",
14+
"level_id",
15+
"script_id",
16+
"thumbs_up",
17+
"school_year",
18+
"metadata",
19+
"ai_interaction_type",
20+
"ai_interaction_id",
21+
"created_at",
22+
"updated_at"
23+
FROM <%=environment_type%>_learningplatform_mysql_zeroetl.dashboard_<%=environment_type%>.ai_interaction_feedbacks;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
-- GENERATED FILE -- do not edit by hand.
2+
-- Regenerated from the ActiveRecord model by `rake db:migrate` and by
3+
-- `rake analytics_export:generate_materialized_view_templates`; edits here are overwritten.
4+
-- Committing a change to this file does NOT update the materialized view in Redshift. That
5+
-- needs a coordinated DROP/CREATE -- see aws/redshift/zeroetl_materialized_views/README.md
6+
-- and coordinate with the data (RED) and Infrastructure Engineering teams.
7+
CREATE MATERIALIZED VIEW learning_platform_<%=environment_type%>_pii.aichat_events
8+
BACKUP NO
9+
DISTSTYLE KEY DISTKEY ("id")
10+
AUTO REFRESH NO
11+
AS SELECT
12+
"id",
13+
"user_id",
14+
"level_id",
15+
"script_id",
16+
"project_id",
17+
"aichat_event",
18+
"created_at",
19+
"updated_at",
20+
"request_id",
21+
"lesson_id"
22+
FROM <%=environment_type%>_learningplatform_mysql_zeroetl.dashboard_<%=environment_type%>.aichat_events;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
-- GENERATED FILE -- do not edit by hand.
2+
-- Regenerated from the ActiveRecord model by `rake db:migrate` and by
3+
-- `rake analytics_export:generate_materialized_view_templates`; edits here are overwritten.
4+
-- Committing a change to this file does NOT update the materialized view in Redshift. That
5+
-- needs a coordinated DROP/CREATE -- see aws/redshift/zeroetl_materialized_views/README.md
6+
-- and coordinate with the data (RED) and Infrastructure Engineering teams.
7+
CREATE MATERIALIZED VIEW learning_platform_<%=environment_type%>_pii.aichat_requests
8+
BACKUP NO
9+
DISTSTYLE KEY DISTKEY ("id")
10+
AUTO REFRESH NO
11+
AS SELECT
12+
"id",
13+
"user_id",
14+
"level_id",
15+
"script_id",
16+
"project_id",
17+
"model_customizations",
18+
"stored_messages",
19+
"new_message",
20+
"execution_status",
21+
"response",
22+
"created_at",
23+
"updated_at"
24+
FROM <%=environment_type%>_learningplatform_mysql_zeroetl.dashboard_<%=environment_type%>.aichat_requests;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-- GENERATED FILE -- do not edit by hand.
2+
-- Regenerated from the ActiveRecord model by `rake db:migrate` and by
3+
-- `rake analytics_export:generate_materialized_view_templates`; edits here are overwritten.
4+
-- Committing a change to this file does NOT update the materialized view in Redshift. That
5+
-- needs a coordinated DROP/CREATE -- see aws/redshift/zeroetl_materialized_views/README.md
6+
-- and coordinate with the data (RED) and Infrastructure Engineering teams.
7+
CREATE MATERIALIZED VIEW learning_platform_<%=environment_type%>.aichat_sessions
8+
BACKUP NO
9+
DISTSTYLE KEY DISTKEY ("id")
10+
AUTO REFRESH NO
11+
AS SELECT
12+
"id",
13+
"user_id",
14+
"level_id",
15+
"script_id",
16+
"project_id",
17+
"model_customizations",
18+
"messages",
19+
"created_at",
20+
"updated_at"
21+
FROM <%=environment_type%>_learningplatform_mysql_zeroetl.dashboard_<%=environment_type%>.aichat_sessions;

0 commit comments

Comments
 (0)