diff --git a/.github/workflows/migrations.yml b/.github/workflows/migrations.yml new file mode 100644 index 00000000..569bb0be --- /dev/null +++ b/.github/workflows/migrations.yml @@ -0,0 +1,85 @@ +name: Migrations + +on: + pull_request: + types: + - opened + - edited + - synchronize + - reopened + +jobs: + migrations: + timeout-minutes: 30 + runs-on: ubuntu-18.04 + services: + postgres: + image: postgres:11 + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + redis: + image: redis + ports: + - 6379:6379 + # Set health checks to wait until redis has started + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + + steps: + # Clone repo and checkout merge commit parent (PR target commit) + - uses: actions/checkout@v2 + with: + fetch-depth: 2 + + - run: git checkout HEAD^ + + # Install ruby + - uses: actions/setup-ruby@v1 + with: + ruby-version: 2.6 + + # Retrieve gem cache for merge commit parent + - uses: actions/cache@v2 + with: + path: vendor/bundle + key: ${{ runner.os }}-gems-parent-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-gems-parent- + + # Install gems, create data to be migrated and revert to PR merge commit + - name: Create data to be migrated + env: + OXE_DB_USER: postgres + OXE_DB_PASS: postgres + run: | + gem install bundler + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 + bundle exec rake db:create db:schema:load db:seed --trace + bundle exec rails runner '10.times { FactoryBot.create :exercise }' + git checkout - + + # Retrieve gem cache for PR merge commit + - uses: actions/cache@v2 + with: + path: vendor/bundle + key: ${{ runner.os }}-gems-pr-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-gems-pr- + + # Migrate the data + - name: Migrate + env: + OXE_DB_USER: postgres + OXE_DB_PASS: postgres + run: | + bundle install --jobs 4 --retry 3 + bundle exec rake db:migrate diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..3e659ee1 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,55 @@ +name: Tests + +on: + pull_request: + push: + branches: + - main + schedule: + - cron: '0 0 * * 0' # weekly + +jobs: + tests: + timeout-minutes: 30 + runs-on: ubuntu-18.04 + services: + postgres: + image: postgres:11 + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + env: + POSTGRES_USER: postgres + POSTGRES_DB: ci_test + POSTGRES_PASSWORD: postgres + strategy: + matrix: + tests: + - name: 'assets' + args: 'ASSETS=true' + - name: 'tests' + args: 'ASSETS=false' + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-ruby@v1 + with: + ruby-version: 2.6 + - uses: actions/cache@v2 + with: + path: vendor/bundle + key: ${{ runner.os }}-gems-pr-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-gems-pr- + - name: Test + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + OXT_DB_USER: postgres + OXT_TEST_DB: ci_test + OXT_DB_PASS: postgres + RAILS_ENV: test + run: | + gem install bundler + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 + ${{ matrix.tests.args }} ./bin/ci diff --git a/.gitignore b/.gitignore index bde4434e..b0b2b1be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,41 +1,82 @@ -# See http://help.github.com/ignore-files/ for more about ignoring files. -# -# If you find yourself ignoring temporary files generated by your text editor -# or operating system, you probably want to add a global ignore instead: -# git config --global core.excludesfile ~/.gitignore_global +*.rbc +capybara-*.html +.rspec +/db/*.sqlite3 +/db/*.sqlite3-journal +/db/*.sqlite3-[0-9]* +/public/system +/coverage/ +/spec/tmp +*.orig +rerun.txt +pickle-email-*.html -# Ignore bundler config +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# TODO Comment out this rule if you are OK with secrets being uploaded to the repo +config/initializers/secret_token.rb +config/master.key + +# Only include if you have production secrets in this file, which is no longer a Rails default +# config/secrets.yml + +# dotenv, dotenv-rails +# TODO Comment out these rules if environment variables can be committed +.env +.env.* + +## Environment normalization: /.bundle +/vendor/bundle -# Ignore installed gems -vendor +# these should all be checked in to normalize the environment: +# Gemfile.lock, .ruby-version, .ruby-gemset -# Ignore the default SQLite database and yaml_db database files -/db/*.sqlite3* -/db/data.yml* +# unless supporting rvm < 1.11.0 or doing something fancy, ignore this: +.rvmrc -# Ignore all logfiles and tempfiles. -/log/*.log -/tmp/**/* +# if using bower-rails ignore default bower_components path bower.json files +/vendor/assets/bower_components +*.bowerrc +bower.json + +# Ignore pow environment settings +.powenv + +# Ignore Byebug command history file. +.byebug_history + +# Ignore node_modules +node_modules/ -# Ignore files containing keys and passwords -config/*settings.yml +# Ignore precompiled javascript packs +/public/packs +/public/packs-test +/public/assets -# Ignore compiled assets and uploaded files -public/assets/**/* -public/attachments/**/* +# Ignore yarn files +/yarn-error.log +yarn-debug.log* +.yarn-integrity -# Ignore vagrant and berkshelf files -.vagrant* -.vagrant_setup.json -Berksfile.lock +# Ignore uploaded files in development +/storage/* +!/storage/.keep +/public/uploads + +# Ignore attached files in development +/public/attachments # Ignore Cucumber and RSpec failure information cucumber_rerun.txt rspec.failures -# Ignore brakeman reports -brakeman.html +# Ignore webdrivers lock file +.webdrivers_update # Ignore coverage reports coverage/* @@ -43,15 +84,11 @@ coverage/* # Ignore ERD diagrams erd*.pdf -# Ignore misc files -notes +# Ignore brakeman reports +brakeman.html -# Ignore gedit and OSX temp files +# Ignore editor temp files *~ -*.DS_Store - -# Ignore dotenv env variable definitions -.env -# Ignore byebug history -.byebug_history +# Ignore OS X's Desktop Service Store files +*.DS_Store diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index eb9d28bc..00000000 --- a/.travis.yml +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2011-2019 Rice University. Licensed under the Affero General Public -# License version 3 or later. See the COPYRIGHT file for details. - -branches: - only: - - master -dist: xenial -language: ruby -addons: - postgresql: 9.6 -services: redis-server -env: - global: - - OXE_DB_USER=postgres - - OXE_DB_PASS= - - OXE_TEST_DB=travis_ci_test - - PARALLEL_TEST_PROCESSORS=2 - matrix: - - ASSETS=true - - TAG=speed:all -rvm: 2.6.1 -cache: bundler -before_install: - - gem install bundler:2.0.1 -bundler_args: --without production --retry=6 -before_script: - - bundle exec rake parallel:create parallel:load_schema parallel:seed --trace -script: - - "[[ -n $ASSETS ]] && bundle exec rake assets:precompile || [[ -z $ASSETS ]] && true" - - >- - [[ -n $TAG ]] && bundle exec parallel_rspec ./spec --test-options "--tag $TAG" || - [[ -z $TAG ]] && true diff --git a/Gemfile b/Gemfile index 08bb5431..3d26159e 100644 --- a/Gemfile +++ b/Gemfile @@ -55,9 +55,6 @@ gem 'sanitize' # Utilities for OpenStax websites gem 'openstax_utilities' -# Cron job scheduling -gem 'whenever' - # Talks to Accounts (latest version is broken) gem 'omniauth-oauth2' @@ -114,14 +111,18 @@ gem 'scout_apm', '~> 3.0.pre28' # PostgreSQL database gem 'pg' +# Support systemd Type=notify services for puma +gem 'sd_notify', require: false + +# Use the puma webserver +gem 'puma' + +# Prevent server memory from growing until OOM +gem 'puma_worker_killer' + # HTTP requests gem 'httparty' -gem 'a15k_client', - git: 'https://github.com/a15k/mothership.git', - glob: 'clients/1.0.0/ruby/*gemspec', - branch: 'master' - # Notify developers of Exceptions in production gem 'openstax_rescue_from' @@ -151,10 +152,10 @@ gem 'bootsnap', '~> 1.4.0', require: false # Bulk inserts and upserts gem 'activerecord-import' -group :development, :test do - # Get env variables from .env file - gem 'dotenv-rails' +# Get env variables from .env file +gem 'dotenv-rails' +group :development, :test do # Run specs in parallel gem 'parallel_tests' @@ -221,11 +222,9 @@ group :test do end group :production do - # Unicorn production server - gem 'unicorn' - - # Unicorn worker killer - gem 'unicorn-worker-killer' + # Used to fetch secrets from the AWS parameter store and secrets manager + gem 'aws-sdk-ssm', require: false + gem 'aws-sdk-secretsmanager', require: false # AWS SES gem 'aws-ses', '~> 0.6.0', require: 'aws/ses' diff --git a/Gemfile.lock b/Gemfile.lock index a8cafe0f..d39ae1d5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,13 +1,3 @@ -GIT - remote: https://github.com/a15k/mothership.git - revision: c627052493d59e147969257f245f4093990ae357 - branch: master - glob: clients/1.0.0/ruby/*gemspec - specs: - a15k_client (1.0.0) - json - typhoeus (~> 1.0, >= 1.0.1) - GEM remote: https://rubygems.org/ specs: @@ -36,10 +26,10 @@ GEM erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - active_attr (0.15.0) - actionpack (>= 3.0.2, < 6.1) - activemodel (>= 3.0.2, < 6.1) - activesupport (>= 3.0.2, < 6.1) + active_attr (0.15.1) + actionpack (>= 3.0.2, < 6.2) + activemodel (>= 3.0.2, < 6.2) + activesupport (>= 3.0.2, < 6.2) activejob (5.2.4.4) activesupport (= 5.2.4.4) globalid (>= 0.3.6) @@ -69,11 +59,29 @@ GEM ast (2.4.0) autoprefixer-rails (9.6.0) execjs + aws-eventstream (1.1.0) + aws-partitions (1.417.0) + aws-sdk-autoscaling (1.53.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-core (3.111.2) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.239.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1.0) + aws-sdk-secretsmanager (1.43.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-ssm (1.95.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) aws-ses (0.6.0) builder mail (> 2.2.5) mime-types xml-simple + aws-sigv4 (1.2.2) + aws-eventstream (~> 1, >= 1.0.2) bindex (0.7.0) bootsnap (1.4.4) msgpack (~> 1.0) @@ -90,7 +98,6 @@ GEM cheat (1.3.3) pager (~> 1.0) choice (0.2.0) - chronic (0.10.2) chunky_png (1.3.11) codeclimate-test-reporter (1.0.9) simplecov (<= 0.13) @@ -122,7 +129,7 @@ GEM compass (~> 1.0.0) sass-rails (< 5.1) sprockets (< 4.0) - concurrent-ruby (1.1.7) + concurrent-ruby (1.1.8) crass (1.0.6) daemons (1.3.1) database_cleaner (1.7.0) @@ -144,9 +151,7 @@ GEM execjs eco-source (1.1.0.rc.1) ejs (1.1.1) - erubi (1.9.0) - ethon (0.12.0) - ffi (>= 1.3.0) + erubi (1.10.0) eventmachine (1.2.7) exception_notification (4.3.0) actionmailer (>= 4.0, < 6) @@ -187,17 +192,19 @@ GEM fog-core nokogiri (>= 1.5.11, < 2.0.0) formatador (0.2.5) - get_process_mem (0.2.3) + get_process_mem (0.2.7) + ffi (~> 1.0) globalid (0.4.2) activesupport (>= 4.2.0) hashie (3.6.0) httparty (0.17.0) mime-types (~> 3.0) multi_xml (>= 0.5.2) - i18n (1.8.5) + i18n (1.8.7) concurrent-ruby (~> 1.0) ipaddress (0.8.3) jaro_winkler (1.5.2) + jmespath (1.4.0) jquery-rails (4.3.3) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) @@ -207,7 +214,6 @@ GEM json (2.3.1) jwt (2.2.1) keyword_search (1.5.0) - kgio (2.11.2) lev (10.1.0) actionpack (>= 4.2) active_attr @@ -223,7 +229,7 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.7.0) + loofah (2.9.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) @@ -241,7 +247,7 @@ GEM mini_portile2 (2.5.0) mini_racer (0.2.6) libv8 (>= 6.9.411) - minitest (5.14.2) + minitest (5.14.3) msgpack (1.2.10) multi_json (1.13.1) multi_xml (0.6.0) @@ -295,12 +301,13 @@ GEM rails (>= 3.0) openstax_rescue_from (4.0.0) rails (>= 3.1, < 6.0) - openstax_utilities (4.3.0) + openstax_utilities (4.5.1) + aws-sdk-autoscaling faraday faraday-http-cache keyword_search lev - rails (~> 5.2) + rails (>= 5.0, < 7.0) request_store pager (1.0.1) parallel (1.17.0) @@ -310,6 +317,11 @@ GEM ast (~> 2.4.0) pg (1.1.4) public_suffix (3.1.0) + puma (5.1.0) + nio4r (~> 2.0) + puma_worker_killer (0.3.1) + get_process_mem (~> 0.2) + puma (>= 2.7) racc (1.5.2) rack (2.2.3) rack-test (1.1.0) @@ -345,8 +357,7 @@ GEM rake (>= 0.8.7) thor (>= 0.19.0, < 2.0) rainbow (3.0.0) - raindrops (0.19.0) - rake (13.0.1) + rake (13.0.3) rb-fsevent (0.10.3) rb-inotify (0.10.0) ffi (~> 1.0) @@ -371,7 +382,7 @@ GEM representable (3.0.0) declarative (~> 0.0.5) uber (~> 0.0.15) - request_store (1.4.1) + request_store (1.5.0) rack (>= 1.4) responders (3.0.0) actionpack (>= 5.0) @@ -440,6 +451,7 @@ GEM ffi (~> 1.9) rake scout_apm (3.0.0.pre28) + sd_notify (0.1.0) sentry-raven (2.9.0) faraday (>= 0.7.6, < 1.0) shoulda-matchers (4.0.1) @@ -465,7 +477,7 @@ GEM daemons (~> 1.0, >= 1.0.9) eventmachine (~> 1.0, >= 1.0.4) rack (>= 1, < 3) - thor (1.0.1) + thor (1.1.0) thread_safe (0.3.6) tilt (2.0.9) timecop (0.9.1) @@ -477,20 +489,12 @@ GEM turbolinks (5.2.0) turbolinks-source (~> 5.2) turbolinks-source (5.2.0) - typhoeus (1.3.1) - ethon (>= 0.9.0) - tzinfo (1.2.7) + tzinfo (1.2.9) thread_safe (~> 0.1) uber (0.0.15) uglifier (4.1.20) execjs (>= 0.3.0, < 3) unicode-display_width (1.6.0) - unicorn (5.5.1) - kgio (~> 2.6) - raindrops (~> 0.7) - unicorn-worker-killer (0.4.4) - get_process_mem (~> 0) - unicorn (>= 4, < 6) web-console (3.7.0) actionview (>= 5.0) activemodel (>= 5.0) @@ -499,20 +503,19 @@ GEM websocket-driver (0.7.3) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - whenever (0.11.0) - chronic (>= 0.6.3) xml-simple (1.1.5) PLATFORMS ruby DEPENDENCIES - a15k_client! activerecord-import acts_as_votable addressable apipie-rails autoprefixer-rails + aws-sdk-secretsmanager + aws-sdk-ssm aws-ses (~> 0.6.0) bootsnap (~> 1.4.0) bootstrap-sass @@ -556,6 +559,8 @@ DEPENDENCIES openstax_utilities parallel_tests pg + puma + puma_worker_killer railroady rails (~> 5.2.3) rails-erd @@ -572,6 +577,7 @@ DEPENDENCIES sanitize sass-rails (~> 5.0) scout_apm (~> 3.0.pre28) + sd_notify sentry-raven shoulda-matchers sortability @@ -579,10 +585,7 @@ DEPENDENCIES timecop turbolinks uglifier (>= 1.3.0) - unicorn - unicorn-worker-killer web-console - whenever BUNDLED WITH 2.1.4 diff --git a/README.md b/README.md index 6edfc7f5..10d37723 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ OpenStax Exercises ================== -[](https://travis-ci.org/openstax/exercises) -[](https://codeclimate.com/github/openstax/exercises) -[](https://codecov.io/gh/openstax/exercises) +[](https://github.com/openstax/exercises/actions?query=workflow:Tests) +[](https://github.com/openstax/exercises/actions?query=workflow:Migrations) +[](https://codecov.io/gh/openstax/exercises) OpenStax Exercises is an open homework and test question bank, where questions are written by the community and access is free. Successor to Quadbase. diff --git a/app/controllers/admin/a15k_controller.rb b/app/controllers/admin/a15k_controller.rb deleted file mode 100644 index 0e4ba0f7..00000000 --- a/app/controllers/admin/a15k_controller.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'a15k/html_preview' -require 'a15k/exporter' - -module Admin - class A15kController < BaseController - - layout false - - def preview - @exercise = Exercise.find(params[:id]) - end - - def format - @format_data = A15k::Exporter.new.local_format_data - end - - end -end - diff --git a/app/controllers/static_pages_controller.rb b/app/controllers/static_pages_controller.rb index 9a911150..af676470 100644 --- a/app/controllers/static_pages_controller.rb +++ b/app/controllers/static_pages_controller.rb @@ -1,18 +1,7 @@ class StaticPagesController < ApplicationController - respond_to :html - skip_before_action :authenticate_user!, - only: [:about, :contact, :copyright, :developers, - :help, :privacy, :share, :status, :terms] - fine_print_skip :general_terms_of_use, :privacy_policy, - only: [:about, :contact, :copyright, :developers, - :help, :privacy, :share, :status, :terms] - - # GET /status - # Used by AWS (and others) to make sure the site is still up - def status - head :ok - end + skip_before_action :authenticate_user! + fine_print_skip :general_terms_of_use, :privacy_policy end diff --git a/app/uploaders/asset_uploader.rb b/app/uploaders/asset_uploader.rb index 8c1e73f9..e0e183df 100644 --- a/app/uploaders/asset_uploader.rb +++ b/app/uploaders/asset_uploader.rb @@ -68,7 +68,7 @@ def cache_dir end def store_dir - 'attachments' + Rails.application.secrets.environment_name end def filename diff --git a/app/views/admin/a15k/format.html.erb b/app/views/admin/a15k/format.html.erb deleted file mode 100644 index 4de06338..00000000 --- a/app/views/admin/a15k/format.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= @format_data["specification"].html_safe %> diff --git a/app/views/admin/a15k/preview.html.erb b/app/views/admin/a15k/preview.html.erb deleted file mode 100644 index 1810c6da..00000000 --- a/app/views/admin/a15k/preview.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= A15k::HtmlPreview.new(@exercise).generate.html_safe %> diff --git a/bin/bundle b/bin/bundle index f19acf5b..a71368e3 100755 --- a/bin/bundle +++ b/bin/bundle @@ -1,3 +1,114 @@ #!/usr/bin/env ruby -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) -load Gem.bin_path('bundler', 'bundle') +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../../Gemfile", __FILE__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_version + @bundler_version ||= + env_var_version || cli_arg_version || + lockfile_version + end + + def bundler_requirement + return "#{Gem::Requirement.default}.a" unless bundler_version + + bundler_gem_version = Gem::Version.new(bundler_version) + + requirement = bundler_gem_version.approximate_recommendation + + return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0") + + requirement += ".a" if bundler_gem_version.prerelease? + + requirement + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/bin/ci b/bin/ci new file mode 100755 index 00000000..6e6c99cc --- /dev/null +++ b/bin/ci @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -eo pipefail + +if [[ -n "${ASSETS}" ]]; then + bundle exec rake assets:precompile +elif [[ -n "${TAG}" ]]; then + bundle exec rake parallel:create parallel:load_schema parallel:seed --trace + bundle exec parallel_rspec ./spec --test-options "--tag $TAG" +else + echo Must be called with ENV of ASSETS or TAG set + exit 1 +fi diff --git a/bin/puma b/bin/puma new file mode 100755 index 00000000..880935b2 --- /dev/null +++ b/bin/puma @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'puma' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +bundle_binstub = File.expand_path("../bundle", __FILE__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("puma", "puma") diff --git a/bin/pumactl b/bin/pumactl new file mode 100755 index 00000000..b698e6e9 --- /dev/null +++ b/bin/pumactl @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'pumactl' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +bundle_binstub = File.expand_path("../bundle", __FILE__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("puma", "pumactl") diff --git a/config.ru b/config.ru index cc6d40ab..19885caf 100644 --- a/config.ru +++ b/config.ru @@ -1,20 +1,5 @@ # This file is used by Rack-based servers to start the application. -if ENV['RAILS_ENV'] == 'production' - require 'unicorn/worker_killer' +require File.expand_path('config/environment', __dir__) - max_request_min = 500 - max_request_max = 600 - - # Max requests per worker - use Unicorn::WorkerKiller::MaxRequests, max_request_min, max_request_max - - oom_min = (240) * (1024**2) - oom_max = (260) * (1024**2) - - # Max memory size (RSS) per worker - use Unicorn::WorkerKiller::Oom, oom_min, oom_max -end - -require ::File.expand_path('../config/environment', __FILE__) -run Exercises::Application +run Rails.application diff --git a/config/application.rb b/config/application.rb index 986c0a83..079ec756 100644 --- a/config/application.rb +++ b/config/application.rb @@ -39,10 +39,15 @@ class Application < Rails::Application # config.i18n.default_locale = :de ActiveSupport.escape_html_entities_in_json = false + redis_secrets = secrets.redis + redis_secrets[:url] ||= "rediss://#{ + ":#{redis_secrets[:password]}@" unless redis_secrets[:password].blank? }#{ + redis_secrets[:host]}#{":#{redis_secrets[:port]}" unless redis_secrets[:port].blank?}/#{ + "/#{redis_secrets[:db]}" unless redis_secrets[:db].blank?}" + # Set the default cache store to Redis # This setting cannot be set from an initializer # See https://github.com/rails/rails/issues/10908 - redis_secrets = secrets[:redis] config.cache_store = :redis_store, { url: redis_secrets[:url], namespace: redis_secrets[:namespaces][:cache], diff --git a/config/cable.yml b/config/cable.yml index 1ac654de..69d50ecb 100644 --- a/config/cable.yml +++ b/config/cable.yml @@ -6,5 +6,5 @@ test: production: adapter: redis - url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + url: <%= Rails.application.secrets.redis[:url] %> channel_prefix: exercises_production diff --git a/config/database.yml b/config/database.yml index 352918e2..7be1c7e2 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1,8 +1,10 @@ default: &default adapter: postgresql host: <%= ENV['OXE_DB_HOST'] || 'localhost' %> + port: <%= ENV['OXE_DB_PORT'] || 5432 %> username: <%= ENV['OXE_DB_USER'] || 'ox_exercises' %> password: <%= ENV['OXE_DB_PASS'] || 'ox_exercises' %> + pool: <%= ENV.fetch('RAILS_MAX_THREADS', 5).to_i %> development: <<: *default @@ -20,5 +22,12 @@ test: reaping_frequency: 0 # 0 = disabled - incompatible with our DatabaseCleaner config production: - <<: *default - database: <%= ENV['OXE_PROD_DB'] || 'ox_exercises' %> + adapter: postgresql + host: <%= ENV['RDS_HOST'] %> + port: <%= ENV['RDS_PORT'] %> + username: <%= ENV['RDS_USERNAME'] %> + password: <%= ENV['RDS_PASSWORD'] %> + database: <%= ENV['RDS_DATABASE'] %> + pool: <%= ENV['RAILS_MAX_THREADS'] %> + sslmode: verify-full + sslrootcert: /etc/ssl/certs/rds.pem diff --git a/config/environments/production.rb b/config/environments/production.rb index 6815310a..da4bb8b0 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -60,15 +60,6 @@ # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. # config.assets.precompile += %w( search.js ) - # Ignore bad email addresses and do not raise email delivery errors. - # Set this to true and configure the email server for immediate delivery to raise delivery errors. - # config.action_mailer.raise_delivery_errors = false - - config.action_mailer.delivery_method = :ses - config.action_mailer.default_url_options = { - protocol: 'https', host: Rails.application.secrets.mail_site_url - } - # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true @@ -79,17 +70,25 @@ # Disable automatic flushing of the log to improve performance. # config.autoflush_log = false + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = ::Logger::Formatter.new + + # Log to STDOUT and let systemd/journald handle the logs + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + # Lograge configuration (one-line logs in production) config.lograge.enabled = true - config.log_tags = [:remote_ip] + config.log_tags = [ :remote_ip ] config.lograge.custom_options = ->(event) do - params = event.payload[:params].reject do |k| - ['controller', 'action', 'format'].include? k - end - { "params" => params } + { + 'params' => event.payload[:params].reject do |k| + ['controller', 'action', 'format'].include? k + end + } end - config.lograge.ignore_actions = ["static_pages#status"] - + config.lograge.ignore_actions = ['static_pages#status'] # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false diff --git a/config/initializers/a15k.rb b/config/initializers/a15k.rb deleted file mode 100644 index 1301766d..00000000 --- a/config/initializers/a15k.rb +++ /dev/null @@ -1,7 +0,0 @@ -secrets = Rails.application.secrets[:a15k] - -A15kClient.configure do |c| - c.scheme = secrets[:scheme] - c.host = secrets[:host] - c.api_key['Authorization'] = secrets[:access_token] -end unless secrets.nil? diff --git a/config/initializers/amazon_ses.rb b/config/initializers/amazon_ses.rb deleted file mode 100644 index 9781ccae..00000000 --- a/config/initializers/amazon_ses.rb +++ /dev/null @@ -1,11 +0,0 @@ -if Rails.env.production? - secrets = Rails.application.secrets[:aws][:ses] - - ActionMailer::Base.add_delivery_method( - :ses, - AWS::SES::Base, - access_key_id: secrets[:access_key_id], - secret_access_key: secrets[:secret_access_key], - server: secrets[:endpoint_server] - ) -end diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb index 145199b0..8f0cbfac 100644 --- a/config/initializers/carrierwave.rb +++ b/config/initializers/carrierwave.rb @@ -5,29 +5,31 @@ config.enable_processing = !Rails.env.test? # Upload to AWS only in the production environment - config.storage = if Rails.env.production? - secrets = Rails.application.secrets[:aws][:s3] - - config.asset_host = secrets[:asset_host] - + if Rails.env.development? || ActiveModel::Type::Boolean.new.cast(ENV.fetch('DISABLE_S3', false)) + config.storage = :file + else config.fog_attributes = { 'Cache-Control' => 'max-age=31536000' } + config.fog_provider = 'fog/aws' + config.fog_public = false + config.fog_authenticated_url_expiration = 1.hour - config.fog_directory = secrets[:bucket_name] + s3_secrets = Rails.application.secrets.aws[:s3] - config.fog_provider = 'fog/aws' + config.asset_host = "https://#{s3_secrets[:uploads_bucket_name]}.s3.amazonaws.com" + config.fog_directory = s3_secrets[:uploads_bucket_name] - fog_credentials = secrets[:access_key_id].blank? ? \ - { use_iam_profile: true } : \ - { aws_access_key_id: secrets[:access_key_id], - aws_secret_access_key: secrets[:secret_access_key] } + fog_credentials = s3_secrets[:access_key_id].blank? ? + { use_iam_profile: true } : + { + aws_access_key_id: s3_secrets[:access_key_id], + aws_secret_access_key: s3_secrets[:secret_access_key] + } config.fog_credentials = fog_credentials.merge( provider: 'AWS', - region: secrets[:region], - endpoint: secrets[:endpoint_server] + region: s3_secrets[:region] ) - :fog - else - :file + # This line must be after config.fog_credentials= + config.storage = Rails.env.production? ? :fog : :file end end diff --git a/config/initializers/openstax_accounts.rb b/config/initializers/openstax_accounts.rb index 17222adc..ca7aeb29 100644 --- a/config/initializers/openstax_accounts.rb +++ b/config/initializers/openstax_accounts.rb @@ -1,6 +1,6 @@ require 'user_mapper' -secrets = Rails.application.secrets[:openstax][:accounts] +secrets = Rails.application.secrets.openstax[:accounts] # By default, stub unless in the production environment stub = secrets[:stub].nil? ? !Rails.env.production? : secrets[:stub] @@ -12,4 +12,4 @@ config.enable_stubbing = stub config.logout_via = :delete config.account_user_mapper = UserMapper -end +end if secrets[:url] diff --git a/config/initializers/openstax_utilities.rb b/config/initializers/openstax_utilities.rb new file mode 100644 index 00000000..f77ae9c9 --- /dev/null +++ b/config/initializers/openstax_utilities.rb @@ -0,0 +1,5 @@ +OpenStax::Utilities.configure do |config| + config.status_authenticate = -> do + raise SecurityTransgression unless current_user.is_administrator? + end +end diff --git a/config/initializers/rescue_from.rb b/config/initializers/rescue_from.rb index c88aed4c..60f3b5e4 100644 --- a/config/initializers/rescue_from.rb +++ b/config/initializers/rescue_from.rb @@ -1,12 +1,10 @@ require 'openstax_rescue_from' -secrets = Rails.application.secrets -exception_secrets = secrets.exception OpenStax::RescueFrom.configure do |config| config.raise_exceptions = Rails.application.config.consider_all_requests_local config.app_name = 'Exercises' - config.contact_name = exception_secrets[:contact_name] + config.contact_name = Rails.application.secrets.exception_contact_name # Notify devs using sentry-raven config.notify_proc = ->(proxy, controller) do diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb new file mode 100644 index 00000000..659d1f11 --- /dev/null +++ b/config/initializers/sentry.rb @@ -0,0 +1,13 @@ +Raven.configure do |config| + sentry_secrets = Rails.application.secrets.sentry + + config.dsn = sentry_secrets[:dsn] + config.current_environment = Rails.application.secrets.environment_name + + # Send POST data and cookies to Sentry + config.processors -= [ Raven::Processor::Cookies, Raven::Processor::PostData ] + config.release = sentry_secrets[:release] + + # Don't log "Sentry is ready" message + config.silence_ready = true +end if Rails.env.production? diff --git a/config/puma.rb b/config/puma.rb index 1e19380d..16e95674 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,56 +1,87 @@ +require 'rails' +require 'active_model' +require 'dotenv/rails-now' + +APP_DIR = File.expand_path('..', __dir__) +directory APP_DIR + +tag 'OpenStax Exercises Puma' + +NUM_WORKERS = ENV.fetch('WEB_CONCURRENCY') { Etc.nprocessors }.to_i + +worker_timeout ENV.fetch('WORKER_TIMEOUT', 60).to_i + +stdout_redirect( + ENV.fetch('STDOUT_LOGFILE', "#{APP_DIR}/log/puma.stdout.log"), + ENV.fetch('STDERR_LOGFILE', "#{APP_DIR}/log/puma.stderr.log"), + true +) if ActiveModel::Type::Boolean.new.cast(ENV.fetch('REDIRECT_STDOUT', false)) + +before_fork do + require 'puma_worker_killer' + + PumaWorkerKiller.config do |config| + # Restart workers when they start consuming more than 1G each + config.ram = ENV.fetch('MAX_MEMORY') do + ENV.fetch('MAX_WORKER_MEMORY', 256).to_i * NUM_WORKERS + end.to_i + + config.frequency = 10 + + config.percent_usage = 0.75 + + config.rolling_restart_frequency = false + + config.reaper_status_logs = false + end + + PumaWorkerKiller.start +end + +# https://github.com/rails/rails/blob/master/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt + # Puma can serve each request in a thread from an internal thread pool. # The `threads` method setting takes two numbers: a minimum and maximum. # Any libraries that use thread pools should be configured to match # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum; this matches the default thread size of Active Record. # -threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } -threads threads_count, threads_count +max_threads = ENV.fetch('RAILS_MAX_THREADS', 5).to_i +threads ENV.fetch('RAILS_MIN_THREADS', max_threads).to_i, max_threads -# Specifies the `port` that Puma will listen on to receive requests; default is 3000. -# -port ENV.fetch("PORT") { 3000 } +if ENV['SOCKET'] + # Specifies the `socket` to which Puma will bind to receive requests. + # + bind ENV['SOCKET'] +else + # Specifies the `port` that Puma will listen on to receive requests; default is 3000. + # + port ENV.fetch('PORT', 3000) +end # Specifies the `environment` that Puma will run in. # -environment ENV.fetch("RAILS_ENV") { "development" } +environment ENV.fetch('RAILS_ENV', 'development') + +# Specifies the `pidfile` that Puma will use. +# +pidfile ENV.fetch('PIDFILE', 'tmp/pids/puma.pid') # Specifies the number of `workers` to boot in clustered mode. -# Workers are forked webserver processes. If using threads and workers together +# Workers are forked web server processes. If using threads and workers together # the concurrency of the application would be max `threads` * `workers`. # Workers do not work on JRuby or Windows (both of which do not support # processes). # -# workers ENV.fetch("WEB_CONCURRENCY") { 2 } +workers NUM_WORKERS # Use the `preload_app!` method when specifying a `workers` number. # This directive tells Puma to first boot the application and load code # before forking the application. This takes advantage of Copy On Write -# process behavior so workers use less memory. If you use this option -# you need to make sure to reconnect any threads in the `on_worker_boot` -# block. -# -# preload_app! - -# If you are preloading your application and using Active Record, it's -# recommended that you close any connections to the database before workers -# are forked to prevent connection leakage. -# -# before_fork do -# ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord) -# end - -# The code in the `on_worker_boot` will be called if you are using -# clustered mode by specifying a number of `workers`. After each worker -# process is booted, this block will be run. If you are using the `preload_app!` -# option, you will want to use this block to reconnect to any threads -# or connections that may have been created at application boot, as Ruby -# cannot share connections between processes. -# -# on_worker_boot do -# ActiveRecord::Base.establish_connection if defined?(ActiveRecord) -# end +# process behavior so workers use less memory. # +preload_app! if ActiveModel::Type::Boolean.new.cast(ENV.fetch('PRELOAD_APP', false)) # Allow puma to be restarted by `rails restart` command. +# plugin :tmp_restart diff --git a/config/routes.rb b/config/routes.rb index 848dc743..2a20f426 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -10,9 +10,7 @@ get :developers get :help get :privacy - get :publishing get :share - get :status get :terms end @@ -51,6 +49,7 @@ mount OpenStax::Accounts::Engine => :accounts mount FinePrint::Engine => :fine_print + mount OpenStax::Utilities::Engine => :status use_doorkeeper do controllers applications: :'oauth/applications' @@ -83,11 +82,6 @@ get :collaborators end end - - resources :a15k, only: [] do - get :preview, on: :member - get :format, on: :collection - end end namespace :dev do diff --git a/config/schedule.rb b/config/schedule.rb deleted file mode 100644 index 02704c82..00000000 --- a/config/schedule.rb +++ /dev/null @@ -1,17 +0,0 @@ -# Learn more: http://github.com/javan/whenever - -bundle_command = ENV['BUNDLE_COMMAND'] || 'bundle exec' - -set :bundle_command, bundle_command -set :runner_command, "#{bundle_command} rails runner" - -# Server time is UTC; times below are interpreted that way. -# Ideally we'd have a better way to specify times relative to Central -# time, independent of the server time. Maybe there's something here: -# * https://github.com/javan/whenever/issues/481 -# * https://github.com/javan/whenever/pull/239 - -# Add this in once a15k is stable -# every 1.day do -# rake "a15k:export" -# end diff --git a/config/scout_apm.yml b/config/scout_apm.yml new file mode 100644 index 00000000..8941ec37 --- /dev/null +++ b/config/scout_apm.yml @@ -0,0 +1,37 @@ +--- +# This configuration file is used for Scout APM. +# Environment variables can also be used to configure Scout. See our help docs at http://help.apm.scoutapp.com#environment-variables for more information. +development: + monitor: false + +test: + monitor: false + +production: + # name: application name in APM Web UI + # - Default: the application names comes from the Rails or Sinatra class name + name: exercises (<%= ENV['ENVIRONMENT_NAME'] %>) + + # key: Your Organization key for Scout APM. Found on the settings screen. + # - Default: none + key: <%= ENV['SCOUT_LICENSE_KEY'] %> + + # monitor: Enable Scout APM or not + # - Default: false + # - Valid Options: true, false + monitor: <%= ActiveModel::Type::Boolean.new.cast(ENV.fetch('SCOUT_MONITOR', false)) %> + + # log_level: Verboseness of logs. + # - Default: 'info' + # - Valid Options: debug, info, warn, error + # log_level: debug + + # log_file_path: The path to the scout_apm.log log file directory. Use stdout to log to STDOUT. + # - Default: "#{Rails.root}/log" + log_file_path: <%= ENV['SCOUT_LOG_FILE_PATH'] %> + + # ignore: An Array of web endpoints that Scout should not instrument. + # Routes that match the prefixed path (ex: ['/health', '/status']) will be ignored by the agent. + # - Default: [] + ignore: + - /ping diff --git a/config/secrets.yml b/config/secrets.yml index c524eb91..abe6799f 100644 --- a/config/secrets.yml +++ b/config/secrets.yml @@ -13,63 +13,80 @@ development: secret_key_base: <%= ENV['SECRET_KEY_BASE'] || '5f29848dc60f2924ac43f13fbf5088b472f16315e6274e78d9abeb9f4d6f964c' %> assets_url: <%= ENV['ASSETS_URL'] || 'http://localhost:8001/dist' %> environment_name: development - a15k: - scheme: <%= ENV['A15K_SCHEME'] || 'http' %> - host: <%= ENV['A15K_HOST'] || 'localhost:4001' %> - access_token: <%= ENV['A15K_ACCESS_TOKEN'] %> + exception_contact_name: <%= ENV['EXCEPTION_CONTACT_NAME'] || 'OpenStax' %> openstax: accounts: client_id: <%= ENV['OPENSTAX_ACCOUNTS_CLIENT_ID'] %> secret: <%= ENV['OPENSTAX_ACCOUNTS_SECRET'] %> url: <%= ENV['OPENSTAX_ACCOUNTS_URL'] || 'http://localhost:2999' %> stub: <%= ENV['OPENSTAX_ACCOUNTS_STUB'] %> - exception: - contact_name: <%= ENV['EXCEPTION_CONTACT_NAME'] || 'OpenStax' %> redis: - url: <%= ENV['REDIS_URL'] || 'redis://localhost:6379/0' %> + password: <%= ENV['REDIS_PASSWORD'] %> + host: <%= ENV['REDIS_HOST'] || 'localhost' %> + port: <%= ENV['REDIS_PORT'] %> + db: <%= ENV['REDIS_DB'] %> + url: <%= ENV['REDIS_URL'] %> namespaces: cache: <%= ENV['REDIS_NAMESPACES_CACHE'] || 'exercises-development-cache' %> test: secret_key_base: 2675b2e6d5b0cdc5474f94715980df111168fbe5ba6e76ddbe345983eaec0000 assets_url: http://localhost:8001/dist - a15k: - scheme: http - host: localhost:4001 - access_token: ~ + environment_name: test + exception_contact_name: OpenStax openstax: accounts: client_id: ~ secret: ~ url: http://localhost:2999 stub: true - environment_name: test - exception: - contact_name: OpenStax redis: - url: redis://localhost:6379/0 + password: <%= ENV['REDIS_PASSWORD'] %> + host: <%= ENV['REDIS_HOST'] || 'localhost' %> + port: <%= ENV['REDIS_PORT'] %> + db: <%= ENV['REDIS_DB'] %> + url: <%= ENV['REDIS_URL'] %> namespaces: cache: exercises-test-cache + aws: + s3: + region: us-east-1 + uploads_bucket_name: not-a-real-bucket + access_key_id: NOTAREALKEY + secret_access_key: NOTAREALSECRET # Do not keep production secrets in the repository, # instead read values from the environment. production: secret_key_base: <%= ENV['SECRET_KEY_BASE'] %> assets_url: <%= ENV['ASSETS_URL'] %> + mail_site_url: <%= ENV['MAIL_SITE_URL'] %> environment_name: <%= ENV['ENVIRONMENT_NAME'] %> - a15k: - scheme: <%= ENV['A15K_SCHEME'] %> - host: <%= ENV['A15K_HOST'] %> - access_token: <%= ENV['A15K_ACCESS_TOKEN'] %> + exception_contact_name: <%= ENV['EXCEPTION_CONTACT_NAME'] %> + release_version: <%= ENV['RELEASE_VERSION'] %> + deployment_version: <%= ENV['DEPLOYMENT_VERSION'] %> openstax: accounts: client_id: <%= ENV['OPENSTAX_ACCOUNTS_CLIENT_ID'] %> secret: <%= ENV['OPENSTAX_ACCOUNTS_SECRET'] %> url: <%= ENV['OPENSTAX_ACCOUNTS_URL'] %> stub: <%= ENV['OPENSTAX_ACCOUNTS_STUB'] %> - exception: - contact_name: <%= ENV['EXCEPTION_CONTACT_NAME'] %> redis: + password: <%= ENV['REDIS_PASSWORD'] %> + host: <%= ENV['REDIS_HOST'] %> + port: <%= ENV['REDIS_PORT'] %> + db: <%= ENV['REDIS_DB'] %> url: <%= ENV['REDIS_URL'] %> namespaces: cache: <%= ENV['REDIS_NAMESPACES_CACHE'] %> + sentry: + dsn: <%= ENV['SENTRY_DSN'] %> + release: <%= ENV['RELEASE_VERSION'] %> + aws: + aws: + s3: + region: <%= ENV['AWS_S3_REGION'] %> + exports_bucket_name: <%= ENV['AWS_S3_EXPORTS_BUCKET_NAME'] %> + uploads_bucket_name: <%= ENV['AWS_S3_UPLOADS_BUCKET_NAME'] %> + access_key_id: <%= ENV['AWS_S3_ACCESS_KEY_ID'] %> + secret_access_key: <%= ENV['AWS_S3_SECRET_ACCESS_KEY'] %> diff --git a/lib/a15k/exporter.rb b/lib/a15k/exporter.rb deleted file mode 100644 index 5a1c5b7f..00000000 --- a/lib/a15k/exporter.rb +++ /dev/null @@ -1,96 +0,0 @@ -require 'a15k/html_preview' - -module A15k - class Exporter - - class CreateAssessmentError < StandardError; end - - def initialize - @outcomes = { - success_count: 0, - failure_info: [] - } - end - - def run - format = make_sure_format_is_uploaded_and_return - - Exercise.published # Select only published exercises - .can_release_to_a15k # That have been marked as releaseable to a15k - .not_released_to_a15k # And that haven't yet been released - .find_each do |exercise| # iterate through them - - begin - export_one_exercise(exercise, format) - - @outcomes[:success_count] += 1 # the rest is error handling and logging - rescue A15kClient::ApiError => ee - message = JSON.parse(ee.response_body)["message"] - @outcomes[:failure_info].push({uid: exercise.uid, message: message}) - rescue CreateAssessmentError => ee - @outcomes[:failure_info].push({uid: exercise.uid, message: ee.message}) - end - - end - - @outcomes - end - - def make_sure_format_is_uploaded_and_return - # See if it is already uploaded - format = formats_api.get_formats - .data - .find{|format| format.identifier == local_format_data['identifier']} - - # If it is uploaded, return it; otherwise upload it via the API and return it - format || formats_api.create_format(local_format_data).data - end - - def local_format_data - @local_format_data ||= YAML.load_file Rails.root.join('lib/a15k', 'format.yml') - end - - def export_one_exercise(exercise, format) - # Get the exercise JSON; we toss out "community solutions" for licensing - # reasons. - - exercise_data = Api::V1::Exercises::Representer.new(exercise).to_hash(user_options: {can_view_solutions: true}) - exercise_data['questions'].each do |question_data| - question_data.delete('community_solutions') - end - - # Make the API call - reply = assessments_api.create_assessment( - source_identifier: exercise.publication_group.uuid, # the shared UUID among versions - source_version: exercise.publication.version, # this version's version number - variants: [ # we don't have generative assessments, - { # so only one 'variant' - format_id: format.id, - content: exercise_data.to_json, - preview_html: A15k::HtmlPreview.new(exercise).generate, - } - ], - metadata: { - tags: exercise.tags.to_a - } - ) - - raise(CreateAssessmentError, reply.message) if !reply.success - - # Store the A15k identifier and version locally to let us report on them later - exercise.update_attributes( - a15k_identifier: reply.data.a15k_identifier, - a15k_version: reply.data.a15k_version - ) - end - - def formats_api - @formats_api ||= A15kClient::FormatsApi.new # the auto-generated Ruby API client - end - - def assessments_api - @assessments_api ||= A15kClient::AssessmentsApi.new # the auto-generated Ruby API client - end - - end -end diff --git a/lib/a15k/format.yml b/lib/a15k/format.yml deleted file mode 100644 index 80041683..00000000 --- a/lib/a15k/format.yml +++ /dev/null @@ -1,47 +0,0 @@ -identifier: openstax_v1 -name: OpenStax V1 -specification: >- - - -
The OpenStax assessment format supports multi-part and single-part questions in multiple formats. The - data is stored in a JSON-encoded string
- -The top level may contain a stimulus_html field. This is where a common introduction - for a multi-part question is stored. Nothing prohibits this field from being used for a - single-part question.
- -The top level will contain a questions array. This field will have one entry for a - single-part question and multiple entries for a multi-part question. Each question contains - the following fields:
- -There are other fields in the content that are largely self-explanatory or not necessary for using - the assessment.
- -OpenStax does have a different multi-stem format which should not be used in any - a15k contributions, but if you find one please contact OpenStax for details.
-You may have mistyped the address or the page may have moved.
diff --git a/public/422.html b/public/422.html index 83660ab1..a82c67fb 100644 --- a/public/422.html +++ b/public/422.html @@ -17,7 +17,6 @@ -Maybe you tried to change something you didn't have access to.
diff --git a/public/500.html b/public/500.html index f3648a0d..0f5a2445 100644 --- a/public/500.html +++ b/public/500.html @@ -17,7 +17,6 @@ -