Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable applab data APIs in exported projects #22522

Merged
merged 9 commits into from Jun 7, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 13 additions & 0 deletions apps/src/applab/Exporter.js
Expand Up @@ -230,14 +230,23 @@ function extractCSSFromHTML(el) {
const fontAwesomeWOFFRelativeSourcePath = '/assets/fontawesome-webfont.woff2';
const fontAwesomeWOFFPath = 'applab/fontawesome-webfont.woff2';

function getExportConfigPath(baseHref) {
const curHref = window.location.href;
const curHrefWithoutEdit = curHref.slice(0, curHref.lastIndexOf('/') + 1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure this works when exporting from a script level like https://studio.code.org/s/csp3/stage/9/puzzle/11 . It's possible that window.dashboard.project.getShareUrl() will give you what you want.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, much better using getShareUrl()!

baseHref = baseHref || curHrefWithoutEdit;
return `${baseHref}export_config?script_call=setExportConfig`;
}

export default {
exportAppToZip(appName, code, levelHtml, expoMode) {
const { css, outerHTML } = transformLevelHtml(levelHtml);

const exportConfigPath = getExportConfigPath();
const jQueryBaseName = 'jquery-1.12.1.min';
var html;
if (expoMode) {
html = exportExpoIndexEjs({
exportConfigPath,
htmlBody: outerHTML,
applabApiPath: "applab-api.j",
jQueryPath: jQueryBaseName + ".j",
Expand All @@ -250,6 +259,7 @@ export default {
});
} else {
html = exportProjectEjs({
exportConfigPath,
htmlBody: outerHTML,
fontPath: fontAwesomeWOFFPath,
});
Expand Down Expand Up @@ -456,7 +466,10 @@ export default {
const appOptionsJs = getAppOptionsFile();
const { css, outerHTML } = transformLevelHtml(levelHtml);
const fontAwesomeCSS = exportFontAwesomeCssEjs({fontPath: fontAwesomeWOFFPath});
const exportBaseHref = `https://studio.code.org/projects/applab/${project.getCurrentId()}/`;
const exportConfigPath = getExportConfigPath(exportBaseHref);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may be misunderstanding, but this looks like it won't work in non-production environments (here and below), making it hard to debug and test. Do you currently need to change all of these urls to point to localhost during development? Ideally, the "origin" (https://studio.code.org/ or http://localhost-studio.code.org:3000/) would be passed from the server via appOptions. That work could be done later, but I'd like to be on the same page about whether there is more work to do here.

Also, is this a typo on line 476 below? appOptionsPath: "appOptions.j",

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the "Try In Expo" scenario results in apps that are published with links back to our production site. We could change this for testing purposes, but the development server URL is unlikely to work from a mobile device already.

And no, the .j extension is not a typo. We are forced to bundle our .js files as .j files such that the Metro bundler doesn't attempt to parse them and include them in the React Native JavaScript bundle.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the hardcoded https://studio.code.org/ URLs. It makes it a little tricky during development for 2 reasons: (1) the lack of a minified applab-api.min.js - but I made that conditional, and (2) the fact that most phones can't access localhost-studio.code.org since the server is not running on the phone. Dealing with #2 is trickier, but at least phone simulators/emulators can be run on the same IP as the dev server.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this! It looks like this sets us up better for having end-to-end tests ensuring that (a) previously exported projects do not break, and (b) newly exported projects do not break, which are the two tests I'd want to eventually have for this feature. Also the applab-api logic looks like it will do the right thing in test and circle environments.

As for local development, I must be missing some context -- are we exporting to a format that can only be run from a phone? or is it just the nature of this feature that viewing from a phone is the main use case? If testing on phone is needed, it seems like the developer's options include either running ios simulator locally (easy on mac, hard on linux) or temporarily setting origin to https://studio.code.org/ while developing. If that's too annoying, I'd be open to putting this back how it was by default, and leaving it up to the eventual UI test writer to figure out how to conditionally turn this origin logic back on.

const html = exportExpoIndexEjs({
exportConfigPath,
htmlBody: outerHTML,
commonLocalePath: "https://studio.code.org/blockly/js/en_us/common_locale.js",
applabLocalePath: "https://studio.code.org/blockly/js/en_us/applab_locale.js",
Expand Down
22 changes: 2 additions & 20 deletions apps/src/applab/api-entry.js
Expand Up @@ -28,15 +28,15 @@ studioApp().highlight = noop;
Applab.render = noop;

// window.APP_OPTIONS gets generated on the fly by the exporter and appended to this file.
setAppOptions(Object.assign(window.APP_OPTIONS, {isExported: true}));
const exportOptions = Object.assign({isExported: true}, window.EXPORT_OPTIONS);
setAppOptions(Object.assign(window.APP_OPTIONS, exportOptions));
setupApp(window.APP_OPTIONS);
loadApplab(getAppOptions());
// reset applab turtle manually (normally called when execution begins)
// before the student's code is run.
Applab.resetTurtle();
getStore().dispatch(setIsRunning(true));


// Expose api functions globally, unless they already exist
// in which case they are probably browser apis that we should
// not overwrite... unless they are in a whitelist of browser
Expand All @@ -50,24 +50,6 @@ for (let key in globalApi) {
}
}

const STORAGE_COMMANDS = [
'createRecord',
'readRecords',
'updateRecord',
'deleteRecord',
'onRecordEvent',
'getKeyValue',
'setKeyValue',
'getKeyValueSync',
'setKeyValueSync',
];

for (let key in STORAGE_COMMANDS) {
STORAGE_COMMANDS[key] = function () {
console.error("Data APIs are not available outside of code studio.");
};
}

// Set up an error handler for student errors and warnings.
injectErrorHandler(new JavaScriptModeErrorHandler(
() => Applab.JSInterpreter,
Expand Down
2 changes: 0 additions & 2 deletions apps/src/code-studio/components/AdvancedShareOptions.jsx
Expand Up @@ -193,7 +193,6 @@ class AdvancedShareOptions extends React.Component {
<p style={style.p}>
Export your project as a zipped file, which will contain the
HTML/CSS/JS files, as well as any assets, for your project.
Note that data APIs will not work outside of Code Studio.
</p>
<button onClick={this.downloadExport} style={{marginLeft: 0}}>
{spinner}
Expand Down Expand Up @@ -227,7 +226,6 @@ class AdvancedShareOptions extends React.Component {
<div>
<p style={style.p}>
Try running your project in the Expo app on iOS or Android.
Note that data APIs will not work outside of Code Studio.
You can also export for submission to the Apple App Store or the
Google Play Store (both require following our step-by-step guide).
</p>
Expand Down
4 changes: 4 additions & 0 deletions apps/src/templates/export/expo/index.html.ejs
Expand Up @@ -5,6 +5,10 @@
<meta name="viewport" content="width=320, minimal-ui, target-densityDpi=device-dpi, user-scalable=no">
<link rel="preload" href="<%- fontPath %>" as="font" type="font/woff2">
<script src="<%- jQueryPath %>"></script>
<script>
function setExportConfig(config) { window.EXPORT_OPTIONS = config; }
</script>
<script src="<%- exportConfigPath %>"></script>
<% if (appOptionsPath) { -%>
<script src="<%- appOptionsPath %>"></script>
<% } -%>
Expand Down
4 changes: 4 additions & 0 deletions apps/src/templates/export/project.html.ejs
Expand Up @@ -5,6 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preload" href="<%- fontPath %>" as="font">
<script src="https://code.jquery.com/jquery-1.12.1.min.js"></script>
<script>
function setExportConfig(config) { window.EXPORT_OPTIONS = config; }
</script>
<script src="<%- exportConfigPath %>"></script>
<script src="applab/applab-api.js"></script>
<link rel="stylesheet" href="applab/applab.css">
<link rel="stylesheet" href="style.css">
Expand Down
14 changes: 12 additions & 2 deletions dashboard/app/controllers/projects_controller.rb
Expand Up @@ -2,9 +2,10 @@
require 'cdo/firehose'

class ProjectsController < ApplicationController
before_action :authenticate_user!, except: [:load, :create_new, :show, :edit, :readonly, :redirect_legacy, :public, :index]
before_action :authenticate_user!, except: [:load, :create_new, :show, :edit, :readonly, :redirect_legacy, :public, :index, :export_config]
before_action :authorize_load_project!, only: [:load, :create_new, :edit, :remix]
before_action :set_level, only: [:load, :create_new, :show, :edit, :readonly, :remix]
before_action :set_level, only: [:load, :create_new, :show, :edit, :readonly, :remix, :export_config]
protect_from_forgery except: :export_config
include LevelsHelper

TEMPLATES = %w(projects).freeze
Expand Down Expand Up @@ -376,6 +377,15 @@ def remix
redirect_to action: 'edit', channel_id: new_channel_id
end

def export_config
return if redirect_under_13_without_tos_teacher(@level)
if params[:script_call]
render js: "#{params[:script_call]}(#{firebase_options.to_json});"
else
render json: firebase_options
end
end

def set_level
@level = get_from_cache STANDALONE_PROJECTS[params[:key]][:name]
@game = @level.game
Expand Down
18 changes: 13 additions & 5 deletions dashboard/app/helpers/levels_helper.rb
Expand Up @@ -420,6 +420,18 @@ def question_options
app_options
end

def firebase_options
fb_options = {}

if @level.game.use_firebase?
fb_options[:firebaseName] = CDO.firebase_name
fb_options[:firebaseAuthToken] = firebase_auth_token
fb_options[:firebaseChannelIdSuffix] = CDO.firebase_channel_id_suffix
end

fb_options
end

# Options hash for Blockly
def blockly_options
l = @level
Expand Down Expand Up @@ -550,11 +562,7 @@ def blockly_options
app_options[:isLegacyShare] = true if @is_legacy_share
app_options[:isMobile] = true if browser.mobile?
app_options[:labUserId] = lab_user_id if @game == Game.applab || @game == Game.gamelab
if @level.game.use_firebase?
app_options[:firebaseName] = CDO.firebase_name
app_options[:firebaseAuthToken] = firebase_auth_token
app_options[:firebaseChannelIdSuffix] = CDO.firebase_channel_id_suffix
end
app_options.merge!(firebase_options)
app_options[:canResetAbuse] = true if current_user && current_user.permission?(UserPermission::PROJECT_VALIDATOR)
app_options[:isSignedIn] = !current_user.nil?
app_options[:isTooYoung] = !current_user.nil? && current_user.under_13? && current_user.terms_version.nil?
Expand Down
1 change: 1 addition & 0 deletions dashboard/config/routes.rb
Expand Up @@ -174,6 +174,7 @@ module OPS
get "/#{key}/:channel_id/view", to: 'projects#show', key: key.to_s, as: "#{key}_project_view", readonly: true
get "/#{key}/:channel_id/embed", to: 'projects#show', key: key.to_s, as: "#{key}_project_iframe_embed", iframe_embed: true
get "/#{key}/:channel_id/remix", to: 'projects#remix', key: key.to_s, as: "#{key}_project_remix"
get "/#{key}/:channel_id/export_config", to: 'projects#export_config', key: key.to_s, as: "#{key}_project_export_config"
end
get '/angular', to: 'projects#angular'
end
Expand Down