diff --git a/Gemfile b/Gemfile index 20e4498e..92afc60f 100644 --- a/Gemfile +++ b/Gemfile @@ -16,6 +16,7 @@ gem 'highline' gem 'i18n-js' gem 'ims-lti', '< 2.0.0' gem 'jbuilder' +gem 'js-routes' gem 'kramdown' gem 'mimemagic' gem 'nokogiri' diff --git a/Gemfile.lock b/Gemfile.lock index 8abfb583..61cb373b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -216,6 +216,8 @@ GEM thor (>= 0.14, < 2.0) jquery-ui-rails (6.0.1) railties (>= 3.2.16) + js-routes (2.0.7) + railties (>= 4) json (2.3.1) jwt (2.2.3) kaminari (1.2.1) @@ -554,6 +556,7 @@ DEPENDENCIES i18n-js ims-lti (< 2.0.0) jbuilder + js-routes kramdown listen mimemagic diff --git a/README.md b/README.md index cf63dc3b..1c02bc0c 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,8 @@ In order to execute code submissions using Docker, source code files are written ## Production Setup -- We recommend using [Capistrano](http://capistranorb.com/) for deployment +- We recommend using [Capistrano](http://capistranorb.com/) for deployment. +- Once deployed, CodeOcean assumes to run exclusively under a (sub)domain. If you want to use it under a custom subpath, you can specify the desired path using an environment variable: `RAILS_RELATIVE_URL_ROOT=/codeocean`. Please ensure to rebuild all assets and restart the server to apply the new path. ## Monitoring - We use a [Prometheus Exporter](https://github.com/discourse/prometheus_exporter) and a [Telegraf Client](https://github.com/jgraichen/telegraf-ruby) diff --git a/app/assets/javascripts/dashboard.js b/app/assets/javascripts/dashboard.js index c8b1ae51..6ae38079 100644 --- a/app/assets/javascripts/dashboard.js +++ b/app/assets/javascripts/dashboard.js @@ -51,7 +51,7 @@ $(document).on('turbolinks:load', function() { } else { var jqxhr = $.ajax({ dataType: 'json', - url: '/admin/dashboard', + url: Routes.admin_dashboard_path(), method: 'GET' }); jqxhr.done(function(response) { diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index f94f9e16..98e095f3 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -1,7 +1,7 @@ var CodeOceanEditor = { //ACE-Editor-Path // ruby part adds the relative_url_root, if it is set. - ACE_FILES_PATH: '<%= (defined? Rails.application.config.relative_url_root) && Rails.application.config.relative_url_root != nil && Rails.application.config.relative_url_root != "" ? Rails.application.config.relative_url_root : "" %>' + '/assets/ace/', + ACE_FILES_PATH: '<%= "#{Rails.application.config.relative_url_root.chomp('/')}/assets/ace/" %>', THEME: 'ace/theme/textmate', //Color-Encoding for Percentages in Progress Bars (For submissions) diff --git a/app/assets/javascripts/editor/participantsupport.js.erb b/app/assets/javascripts/editor/participantsupport.js.erb index ba9e03ec..af4cb501 100644 --- a/app/assets/javascripts/editor/participantsupport.js.erb +++ b/app/assets/javascripts/editor/participantsupport.js.erb @@ -142,7 +142,7 @@ CodeOceanEditorRequestForComments = { var createRequestForComments = function (submission) { $.ajax({ method: 'POST', - url: '/request_for_comments', + url: Routes.request_for_comments_path(), data: { request_for_comment: { exercise_id: exercise_id, diff --git a/app/assets/javascripts/exercises.js.erb b/app/assets/javascripts/exercises.js.erb index f669951e..5ffea543 100644 --- a/app/assets/javascripts/exercises.js.erb +++ b/app/assets/javascripts/exercises.js.erb @@ -1,6 +1,6 @@ $(document).on('turbolinks:load', function () { // ruby part adds the relative_url_root, if it is set. - var ACE_FILES_PATH = '<%= (defined? Rails.application.config.relative_url_root) && Rails.application.config.relative_url_root != nil && Rails.application.config.relative_url_root != "" ? Rails.application.config.relative_url_root : "" %>' + '/assets/ace/'; + var ACE_FILES_PATH = '<%= "#{Rails.application.config.relative_url_root.chomp('/')}/assets/ace/" %>'; var THEME = 'ace/theme/textmate'; var TAB_KEY_CODE = 9; @@ -351,7 +351,7 @@ $(document).on('turbolinks:load', function () { return $.ajax({ type: 'POST', - url: '/exercises/' + exerciseID + '/export_external_check', + url: Routes.export_external_check_exercise_path(exerciseID), dataType: 'json', success: function (response) { $messageDiv.html(response.message); @@ -370,7 +370,7 @@ $(document).on('turbolinks:load', function () { return $.ajax({ type: 'POST', - url: '/exercises/' + exerciseID + '/export_external_confirm', + url: Routes.export_external_confirm_exercise_path(exerciseID), dataType: 'json', success: function (response) { $messageDiv.html(response.message) @@ -427,9 +427,8 @@ $(document).on('turbolinks:load', function () { }; var updateFileTemplates = function (fileType) { - var rel_url_root = '<%= (defined? Rails.application.config.relative_url_root) && Rails.application.config.relative_url_root != nil && Rails.application.config.relative_url_root != "" ? Rails.application.config.relative_url_root : "" %>'; var jqxhr = $.ajax({ - url: rel_url_root + '/file_templates/by_file_type/' + fileType + '.json', + url: Routes.by_file_type_file_templates_path(fileType), dataType: 'json' }); jqxhr.done(function (response) { diff --git a/app/assets/javascripts/markdown_ace_editor.js b/app/assets/javascripts/markdown_ace_editor.js.erb similarity index 82% rename from app/assets/javascripts/markdown_ace_editor.js rename to app/assets/javascripts/markdown_ace_editor.js.erb index bd9845f3..9ac28df1 100644 --- a/app/assets/javascripts/markdown_ace_editor.js +++ b/app/assets/javascripts/markdown_ace_editor.js.erb @@ -1,5 +1,5 @@ (function() { - var ACE_FILES_PATH = '/assets/ace/'; + var ACE_FILES_PATH = '<%= "#{Rails.application.config.relative_url_root.chomp('/')}/assets/ace/" %>'; window.MarkdownEditor = function(selector) { ace.config.set('modePath', ACE_FILES_PATH); diff --git a/app/assets/javascripts/submission_statistics.js b/app/assets/javascripts/submission_statistics.js.erb similarity index 97% rename from app/assets/javascripts/submission_statistics.js rename to app/assets/javascripts/submission_statistics.js.erb index 71b9427f..0fab3777 100644 --- a/app/assets/javascripts/submission_statistics.js +++ b/app/assets/javascripts/submission_statistics.js.erb @@ -1,6 +1,6 @@ $(document).on('turbolinks:load', function() { - var ACE_FILES_PATH = '/assets/ace/'; + var ACE_FILES_PATH = '<%= "#{Rails.application.config.relative_url_root.chomp('/')}/assets/ace/" %>'; var THEME = 'ace/theme/textmate'; var currentSubmission = 0; diff --git a/app/helpers/statistics_helper.rb b/app/helpers/statistics_helper.rb index a9e669b7..4472e538 100644 --- a/app/helpers/statistics_helper.rb +++ b/app/helpers/statistics_helper.rb @@ -44,7 +44,7 @@ module StatisticsHelper data: ExternalUser.joins(:submissions) .where(['submissions.created_at >= ?', DateTime.now - 5.minutes]) .distinct('external_users.id').count, - url: 'statistics/graphs', + url: statistics_graphs_path, }, ] end diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 8e0bd22d..cf59ed81 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -39,3 +39,7 @@ import 'jquery-ui/themes/base/core.css' import 'jquery-ui/themes/base/resizable.css' import 'jquery-ui/themes/base/selectable.css' import 'jquery-ui/themes/base/sortable.css' + +// Routes +import * as Routes from 'routes.js.erb'; +window.Routes = Routes; diff --git a/app/javascript/routes.js.erb b/app/javascript/routes.js.erb new file mode 100644 index 00000000..5965eb2a --- /dev/null +++ b/app/javascript/routes.js.erb @@ -0,0 +1 @@ +<%= JsRoutes.generate %> diff --git a/app/views/request_for_comments/show.html.slim b/app/views/request_for_comments/show.html.slim index 183ddeda..c25d8ab6 100644 --- a/app/views/request_for_comments/show.html.slim +++ b/app/views/request_for_comments/show.html.slim @@ -148,7 +148,7 @@ javascript: }); // set file paths for ace - var ACE_FILES_PATH = '/assets/ace/'; + var ACE_FILES_PATH = "#{Rails.application.config.relative_url_root.chomp('/')}/assets/ace/"; _.each(['modePath', 'themePath', 'workerPath'], function(attribute) { ace.config.set(attribute, ACE_FILES_PATH); }); @@ -231,7 +231,7 @@ javascript: var jqrequest = $.ajax({ dataType: 'json', method: 'GET', - url: '/comments', + url: Routes.comments_path(), data: { file_id: fileid } @@ -254,7 +254,7 @@ javascript: function deleteComment(commentId, editor, file_id, callback) { var jqxhr = $.ajax({ type: 'DELETE', - url: "/comments/" + commentId + url: Routes.comments_path(commentId) }); jqxhr.done(function () { setAnnotations(editor, file_id); @@ -266,7 +266,7 @@ javascript: function updateComment(commentId, text, editor, file_id, callback) { var jqxhr = $.ajax({ type: 'PATCH', - url: "/comments/" + commentId, + url: Routes.comments_path(commentId), data: { comment: { text: text @@ -293,7 +293,7 @@ javascript: }, dataType: 'json', method: 'POST', - url: "/comments" + url: Routes.comments_path() }); jqxhr.done(function(){ setAnnotations(editor, file_id); @@ -312,7 +312,7 @@ javascript: }, dataType: 'json', method: 'POST', - url: "/subscriptions.json" + url: Routes.subscriptions_path({format: 'json'}) }); jqxhr.done(function(subscription) { checkbox.data('subscription', subscription.id); @@ -329,7 +329,7 @@ javascript: checkbox.attr("disabled", true); var subscriptionId = checkbox.data('subscription'); var jqxhr = $.ajax({ - url: '/subscriptions/' + subscriptionId + '/unsubscribe.json' + url: Routes.unsubscribe_subscription_path(subscriptionId, {format: 'json'}) }); jqxhr.done(function(response) { checkbox.prop('checked', false); diff --git a/config.ru b/config.ru index 6dc83218..4a160382 100644 --- a/config.ru +++ b/config.ru @@ -4,5 +4,7 @@ require_relative 'config/environment' -run Rails.application -Rails.application.load_server +map Rails.application.config.relative_url_root do + run Rails.application + Rails.application.load_server +end diff --git a/config/application.rb b/config/application.rb index 78b2bee6..91c8782d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -40,7 +40,9 @@ module CodeOcean config.autoload_paths += extra_paths config.eager_load_paths += extra_paths - config.action_cable.mount_path = '/cable' + config.relative_url_root = ENV.fetch('RAILS_RELATIVE_URL_ROOT', '/').to_s + + config.action_cable.mount_path = "#{ENV.fetch('RAILS_RELATIVE_URL_ROOT', '')}/cable" config.telegraf.tags = {application: 'codeocean'} diff --git a/config/environments/staging.rb b/config/environments/staging.rb index 41a7e30a..2ad25a24 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -100,7 +100,4 @@ Rails.application.configure do # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false - - # Run on subfolder in production environment. - # config.relative_url_root = '/co-staging' end diff --git a/config/initializers/js_routes.rb b/config/initializers/js_routes.rb new file mode 100644 index 00000000..2bac2935 --- /dev/null +++ b/config/initializers/js_routes.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +JsRoutes.setup do |config| + config.documentation = false + config.prefix = Rails.application.config.relative_url_root + config.url_links = true +end diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 43ed5fc8..234f4f22 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -2,4 +2,7 @@ # Be sure to restart your server when you modify this file. -Rails.application.config.session_store :cookie_store, key: '_code_ocean_session', expire_after: 1.month +Rails.application.config.session_store :cookie_store, + key: '_code_ocean_session', + expire_after: 1.month, + path: Rails.application.config.relative_url_root diff --git a/config/webpack/environment.js b/config/webpack/environment.js index 54e50536..94f7ba80 100644 --- a/config/webpack/environment.js +++ b/config/webpack/environment.js @@ -4,9 +4,10 @@ Info for this file can be found github.com/rails/webpacker/blob/master/docs/webpack.md */ -const { environment } = require('@rails/webpacker') -const { merge } = require('webpack-merge') +const {environment} = require('@rails/webpacker') +const {merge} = require('webpack-merge') const webpack = require('webpack') +const erb = require('./loaders/erb') // Add an additional plugin of your choosing : ProvidePlugin environment.plugins.prepend('Provide', new webpack.ProvidePlugin({ @@ -24,6 +25,16 @@ environment.plugins.prepend('Provide', new webpack.ProvidePlugin({ }) ) +// This setting will change the absolute path used to refer +// external files (images, fonts, ...) in the generated assets +const relative_url_root = process.env.RAILS_RELATIVE_URL_ROOT || ''; +const public_output_path = environment.config.output.publicPath; +environment.loaders.get('file') + .use.find(item => item.loader === 'file-loader') + .options.publicPath = relative_url_root + public_output_path; + +environment.loaders.append('erb', erb) + const envConfig = module.exports = environment const aliasConfig = module.exports = { resolve: { diff --git a/config/webpack/loaders/erb.js b/config/webpack/loaders/erb.js new file mode 100644 index 00000000..8c69c6a2 --- /dev/null +++ b/config/webpack/loaders/erb.js @@ -0,0 +1,16 @@ +module.exports = { + test: /\.erb$/, + enforce: "pre", + exclude: /node_modules/, + + use: [{ + loader: "rails-erb-loader", + options: { + runner: (/^win/.test(process.platform) ? "ruby " : "") + "bin/rails runner", + env: { + ...process.env, + DISABLE_SPRING: 1, + }, + }, + }], +} diff --git a/package.json b/package.json index bde0a97d..cb3d971b 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "jstree": "^3.3.11", "opensans-webkit": "^1.1.0", "popper.js": "^1.16.1", + "rails-erb-loader": "^5.5.2", "sortablejs": "^1.14.0", "underscore": "^1.13.1", "vis": "^4.21.0", diff --git a/yarn.lock b/yarn.lock index 1fa7a7f8..5ff95dbd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4272,6 +4272,11 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= + lodash.get@^4.0: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" @@ -5879,6 +5884,14 @@ querystringify@^2.1.1: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== +rails-erb-loader@^5.5.2: + version "5.5.2" + resolved "https://registry.yarnpkg.com/rails-erb-loader/-/rails-erb-loader-5.5.2.tgz#db3fa8ac89600f09d179a1a70a2ca18c592576ea" + integrity sha512-cjQH9SuSvRPhnWkvjmmAW/S4AFVDfAtYnQO4XpKJ8xpRdZayT73iXoE+IPc3VzN03noZXhVmyvsCvKvHj4LY6w== + dependencies: + loader-utils "^1.1.0" + lodash.defaults "^4.2.0" + randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"