diff --git a/apps/package.json b/apps/package.json index 74ef5f5443e56..21b2c7b8300e8 100644 --- a/apps/package.json +++ b/apps/package.json @@ -42,7 +42,7 @@ "@code-dot-org/artist": "0.2.1", "@code-dot-org/blockly": "3.4.2", "@code-dot-org/bramble": "0.1.26", - "@code-dot-org/craft": "0.2.1", + "@code-dot-org/craft": "0.2.2", "@code-dot-org/dance-party": "0.0.42", "@code-dot-org/johnny-five": "0.11.1-cdo.2", "@code-dot-org/js-interpreter-tyrant": "0.2.2", @@ -217,6 +217,7 @@ }, "dependencies": { "@code-dot-org/js-interpreter": "^1.3.8", + "@code-dot-org/snack-sdk": "2.2.0-apkurl-v2", "crypto-js": "^3.1.9-1", "details-element-polyfill": "https://github.com/javan/details-element-polyfill", "filesaver.js": "0.2.0", @@ -232,7 +233,6 @@ "react-portal": "^3.0.0", "react-router-dom": "^4.3.1", "selectize": "^0.12.4", - "snack-sdk": "2.0.0", "wgxpath": "^1.2.0", "whatwg-fetch": "^2.0.3" } diff --git a/apps/src/applab/Exporter.js b/apps/src/applab/Exporter.js index 502223009c4fe..eb15d841bf507 100644 --- a/apps/src/applab/Exporter.js +++ b/apps/src/applab/Exporter.js @@ -3,7 +3,7 @@ import $ from 'jquery'; import _ from 'lodash'; import JSZip from 'jszip'; import {saveAs} from 'filesaver.js'; -import {SnackSession} from 'snack-sdk'; +import {SnackSession} from '@code-dot-org/snack-sdk'; import * as applabConstants from './constants'; import * as assetPrefix from '../assetManagement/assetPrefix'; @@ -27,6 +27,7 @@ import exportExpoSplashPng from '../templates/export/expo/splash.png'; import logToCloud from '../logToCloud'; import {getAppOptions} from '@cdo/apps/code-studio/initApp/loadApp'; import project from '@cdo/apps/code-studio/initApp/project'; +import {EXPO_SESSION_SECRET} from '../constants'; // This whitelist determines which appOptions properties // will get exported with the applab app, appearing in the @@ -550,10 +551,10 @@ export default { }); }, - async exportApp(appName, code, levelHtml, suppliedExpoOpts) { + async exportApp(appName, code, levelHtml, suppliedExpoOpts, config) { const expoOpts = suppliedExpoOpts || {}; if (expoOpts.mode === 'expoPublish') { - return await this.publishToExpo(appName, code, levelHtml); + return await this.publishToExpo(appName, code, levelHtml, config); } return this.exportAppToZip( appName, @@ -597,7 +598,25 @@ export default { return exportExpoPackagedFilesEjs({entries}); }, - async publishToExpo(appName, code, levelHtml) { + async generateExpoApk(snackId, config) { + const session = new SnackSession({ + sessionId: `${getEnvironmentPrefix()}-${project.getCurrentId()}`, + name: `project-${project.getCurrentId()}`, + sdkVersion: '31.0.0', + snackId, + user: { + sessionSecret: config.expoSession || EXPO_SESSION_SECRET + } + }); + + const appJson = session.generateAppJson(); + + const artifactUrl = await session.getApkUrlAsync(appJson); + + return artifactUrl; + }, + + async publishToExpo(appName, code, levelHtml, config) { const {css, outerHTML} = transformLevelHtml(levelHtml); const fontAwesomeCSS = exportFontAwesomeCssEjs({ fontPath: fontAwesomeWOFFPath @@ -642,7 +661,10 @@ export default { sessionId: `${getEnvironmentPrefix()}-${project.getCurrentId()}`, files, name: project.getCurrentName(), - sdkVersion: '31.0.0' + sdkVersion: '31.0.0', + user: { + sessionSecret: config.expoSession || EXPO_SESSION_SECRET + } }); // Important that index.html comes first: @@ -707,9 +729,13 @@ export default { await session.sendCodeAsync(files); const saveResult = await session.saveAsync(); - const expoURL = `exp://expo.io/@snack/${saveResult.id}`; + const expoUri = `exp://expo.io/${saveResult.id}`; + const expoSnackId = saveResult.id; - return expoURL; + return { + expoUri, + expoSnackId + }; } }; diff --git a/apps/src/applab/applab.js b/apps/src/applab/applab.js index c415a7f25e678..1b23db87bd492 100644 --- a/apps/src/applab/applab.js +++ b/apps/src/applab/applab.js @@ -825,12 +825,19 @@ Applab.exportApp = function(expoOpts) { Applab.runButtonClick(); var html = document.getElementById('divApplab').outerHTML; studioApp().resetButtonClick(); + + const {mode, expoSnackId} = expoOpts || {}; + if (mode === 'expoGenerateApk') { + return Exporter.generateExpoApk(expoSnackId, studioApp().config); + } + return Exporter.exportApp( // TODO: find another way to get this info that doesn't rely on globals. (window.dashboard && window.dashboard.project.getCurrentName()) || 'my-app', studioApp().editor.getValue(), html, - expoOpts + expoOpts, + studioApp().config ); }; diff --git a/apps/src/code-studio/components/AdvancedShareOptions.jsx b/apps/src/code-studio/components/AdvancedShareOptions.jsx index e2e220360cf48..9a4dcd895a629 100644 --- a/apps/src/code-studio/components/AdvancedShareOptions.jsx +++ b/apps/src/code-studio/components/AdvancedShareOptions.jsx @@ -62,6 +62,10 @@ const style = { expoButtonLast: { marginRight: 0 }, + expoButtonApk: { + marginBottom: 10, + maxWidth: 280 + }, expoContainer: { display: 'flex', flexDirection: 'column' @@ -141,7 +145,8 @@ class AdvancedShareOptions extends React.Component { try { await this.props.exportApp({mode: 'expoZip'}); this.setState({ - exportingExpo: null + exportingExpo: null, + exportExpoError: null }); } catch (e) { this.setState({ @@ -154,20 +159,48 @@ class AdvancedShareOptions extends React.Component { publishExpoExport = async () => { this.setState({exportingExpo: 'publish'}); try { - const expoUri = await this.props.exportApp({mode: 'expoPublish'}); + const {expoUri, expoSnackId} = await this.props.exportApp({ + mode: 'expoPublish' + }); this.setState({ exportingExpo: null, - expoUri + exportExpoError: null, + expoUri, + expoSnackId }); } catch (e) { this.setState({ exportingExpo: null, + expoUri: null, + expoSnackId: null, exportExpoError: 'Failed to publish project to Expo. Please try again later.' }); } }; + generateExpoApk = async () => { + const {expoSnackId} = this.state; + this.setState({generatingExpoApk: true}); + try { + const expoApkUri = await this.props.exportApp({ + mode: 'expoGenerateApk', + expoSnackId + }); + this.setState({ + generatingExpoApk: false, + generatingExpoApkError: null, + expoApkUri + }); + } catch (e) { + this.setState({ + generatingExpoApk: false, + generatingExpoApkError: + 'Failed to create Android app. Please try again later.' + }); + } + }; + renderEmbedTab() { let url = `${this.props.shareUrl}/embed`; if (this.state.embedWithoutCode) { @@ -241,7 +274,12 @@ class AdvancedShareOptions extends React.Component { }; renderExportExpoTab() { - const {expoUri, exportedExpoZip} = this.state; + const { + expoUri, + exportedExpoZip, + expoApkUri, + generatingExpoApk + } = this.state; const exportSpinner = this.state.exportingExpo === 'zip' ? ( @@ -250,10 +288,21 @@ class AdvancedShareOptions extends React.Component { this.state.exportingExpo === 'publish' ? ( ) : null; + const generateApkSpinner = generatingExpoApk ? ( + + ) : null; // TODO: Make this use a nice UI component from somewhere. const alert = this.state.exportExpoError ? (
{this.state.exportExpoError}
) : null; + const apkAlert = this.state.generatingExpoApkError ? ( +
{this.state.generatingExpoApkError}
+ ) : null; + const apkStatusString = expoApkUri + ? 'App created successfully' + : generatingExpoApk + ? 'Creating app...' + : '(This will take 5-10 minutes)'; return (
@@ -307,6 +356,31 @@ class AdvancedShareOptions extends React.Component { value={expoUri} style={style.expoInput} /> + +

{apkStatusString}

+ {!!expoApkUri && ( +
+

+ Send this URL to an Android phone: +

+
+ )} + {!!expoApkUri && ( + + )} + {apkAlert}
diff --git a/apps/src/code-studio/pd/fit_weekend_registration/Releases.jsx b/apps/src/code-studio/pd/fit_weekend_registration/Releases.jsx index 991c90439e99f..3973013e15d08 100644 --- a/apps/src/code-studio/pd/fit_weekend_registration/Releases.jsx +++ b/apps/src/code-studio/pd/fit_weekend_registration/Releases.jsx @@ -32,7 +32,7 @@ export default class Releases extends LabeledFormComponent { photo release. - {this.singleCheckboxFor('photoRelease')} + {this.singleCheckboxFor('photoRelease', {required: false})} Please read this{' '} Resources: + # Stack-specific IAM permissions applied to both daemon and frontends. + CDOPolicy: + Type: AWS::IAM::ManagedPolicy + Properties: + Description: !Sub "Application permissions for ${AWS::StackName}." + PolicyDocument: + Version: 2012-10-17 + Statement: + # Read-only access to bootstrap scripts. + - Effect: Allow + Action: 's3:GetObject' + Resource: 'arn:aws:s3:::cdo-dist/<%=environment%>/*' + # Instance-bootstrap CloudFormation hook. + - Effect: Allow + Action: 'cloudformation:SignalResource' + Resource: !Ref AWS::StackId + # Forward syslog to CloudWatch Logs via cdo-cloudwatch-logger. + - Effect: Allow + Action: + - 'logs:CreateLogGroup' + - 'logs:CreateLogStream' + - 'logs:PutRetentionPolicy' + - 'logs:PutLogEvents' + Resource: + - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:<%=environment%>-syslog" + - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:<%=environment%>-syslog:log-stream:*" + - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:${AWS::StackName}" + - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:${AWS::StackName}:log-stream:*" + # Put custom metrics to CloudWatch. + - Effect: Allow + Action: 'cloudwatch:PutMetricData' + Resource: '*' + # Frontend-bootstrap lifecycle hooks. + - Effect: Allow + Action: 'autoscaling:CompleteLifecycleAction' + Resource: !Sub "arn:aws:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*:autoScalingGroupName/Frontends-${AWS::StackName}" + # Read/Write DCDO and Gatekeeper tables. + # TODO: Import resources into stack. + - Effect: Allow + Action: + - 'dynamodb:GetItem' + - 'dynamodb:PutItem' + - 'dynamodb:Scan' + Resource: + - !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/<%=CDO.dcdo_table_name%>" + - !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/<%=CDO.gatekeeper_table_name%>" + # Write analysis events to Firehose. + # TODO: Import resources into stack. + - Effect: Allow + Action: 'firehose:PutRecord' + Resource: !Sub "arn:aws:firehose:${AWS::Region}:${AWS::AccountId}:deliverystream/analysis-events" + # General s3 access. + # TODO: Further restrict permissions to grant least privilege. + - Effect: Allow + Action: 's3:*' + Resource: '*' <% if frontends -%> + FrontendRole: + Type: AWS::IAM::Role + Properties: + <%=service_role 'ec2'%> + Policies: + - PolicyName: LifecycleHook + PolicyDocument: + Statement: + - Effect: Allow + Action: 'autoscaling:CompleteLifecycleAction' + Resource: !Sub "arn:aws:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*:autoScalingGroupName/Frontends-${AWS::StackName}" + ManagedPolicyArns: [!Ref CDOPolicy] + PermissionsBoundary: !ImportValue IAM-DevPermissions + FrontendInstanceProfile: + Type: AWS::IAM::InstanceProfile + Properties: {Roles: [!Ref FrontendRole]} # Signal when the instance is fully provisioned and ready for AMI creation. AMICreate<%=ami%>: Type: AWS::CloudFormation::WaitCondition @@ -91,7 +163,7 @@ Resources: Properties: ImageId: <%=IMAGE_ID%> InstanceType: !Ref InstanceType - IamInstanceProfile: !ImportValue IAM-FrontendInstanceProfile + IamInstanceProfile: !ImportValue IAM-FrontendInstanceProfile # Migrate to: !Ref FrontendInstanceProfile SecurityGroupIds: [!ImportValue VPC-FrontendSecurityGroup] SubnetId: !ImportValue VPC-Subnet<%=azs.first%> KeyName: <%=SSH_KEY_NAME%> @@ -200,7 +272,7 @@ Resources: Properties: ImageId: !GetAtt [AMI<%=ami%>, ImageId] InstanceType: !Ref InstanceType - IamInstanceProfile: !ImportValue IAM-FrontendInstanceProfile + IamInstanceProfile: !ImportValue IAM-FrontendInstanceProfile # Migrate to: !Ref FrontendInstanceProfile SecurityGroups: [!ImportValue VPC-FrontendSecurityGroup] KeyName: <%=SSH_KEY_NAME%> BlockDeviceMappings: @@ -234,7 +306,7 @@ Resources: DefaultResult: ABANDON HeartbeatTimeout: 1200 # seconds = 20 minutes NotificationTargetARN: !Ref WebServerHookTopicNew - RoleARN: !ImportValue IAM-LifecycleHookRoleARN + RoleARN: !ImportValue IAM-LifecycleHookRoleARN # Migrate to: !Ref WebServerHookRole WebServerHookEventRule: Type: AWS::Events::Rule Properties: @@ -262,6 +334,18 @@ Resources: Principal: events.amazonaws.com SourceArn: !GetAtt WebServerHookEventRule.Arn WebServerHookTopicNew: {Type: 'AWS::SNS::Topic'} + WebServerHookRole: + Type: AWS::IAM::Role + Properties: + <%=service_role 'autoscaling'%> + Policies: + - PolicyName: snsPublish + PolicyDocument: + Statement: + - Effect: Allow + Action: 'sns:Publish' + Resource: !Ref WebServerHookTopicNew + PermissionsBoundary: !ImportValue IAM-DevPermissions <% if environment == :production -%> HealthEventRule: Type: AWS::Events::Rule @@ -422,6 +506,44 @@ Resources: VisibilityTimeout: 15 QueueName: !Sub "activities_dead-${AWS::StackName}" <% end -%> + DaemonRole: + Type: AWS::IAM::Role + Properties: + <%=service_role 'ec2'%> + Policies: + - PolicyName: Daemon + PolicyDocument: + Statement: + # SQS queues for Asynchronous batch processing by the `process_queues` service. + - Effect: Allow + Action: + - 'sqs:SendMessage' + - 'sqs:ReceiveMessage' + - 'sqs:DeleteMessageBatch' + - 'sqs:DeleteMessage' + Resource: +<% unless daemon -%> + - !GetAtt ActivitiesQueue.Arn + - !GetAtt ActivitiesDeadQueue.Arn +<% end -%> + # TODO: Import pd_workshop resources into stack. + - !Sub "arn:aws:sqs:${AWS::Region}:${AWS::AccountId}:pd_workshop-<%=environment%>" + - !Sub "arn:aws:sqs:${AWS::Region}:${AWS::AccountId}:pd_workshop_dead-<%=environment%>" + - Effect: Allow + Action: + - 'cloudformation:DescribeStacks' + - 'cloudformation:DescribeStackEvents' + - 'cloudformation:GetStackPolicy' + Resource: !Ref AWS::StackId + # Update Stack through `ci:deploy_stack` task. + - Effect: Allow + Action: 'cloudformation:UpdateStack' + Resource: !Ref AWS::StackId + ManagedPolicyArns: [!Ref CDOPolicy] + PermissionsBoundary: !ImportValue IAM-DevPermissions + DaemonInstanceProfile: + Type: AWS::IAM::InstanceProfile + Properties: {Roles: [!Ref DaemonRole]} <% if daemon -%> <%=daemon%>: Type: AWS::EC2::Instance @@ -431,7 +553,7 @@ Resources: Properties: ImageId: <%=IMAGE_ID%> InstanceType: !Ref InstanceType - IamInstanceProfile: !ImportValue IAM-<%=environment == :adhoc ? 'Standalone' : 'Staging'%>InstanceProfile + IamInstanceProfile: !Ref DaemonInstanceProfile KeyName: <%=SSH_KEY_NAME%> Tags: - Key: Name diff --git a/aws/cloudformation/iam.yml.erb b/aws/cloudformation/iam.yml.erb index ad59c2be0f7f7..016bc7cebf059 100644 --- a/aws/cloudformation/iam.yml.erb +++ b/aws/cloudformation/iam.yml.erb @@ -1,9 +1,112 @@ --- AWSTemplateFormatVersion: 2010-09-09 Description: >- - IAM layer including roles and access permissions for Code.org infrastructure. - Note: Admin permissions are required to manage this stack. + IAM layer including global/shared roles and access permissions for Code.org infrastructure. + Note: Admin permissions are required to manage some admin-only resources in this stack. Resources: + # Admin Service Role for managing all resources in CloudFormation stacks. + # Only admins can update stacks using this service role. + CloudFormationAdmin: + Type: AWS::IAM::Role + Properties: + RoleName: CloudFormationAdmin + <%=service_role 'cloudformation'%> + Path: /admin/ + ManagedPolicyArns: ['arn:aws:iam::aws:policy/AdministratorAccess'] + # Shared CloudFormation Service Role used by all stacks. + CloudFormationService: + Type: AWS::IAM::Role + Properties: + RoleName: CloudFormationService + <%=service_role 'cloudformation'%> + Path: /admin/ + Policies: + # Grant permissions for managing specific non-admin AWS::IAM resources. + - PolicyName: RolePermissionsBoundary + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + # AWS::IAM::ManagedPolicy + - iam:CreatePolicy + - iam:CreatePolicyVersion + - iam:DeletePolicy + - iam:DeletePolicyVersion + NotResource: !Sub "arn:aws:iam::${AWS::AccountId}:role/admin/*" + - Effect: Allow + Action: + # AWS::IAM::InstanceProfile + - iam:CreateInstanceProfile + - iam:AddRoleToInstanceProfile + - iam:RemoveRoleFromInstanceProfile + - iam:DeleteInstanceProfile + NotResource: !Sub "arn:aws:iam::${AWS::AccountId}:instance-profile/admin/*" + - Effect: Allow + Action: + - iam:DeleteRole + - iam:UpdateAssumeRolePolicy + NotResource: !Sub "arn:aws:iam::${AWS::AccountId}:role/admin/*" + - Effect: Allow + Action: + # AWS::IAM::Role + - iam:CreateRole + - iam:PutRolePermissionsBoundary + # Managed policies attached to a Role + - iam:AttachRolePolicy + - iam:DetachRolePolicy + # Inline policies embedded in a Role + - iam:PutRolePolicy + - iam:DeleteRolePolicy + NotResource: !Sub "arn:aws:iam::${AWS::AccountId}:role/admin/*" + # Require DevPermissions boundary on all Roles, e.g.: + # PermissionsBoundary: !ImportValue IAM-DevPermissions + Condition: + StringEquals: + iam:PermissionsBoundary: !Ref DevPermissions + ManagedPolicyArns: [!Ref DevPermissions] + # Shared 'Developer' permissions. + # Used as default permissions for all developer Roles, + # and as a required permissions boundary for CloudFormation-managed resources. + DevPermissions: + Type: AWS::IAM::ManagedPolicy + Properties: + ManagedPolicyName: DevPermissions + Path: /admin/ + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + NotAction: + - iam:* + - organizations:* + Resource: '*' + - Sid: IAMReadOnlyAccess + Effect: Allow + Action: + - iam:GenerateCredentialReport + - iam:GenerateServiceLastAccessedDetails + - iam:Get* + - iam:List* + - iam:SimulateCustomPolicy + - iam:SimulatePrincipalPolicy + Resource: '*' + - Effect: Allow + Action: iam:PassRole + Resource: '*' + # Require CloudFormation Service Role on all stack operations. + - Effect: Deny + Action: + - cloudformation:CreateStack + - cloudformation:UpdateStack + - cloudformation:DeleteStack + - cloudformation:CreateChangeSet + Resource: '*' + Condition: + StringNotEquals: + cloudformation:RoleARN: !Sub "arn:aws:iam::${AWS::AccountId}:role/admin/CloudFormationService" + + # Deprecated: use FrontendRole in application stack FrontendRole: Type: AWS::IAM::Role Properties: @@ -42,6 +145,7 @@ Resources: - 'dynamodb:*' - 'sqs:*' Resource: '*' + # Deprecated: use FrontendRole in application stack StandaloneFrontendRole: Type: AWS::IAM::Role Properties: @@ -83,6 +187,7 @@ Resources: Resource: - !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/adhoc_tables" - !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/adhoc_properties" + # Deprecated: use DaemonRole in application stack StandaloneRole: Type: AWS::IAM::Role Properties: @@ -130,6 +235,7 @@ Resources: - "arn:aws:s3:::cdo-dist/adhoc/*" - "arn:aws:s3:::cdo-build-logs/*" - "arn:aws:s3:::<%=TEMP_BUCKET%>/*" + # Deprecated: use DaemonRole in application stack DaemonRole: Type: AWS::IAM::Role Properties: @@ -151,31 +257,37 @@ Resources: Action: - 'iam:GetServerCertificate' Resource: '*' + # Deprecated: use FrontendInstanceProfile in application stack FrontendInstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: '/' Roles: [!Ref FrontendRole] + # Deprecated: use DaemonInstanceProfile in application stack StandaloneInstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: / Roles: [!Ref StandaloneRole] + # Deprecated: use FrontendInstanceProfile in application stack StandaloneFrontendInstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: '/' Roles: [!Ref StandaloneFrontendRole] + # Deprecated: use DaemonInstanceProfile in application stack ProductionInstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: '/' Roles: [!Ref DaemonRole] + # Deprecated: use DaemonInstanceProfile in application stack StagingInstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: '/' Roles: [!Ref DaemonRole] + # Deprecated: use WebServerHookRole in application stack LifecycleHookRole: Type: AWS::IAM::Role Properties: @@ -193,6 +305,8 @@ Resources: - Effect: Allow Action: ['sns:Publish'] Resource: '*' + # Used by FirehoseMicroservice Lambda function. + # TODO move to Data stack FirehoseLambdaRole: Type: AWS::IAM::Role Properties: @@ -216,6 +330,7 @@ Resources: - !Sub "arn:aws:firehose:${AWS::Region}:${AWS::AccountId}:deliverystream/analysis-events" ManagedPolicyArns: - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + # Deprecated: use CloudFormationService for all stack updates CloudFormationRole: Type: AWS::IAM::Role Properties: @@ -235,7 +350,8 @@ Resources: - 'iam:PassRole' Resource: '*' ManagedPolicyArns: - - "arn:aws:iam::aws:policy/PowerUserAccess" + - "arn:aws:iam::aws:policy/PowerUserAccess" + # TODO: Move to Data stack GlueRole: Type: AWS::IAM::Role Properties: @@ -259,6 +375,7 @@ Resources: - 'arn:aws:iam::aws:policy/service-role/AWSGlueServiceRole' # Percona Monitoring and Management RDS access role. # See: https://www.percona.com/doc/percona-monitoring-and-management/amazon-rds.html#creating-a-policy + # TODO: Move to Data stack PMMRole: Type: AWS::IAM::Role Properties: @@ -288,6 +405,7 @@ Resources: - 'logs:FilterLogEvents' Resource: - "arn:aws:logs:*:*:log-group:RDSOSMetrics:*" + # TODO: Move to Data stack PMMInstanceProfile: Type: AWS::IAM::InstanceProfile Properties: @@ -309,6 +427,10 @@ Resources: ManagedPolicyArns: - 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' Outputs: + DevPermissions: + Description: Developer Permissions + Value: !Ref DevPermissions + Export: {Name: !Sub "${AWS::StackName}-DevPermissions"} FrontendInstanceProfile: Description: Frontend Instance Profile Value: !Ref FrontendInstanceProfile diff --git a/dashboard/app/models/pd/fit_weekend_registration_base.rb b/dashboard/app/models/pd/fit_weekend_registration_base.rb index a747b91668aa7..a66786bdd499d 100644 --- a/dashboard/app/models/pd/fit_weekend_registration_base.rb +++ b/dashboard/app/models/pd/fit_weekend_registration_base.rb @@ -91,7 +91,6 @@ def self.required_fields :how_traveling, :need_hotel, :need_disability_support, - :photo_release, :liability_waiver, :agree_share_contact, ].freeze diff --git a/dashboard/test/models/pd/fit_weekend_registration_base_test.rb b/dashboard/test/models/pd/fit_weekend_registration_base_test.rb index ed72e41fbd8eb..52456ba39a307 100644 --- a/dashboard/test/models/pd/fit_weekend_registration_base_test.rb +++ b/dashboard/test/models/pd/fit_weekend_registration_base_test.rb @@ -25,7 +25,6 @@ class Pd::FitWeekendRegistrationBaseTest < ActiveSupport::TestCase "Form data howTraveling", "Form data needHotel", "Form data needDisabilitySupport", - "Form data photoRelease", "Form data liabilityWaiver", "Form data agreeShareContact", ], registration.errors.full_messages diff --git a/lib/cdo/aws/cloud_formation.rb b/lib/cdo/aws/cloud_formation.rb index 8bc430ba2dced..c9e89cad3f0cf 100644 --- a/lib/cdo/aws/cloud_formation.rb +++ b/lib/cdo/aws/cloud_formation.rb @@ -191,18 +191,22 @@ def stack_options(template) } ], }.merge(string_or_url(template)).tap do |options| - if %w[IAM lambda].include? stack_name - options[:capabilities] = %w[ - CAPABILITY_IAM - CAPABILITY_NAMED_IAM - ] - end + options[:capabilities] = %w[ + CAPABILITY_IAM + CAPABILITY_NAMED_IAM + ] if rack_env?(:adhoc) options[:tags].push( key: 'owner', value: Aws::STS::Client.new.get_caller_identity.arn ) end + + # All stacks use the same shared Service Role for CloudFormation resource-management permissions. + # Pass `ADMIN=1` to update admin resources with a privileged Service Role. + role_name = "CloudFormation#{ENV['ADMIN'] ? 'Admin' : 'Service'}" + account = Aws::STS::Client.new.get_caller_identity.account + options[:role_arn] = "arn:aws:iam::#{account}:role/admin/#{role_name}" end end @@ -218,12 +222,8 @@ def create_or_update options[:stack_policy_body] = stack_policy options[:stack_policy_during_update_body] = stack_policy if action == :update end - if action == :create - options[:on_failure] = 'DO_NOTHING' - if daemon - options[:role_arn] = "arn:aws:iam::#{Aws::STS::Client.new.get_caller_identity.account}:role/CloudFormationRole" - end - end + options[:on_failure] = 'DO_NOTHING' if action == :create + begin updated_stack_id = cfn.method("#{action}_stack").call(options).stack_id rescue Aws::CloudFormation::Errors::ValidationError => e @@ -602,6 +602,18 @@ def erb_eval(str, filename=nil, local_vars=nil) end ERB.new(str, nil, '-').tap {|erb| erb.filename = filename}.result(local_binding) end + + # Generate boilerplate Trust Policy for an AWS Service Role. + def service_role(service) + document = { + Statement: [ + Effect: 'Allow', + Action: 'sts:AssumeRole', + Principal: {Service: ["#{service}.amazonaws.com"]} + ] + } + "AssumeRolePolicyDocument: #{document.to_json}" + end end end end diff --git a/lib/cdo/pardot.rb b/lib/cdo/pardot.rb index 66afb3c3d86d3..efbb0dfd8e0fc 100644 --- a/lib/cdo/pardot.rb +++ b/lib/cdo/pardot.rb @@ -271,11 +271,8 @@ def self.apply_special_fields(src, dest) end # The custom Pardot db_Opt_In field has type "Dropdown" with permitted values "Yes" or "No". - # Explicitly check for the source opt_in field to be true or false, because nil is 'falsey' - # and it's a little misleading to set a value for db_Opt_In in Pardot when there's no information either - # way from the user in our database. - dest[:db_Opt_In] = 'Yes' if src[:opt_in] == true - dest[:db_Opt_In] = 'No' if src[:opt_in] == false + # Set db_Opt_in to 'No' when there is no source entry, matching the process used by Marketing team. + dest[:db_Opt_In] = src[:opt_in] == true ? 'Yes' : 'No' # If this contact has a dashboard user ID (which means it is a teacher # account), mark that in a Pardot field so we can segment on that. diff --git a/pegasus/data/cdo-cs-statistics.csv b/pegasus/data/cdo-cs-statistics.csv index 5dcd28c60fc27..617ed2df2d84e 100644 --- a/pegasus/data/cdo-cs-statistics.csv +++ b/pegasus/data/cdo-cs-statistics.csv @@ -6,5 +6,5 @@ Slide3_Diversity_K12,https://www.facebook.com/Code.org/photos/a.899372753492362. Slide4_CS_Foundational,https://www.facebook.com/Code.org/photos/a.899372753492362.1073741858.309754825787494/1689600024469627/?type=3&theater Slide5_CS_Education,https://www.facebook.com/Code.org/photos/a.899372753492362.1073741858.309754825787494/1689597964469833/?type=3&theater Slide6_Computing_Jobs,https://www.facebook.com/Code.org/photos/a.899372753492362.1073741858.309754825787494/1664423290320634/?type=3&theater -Slide_States_Standards,https://www.facebook.com/Code.org/photos/a.899372753492362/2070989602997332/?type=3&theater +Slide_States_Standards,https://www.facebook.com/Code.org/photos/a.899372753492362/2092434260852866/?type=3&theater Slide7_States_Map,https://www.facebook.com/Code.org/photos/a.899372753492362/2090952547667704/?type=3&theater diff --git a/pegasus/data/cdo-partners.csv b/pegasus/data/cdo-partners.csv index affb8350c309a..bb4d1bd0cf522 100644 --- a/pegasus/data/cdo-partners.csv +++ b/pegasus/data/cdo-partners.csv @@ -169,6 +169,7 @@ Code en mi Cole,http://www.codenmicole.com/,international,TRUE,FALSE,TRUE, Code for Everyone,http://code.or.jp/,international,TRUE,FALSE,TRUE, Code4KT,http://www.code4kt.com,international,FALSE,FALSE,TRUE, Codeforsu,http://www.codeforsu.org,international,TRUE,FALSE,TRUE, +Codercub,https://medium.com/codercub,international,FALSE,FALSE,TRUE, CoderDojo,http://coderdojo.com/,international,TRUE,FALSE,TRUE, CodersGuild.Net,https://www.codersguild.net/,international,FALSE,FALSE,TRUE, CodeTigers,http://codetigers.in/,international,TRUE,FALSE,TRUE, diff --git a/pegasus/sites.v3/code.org/public/educate/curriculum/apcsa.md b/pegasus/sites.v3/code.org/public/educate/curriculum/apcsa.md index b3910f7ea7e5a..2a7ec99b3d506 100644 --- a/pegasus/sites.v3/code.org/public/educate/curriculum/apcsa.md +++ b/pegasus/sites.v3/code.org/public/educate/curriculum/apcsa.md @@ -29,6 +29,7 @@ pedagogical techniques and content-specific strategies that you can use in your |--------------|------|-----| | [A+ Computer Science](https://www.apluscompsci.com/material.htm) | $535 in year 1, $325 for renewal | Half-day, full-day, and multi-day Computer Science workshops available upon request | | [CodeHS](https://codehs.com/info/curriculum/apjava) | FREE. Pro plans for schools start at $2500 | 30-40 hour online course, $1500/teacher | +| [CompuScholar](https://www.compuscholar.com/schools/courses/ap-java/) | 6+ courses including AP CS A. Multiple licensing options for all size schools or classes. | Pre-approved AP CS A syllabus, FREE onboarding, FREE monthly webinars, FREE dedicated CSR for 1:1 support, FREE personal skill-building opportunities. | | [Edhesive](https://edhesive.com/courses/apcs_java) | FREE | Online PD, community and content/technical/program support available, $2,200 per school | | [IMACS](https://www.eimacs.com/educ_apcsoverview.htm) | $19.95/student, 10-student minimum; $50/class annual setup fee | Teacher support via IMACS Message Center, email, and telephone.| diff --git a/pegasus/sites.v3/code.org/public/educate/regional-partner/playbook/advocacy.md b/pegasus/sites.v3/code.org/public/educate/regional-partner/playbook/advocacy.md index 37c10ed392c1a..392ef2524198a 100644 --- a/pegasus/sites.v3/code.org/public/educate/regional-partner/playbook/advocacy.md +++ b/pegasus/sites.v3/code.org/public/educate/regional-partner/playbook/advocacy.md @@ -24,6 +24,7 @@ Regional partners who are interested in advocacy can use the following resources - [Digging Deeper into AP Computer Science](http://code.org/promote/ap): Graphs and descriptions of AP CS A exam participation over the past decade, broken down by state, gender, and race/ethnicity - [State-by-state 9 policies](https://docs.google.com/spreadsheets/d/1YtTVcpQXoZz0IchihwGOihaCNeqCz2HyLwaXYpyb2SQ/pubhtml): Data on each state for each of the 9 policy ideas in spreadsheet format - [State Policy Landscape](https://docs.google.com/document/d/1J3TbEQt3SmIWuha7ooBPvlWpiK-pNVIV5uuQEzNzdkE/edit?usp=sharing): Provides a summary of state accomplishments in computer science policy in narrative format +- 9 Policies Tracker: Track the number of policies adopted per state. ## Policy Development Resources - [State Planning Toolkit](https://docs.google.com/document/d/13N843-BjK9JHXNWKFzJlxhpw7f6Y2pJF6tpV2aHM1HU/edit?usp=sharing): A guide for state teams creating strategic plans for implementing K-12 computer science diff --git a/pegasus/sites.v3/code.org/public/educate/regional-partner/playbook/payments.md b/pegasus/sites.v3/code.org/public/educate/regional-partner/playbook/payments.md index 5534f8112dc1e..1fa773dbf8faf 100644 --- a/pegasus/sites.v3/code.org/public/educate/regional-partner/playbook/payments.md +++ b/pegasus/sites.v3/code.org/public/educate/regional-partner/playbook/payments.md @@ -15,12 +15,21 @@ ________________ - [Administrator and Counselor Workshops](#ac) - [Community Events](#community) - [CS Fairs](#csfair) +- [Teacher Recruitment Fund](#recruitment) ________________ ## Announcements -**Beginning March 1**, all invoices should be submitted by completing the form and attaching the required documentation at [bit.ly/codeinvoices](bit.ly/codeinvoices). +**March 8 -** + +Instructions for requesting reimbursement towards the [Teacher Recruitment Fund](https://code.org/educate/regional-partner/playbook/teacher-recruitment#recruitment) is now available. Jump to the [Teacher Recruitment Fund](#recruitment) section below. + + + +**March 1 -** + +All invoices should be submitted by completing the form and attaching the required documentation at [bit.ly/codeinvoices](bit.ly/codeinvoices). Please note that invoices submitted to invoices@code.org on or after March 1 will be redirected to the new submission form. Invoices submitted more than 30 days after the event or workshop date will not be paid per our existing policy. @@ -170,3 +179,14 @@ To receive reimbursement for a CS Fair, provide an official invoice and receipts See more information about the CS Fair and reimbursable expenses on the [Community Building tile](http://code.org/educate/regional-partner/playbook/community). [**Back to the top**](#top) + +________________ + + +## Teacher Recruitment Fund + +To receive reimbursement for expenses incurred towards your Recruitment Fund, complete the form and attach recripts for [allowable expenses](https://staging.code.org/educate/regional-partner/playbook/teacher-recruitment#recruitment) at [bit.ly/codeinvoices](bit.ly/codeinvoices). + +Required information for reimbursement will include date, amount, invoice, receipts, a brief explanation of how the expenses supported recruitment in your region, and your best estimate of the number of teachers reached. + +[**Back to the top**](#top) \ No newline at end of file diff --git a/pegasus/sites.v3/code.org/public/educate/regional-partner/playbook/teacher-recruitment.md b/pegasus/sites.v3/code.org/public/educate/regional-partner/playbook/teacher-recruitment.md index 0d23fa10ba92e..6441e1d9fc933 100644 --- a/pegasus/sites.v3/code.org/public/educate/regional-partner/playbook/teacher-recruitment.md +++ b/pegasus/sites.v3/code.org/public/educate/regional-partner/playbook/teacher-recruitment.md @@ -8,12 +8,18 @@ nav: regional_partner_playbook_nav # Teacher and District Recruitment ## Table of Contents -- [Teacher Application](#teachapp) +- [Teacher Applications and Recruitment](#teachapp) - [Teacher Outreach](#teachout) - [District Outreach](#district) Compiled document of all Teacher Recruitment Resources and Strategies **[here](https://docs.google.com/document/d/1pNyPvt54ACH7sfA0f13_-CZGQ4m-VhcH4NorEIhdx24/edit?ts=5a6baf6c#heading=h.a8j457kayvro)**. +________________ + +## Announcements + +**March 8:** A new fund is available to support teacher recruitment for the '19-'20 school year in your region. Jump to the [Teacher Recruitment Fund](#recruitment) section below. + ________________ ## Teacher Applications and Recruitment @@ -40,6 +46,19 @@ Review the course and Professional Learning Program options to determine the best fit for their teaching situation. + + +### Teacher Recruitment Fund +In order to support teacher recruitment efforts and increase the number of submitted teacher applications, Code.org will reimburse each Regional Partner a maximum of $1,000 towards the costs of teacher recruitment. + +This additional funding may be used for: + +- Registration fees for a booth at education leadership/educator conferences +- Travel for on-the-ground recruitment, including mileage, hotels, flights, and meals +- Marketing materials such as flyers, swag/collateral (including printing), purchasing a distribution list, and other advertising (radio, print, etc.) +- Facilitator payment/temporary staff stipends to help with teacher recruitment +- Other ideas, with pre-approval from your Regional Manager + ________________ ## Teacher Outreach diff --git a/pegasus/sites.v3/code.org/public/educate/regional-partner/playbook/workshops-pl.md b/pegasus/sites.v3/code.org/public/educate/regional-partner/playbook/workshops-pl.md index 9b03252339831..a5ae952d6102f 100644 --- a/pegasus/sites.v3/code.org/public/educate/regional-partner/playbook/workshops-pl.md +++ b/pegasus/sites.v3/code.org/public/educate/regional-partner/playbook/workshops-pl.md @@ -17,6 +17,7 @@ Workshops are the core of Code.org's Teacher [Professional Learning Program](htt - **[6-12 Workshops](#csp)** - **[Local Summer Workshop Specific](#local)** - **[Academic Year Workshop Specific](#academic)** +- **[Virtual PL Specific](#virtual)** - **[Workshop Dashboard](#dashboard)** - **[Professional Learning Program One-Pager](https://code.org/files/PLProgramsOverview_1-Pager.pdf)** @@ -26,7 +27,8 @@ ________________ ## Announcements -- Printing instructions and materials are now available for Academic Year Workshop 3 [here](#printing)! +- **New** resources for the '19-'20 Virtual Professional Learning Program are available [here](#virtual) +- **New** resources for the CS Fundamentals Deep Dive workshop are available [here](#csf) [**Back to the top**](#top) @@ -40,6 +42,10 @@ ________________ See these resources to plan and run CS Fundamentals workshops. Partners should work with an approved CS Fundamentals facilitator when organizing one of these workshops. - CS Fundamentals Workshop Process Document +- 2019 Intro and Deep Dive Workshop Overview +- 2019 Deep Dive Workshop FAQ +- 2019 Deep Dive One-Pager +- 2019 Deep Dive Marketing Deck - Mimeo Marketplace Guidelines for ordering materials & swag [**Back to the top**](#top) @@ -96,14 +102,6 @@ Always accommodate vegetarian needs, roughly ⅓ of the order. In communications

-
- **Other** -

- -- **[2018-19 Workshop Surveys Guide](https://docs.google.com/document/d/1YFHuan6wZWwqiN9YMymtzOKrAHMhiM7bOgRS8QPPF6w/edit):** Includes guidance for summer and academic year workshop surveys -- **Automated Emails:** Teachers attending your workshop will receive automated emails 10 days and 3 days prior to attending the workshop. You can see a copy of these emails in your workshop process documents. -

-
@@ -112,7 +110,7 @@ Always accommodate vegetarian needs, roughly ⅓ of the order. In communications Updated guidance for hosting 2019 Local Summer Workshops is now available! - **[2019 Local Summer Workshop Process Document](https://docs.google.com/document/d/1YSLD6U1gkUSAMpymoHeXX_NniC0WY7eSFxzOvu9U23I/edit?usp=sharing)** -- [Sample Logistics Questions for Registration](https://docs.google.com/document/d/1PPnxnoWYim-8qWqOEdwHi0fadx2y-Hc2PWd4QWKd3p8/edit?ts=5c5dc1e6#heading=h.9xw1ckl4l6ru) +- **[Sample Logistics Questions for Registration](https://docs.google.com/document/d/1PPnxnoWYim-8qWqOEdwHi0fadx2y-Hc2PWd4QWKd3p8/edit?ts=5c5dc1e6#heading=h.9xw1ckl4l6ru)** @@ -122,43 +120,14 @@ Updated guidance for hosting 2019 Local Summer Workshops is now available! - **[2019-2020 Academic Year Workshop Process Document] (https://docs.google.com/document/d/1sJUI1F7r270k8LYMbLBBZwqQho7xJMc0G-YUrfiRJwM/edit?usp=sharing)** - **[2018-2019 Academic Year Workshop Process Document] (https://docs.google.com/document/d/1P1IhiMVgGCGZFVaof9bdc7pRKmTK0ZOQTOw0KDkhMxs/edit?ts=5b294bbb)** - - -##### Printing Instructions for Academic Year - Workshop 3 - -
- CS Discoveries -

- -Unit 4 Lesson 4: - -- Print one copy of the User Interface Screens - Activity Guide for each pair of participants -- Print one copy of the User Testing (Computer) - Activity Guide for each pair of participants -- Print one copy of the User Testing (User) - Activity Guide for each pair of participants -- Cut out the UI Screens - -Unit 5 Lesson 2 + -- Print one copy per pair of participants - Representing Information - Activity Guide -- Print one copy per pair of participants - Animal Shapes - Manipulative -- Make sure to cut up animal shapes before workshop +### Virtual PL Specific -Unit 5 Lesson 6 - -- Print one copy per participants - Keeping Data Secret - Activity Guide - -

-
- -
- CS Principles -

- -1. Print one copy of the Explore Task Rubric for each participant - - -

-
+- **[Teacher Signup Form](https://form.jotform.com/90353759556165)** +- **[Teacher Tracking Sheet](https://docs.google.com/spreadsheets/d/19KrOp4JyAufdFowL9Yqnq1HTs7XXlUwUJF7QFotVRsg/edit)** +- **[Process for Claiming Seats](https://docs.google.com/document/d/1d-PaJErPIoID-N5fyJHdwkwxOHFiJWY2VpYM6QaAChU/edit)** +- **[Teacher-Facing Handout](https://docs.google.com/document/d/1lX_jgewtUmPxfCqCdNopaylvnNKnolIkIknoSJtZ4Zc/edit)** [**Back to the top**](#top)
diff --git a/pegasus/sites.v3/code.org/public/files/computer_science_advocacy.pptx b/pegasus/sites.v3/code.org/public/files/computer_science_advocacy.pptx index 5b35037154a01..ad18f8a754f7f 100644 Binary files a/pegasus/sites.v3/code.org/public/files/computer_science_advocacy.pptx and b/pegasus/sites.v3/code.org/public/files/computer_science_advocacy.pptx differ diff --git a/pegasus/sites.v3/code.org/public/images/cs-stats/Slide_States_Standards.png b/pegasus/sites.v3/code.org/public/images/cs-stats/Slide_States_Standards.png index 79c5e1445b790..1cd0960e5d00a 100644 Binary files a/pegasus/sites.v3/code.org/public/images/cs-stats/Slide_States_Standards.png and b/pegasus/sites.v3/code.org/public/images/cs-stats/Slide_States_Standards.png differ diff --git a/pegasus/sites.v3/code.org/public/pluralsight.md b/pegasus/sites.v3/code.org/public/pluralsight.md index ea15e812aeaac..84c787776f249 100644 --- a/pegasus/sites.v3/code.org/public/pluralsight.md +++ b/pegasus/sites.v3/code.org/public/pluralsight.md @@ -8,7 +8,7 @@ theme: responsive ## Keep growing their skills long after the AP Exam Once the AP Computer Science Principles Exam is over, continue growing learning and engagement in your classroom with [Pluralsight One's special partnership with Code.org](https://www.pluralsightone.org/product/education). -Students who participate in Code.org’s AP Computer Science Principles curriculum can continue to build on the skills they've spent the year building with free and unlimited access to the Pluralsight course library. Designed specifically for your students, these courses allow students to dive deeper into intermediate and advanced topics.
+Students who participate in Code.org’s AP Computer Science Principles curriculum can continue to develop the skills they've spent the year building with free and unlimited access to the Pluralsight course library. Designed specifically for your students, these courses allow your classroom to dive deeper into intermediate and advanced topics.
### **Code.org AP Computer Science student access**
Advance your students’ futures by enabling them to build in-demand skills and learn from expert technologists. diff --git a/pegasus/sites.v3/hourofcode.com/public/images/codercub.png b/pegasus/sites.v3/hourofcode.com/public/images/codercub.png new file mode 100644 index 0000000000000..13fc01fcbdb00 Binary files /dev/null and b/pegasus/sites.v3/hourofcode.com/public/images/codercub.png differ diff --git a/pegasus/sites.v3/hourofcode.com/public/images/cs-stats/Slide_States_Standards.png b/pegasus/sites.v3/hourofcode.com/public/images/cs-stats/Slide_States_Standards.png index 79c5e1445b790..1cd0960e5d00a 100644 Binary files a/pegasus/sites.v3/hourofcode.com/public/images/cs-stats/Slide_States_Standards.png and b/pegasus/sites.v3/hourofcode.com/public/images/cs-stats/Slide_States_Standards.png differ diff --git a/pegasus/sites.v3/hourofcode.com/public/international-partners.md b/pegasus/sites.v3/hourofcode.com/public/international-partners.md index 392cdfb64648a..7ad53792ec1c8 100644 --- a/pegasus/sites.v3/hourofcode.com/public/international-partners.md +++ b/pegasus/sites.v3/hourofcode.com/public/international-partners.md @@ -73,19 +73,21 @@ You, too, can play a leading role to get more people in your country involved! I | Mauritania | HADINA RIMTIC | | http://hadinarimtic.org | | Mexico | Colegio Nikola Tesla | Larisa Hinojosa | http://nikolatesla.edu.mx | | Mexico | Cuantrix - Fundación Televisa | Nomara Parra | www.cuantrix.mx
nomara.parra@fundaciontelevisa.org | -| Morocco | S.T.E.M ROBOTICS AGADIR | Dr. Isha Daramy | Stemagadir@gmail.com | +| Mongolia | Codercub | Zolbayar Magsar | https://medium.com/codercub
codercub@outlook.com | +| Morocco | S.T.E.M ROBOTICS AGADIR | Dr. Isha Daramy | Stemagadir@gmail.com | +| Nepal | Adhyayan School | Sandeep Sharma | http://adhyayan.edu.np
sandeep.sharma@adhyayan.edu.np | | New Zealand | Ara Institute of Canterbury | Amitrajit Sarkar | www.ara.ac.nz
sarkara@cpit.ac.nz | | New Zealand | Google | Sally-Ann Williams | https://www.google.com/edu/cs | | New Zealand (Aotearoa) | OMGTech! | Vivian Chandra | https://omgtech.co.nz
viv@omgtech.co.nz | -| Nigeria | CODE4KT- coding for kids & teens | Mayokun Odusote | odusotemayokun@gmail.com | +| Nigeria | CODE4KT- coding for kids & teens | Mayokun Odusote | odusotemayokun@gmail.com | | Nigeria | Dev's District Nigeria | Emmanuel Odunlade | http://devsdistrictnigeria.com
E.odunlade@devsdistrictnigeria.com | -| Nigeria | Eduxtra | Paul Obuke Omu | hello@eduxtra.com.ng | +| Nigeria | Eduxtra | Paul Obuke Omu | hello@eduxtra.com.ng | | Nigeria | Odyssey Educational Foundation | Stella Uzochukwu-Denis | www.odysseyedufoundation.org
ifywayne@gmail.com | | Nigeria | KodeHauz | Glory Agatevure | http://www.kodehauz.com | | Nigeria | The Zariah Elizabeth Foundation | Akindayo Akindolani | www.tzefoundation.org
dayo@tzefoundation.org | | Norway | Lær Kidsa Koding | Line Moseng | https://kidsakoder.no | | Oman | Engineering Village | | www.ev-center.com | -| Pakistan | KnowledgeJump | Nasir Farhat Khan | nasir@knowledgejump.net | +| Pakistan | KnowledgeJump | Nasir Farhat Khan | nasir@knowledgejump.net | | Pakistan | TechValley | Dr. Waqar S. Qureshi | | | Peru | Code en mi Cole | Renzo Sousa | http://codenmicole.com | | Philippines | CodersGuild.Net | Joey Gurango | https://www.codersguild.net
joey@gurango.net |