diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 30a7a45f2..8bf61dea8 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,12 +13,12 @@ name: "CodeQL" on: push: - branches: [ "development" ] + branches: ["development"] pull_request: # The branches below must be a subset of the branches above - branches: [ "development" ] + branches: ["development"] schedule: - - cron: '45 20 * * 3' + - cron: "45 20 * * 3" jobs: analyze: @@ -32,42 +32,40 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'ruby' ] + language: ["ruby"] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - - name: Checkout repository - uses: actions/checkout@v3 + - name: Checkout repository + uses: actions/checkout@v4 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index fdb8f48d6..2ca1d378c 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -2,7 +2,7 @@ name: create-doubtfire-deployment on: push: tags: - - 'v*' + - "v*" # branches: # - '*.x' # - 'development' @@ -16,25 +16,25 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 if: github.event_name != 'pull_request' with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Setup meta for development image id: docker_meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: lmsdoubtfire/doubtfire-api tags: | type=semver,pattern={{major}}.{{minor}}.x-dev - name: Build and push api server id: docker_build - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: context: . push: ${{ github.event_name != 'pull_request' }} @@ -48,18 +48,18 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v5 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 if: github.event_name != 'pull_request' with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Setup meta for api server id: docker_meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: lmsdoubtfire/apiServer tags: | @@ -70,7 +70,7 @@ jobs: type=semver,pattern=prod-{{major}} - name: Build and push api server id: docker_build - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: file: deployApi.Dockerfile context: . @@ -85,18 +85,18 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 if: github.event_name != 'pull_request' with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Setup meta for app server id: docker_meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: lmsdoubtfire/appServer tags: | @@ -107,7 +107,7 @@ jobs: type=semver,pattern=prod-{{major}} - name: Build and push app server id: docker_build - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: file: deployAppSvr.Dockerfile context: . diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 6e67ea80c..482af4d78 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -1,5 +1,13 @@ name: Unit Tests -on: [push, pull_request] +on: + push: + paths-ignore: + - "*.md" + - "docs/**" + pull_request: + paths-ignore: + - "*.md" + - "docs/**" env: RAILS_ENV: "test" @@ -18,6 +26,7 @@ env: DF_ENCRYPTION_PRIMARY_KEY: "AMLOMYA5GV8B4fTK3VKMhVGn8WdvUW8g" DF_ENCRYPTION_DETERMINISTIC_KEY: "anlmuJ6cB3bN3biXRbYvmPsC5ALPFqGG" DF_ENCRYPTION_KEY_DERIVATION_SALT: "hzPR8D4qpOnAg7VeAhkhWw6JmmzKJB10" + DF_REDIS_SIDEKIQ_URL: "redis://redis:6379/0" jobs: unit-tests: @@ -31,16 +40,24 @@ jobs: MARIADB_DATABASE: ${{ env.DF_TEST_DB_DATABASE }} MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: yes # This is required or the healthcheck script can't connect to the db options: --health-cmd "/usr/local/bin/healthcheck.sh --connect --innodb_initialized" --health-interval 10s --health-timeout 5s --health-retries 5 + redis: + image: redis:7.0 + options: --health-cmd "redis-cli ping | grep PONG" --health-interval 1s --health-timeout 5s --health-retries 5 + steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + - name: Set up docker buildx + uses: docker/setup-buildx-action@v3 - name: Build base doubtfire-api development image - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: context: . push: false load: true tags: doubtfire-api-development:local + cache-from: type=gha + cache-to: type=gha,mode=max - name: Populate database uses: addnab/docker-run-action@v3 with: @@ -63,8 +80,9 @@ jobs: -e DF_ENCRYPTION_PRIMARY_KEY -e DF_ENCRYPTION_DETERMINISTIC_KEY -e DF_ENCRYPTION_KEY_DERIVATION_SALT + -e DF_REDIS_SIDEKIQ_URL run: bundle exec rake db:populate - - name: Run rake tests + - name: Run unit tests uses: addnab/docker-run-action@v3 with: image: doubtfire-api-development:local @@ -86,4 +104,5 @@ jobs: -e DF_ENCRYPTION_PRIMARY_KEY -e DF_ENCRYPTION_DETERMINISTIC_KEY -e DF_ENCRYPTION_KEY_DERIVATION_SALT + -e DF_REDIS_SIDEKIQ_URL run: TERM=xterm bundle exec rails test diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml index f241bf104..ead1ef0c4 100644 --- a/.github/workflows/rubocop.yml +++ b/.github/workflows/rubocop.yml @@ -1,6 +1,14 @@ name: RuboCop -on: [push, pull_request] +on: + push: + paths-ignore: + - "*.md" + - "docs/**" + pull_request: + paths-ignore: + - "*.md" + - "docs/**" permissions: contents: read @@ -12,13 +20,13 @@ jobs: BUNDLE_WITHOUT: default doc job cable storage ujs test db steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - name: Set up Ruby 3.1 - uses: ruby/setup-ruby@v1 - with: - ruby-version: 3.1 - bundler-cache: true + - name: Set up Ruby 3.1 + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.1 + bundler-cache: true - - name: Run RuboCop - run: bundle exec rubocop --parallel + - name: Run RuboCop + run: bundle exec rubocop --parallel diff --git a/Gemfile b/Gemfile index 4b6b5cc68..eafc52ad6 100644 --- a/Gemfile +++ b/Gemfile @@ -24,6 +24,7 @@ group :development, :test do gem 'rubocop' gem 'rubocop-faker' gem 'rubocop-rails' + gem 'ruby-lsp' gem 'simplecov', require: false gem 'solargraph' gem 'sprockets-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 78a28a737..6dcc284b5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -270,6 +270,7 @@ GEM ast (~> 2.4.1) racc pkg-config (1.5.6) + prism (0.24.0) psych (5.1.2) stringio public_suffix (5.0.4) @@ -388,6 +389,10 @@ GEM rubocop (>= 1.33.0, < 2.0) rubocop-ast (>= 1.30.0, < 2.0) ruby-filemagic (0.7.3) + ruby-lsp (0.14.6) + language_server-protocol (~> 3.17.0) + prism (>= 0.22.0, < 0.25) + sorbet-runtime (>= 0.5.10782) ruby-ole (1.2.12.2) ruby-progressbar (1.13.0) ruby-saml (1.13.0) @@ -428,6 +433,7 @@ GEM thor (~> 1.0) tilt (~> 2.0) yard (~> 0.9, >= 0.9.24) + sorbet-runtime (0.5.11307) sorted_set (1.0.3) rbtree set (~> 1.0) @@ -512,6 +518,7 @@ DEPENDENCIES rubocop-faker rubocop-rails ruby-filemagic + ruby-lsp ruby-saml (~> 1.13.0) rubyzip sidekiq diff --git a/README.md b/README.md index aee1f068f..faedf0645 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![Doubtfire Logo](https://github.com/doubtfire-lms/doubtfire-web/raw/6.2.x/src/assets/icons/android-chrome-192x192.png) -# Doubtfire API [![test-doubtfire-api](https://github.com/doubtfire-lms/doubtfire-api/actions/workflows/push.yml/badge.svg)](https://github.com/doubtfire-lms/doubtfire-api/actions/workflows/push.yml) [![CodeQL](https://github.com/doubtfire-lms/doubtfire-api/actions/workflows/codeql.yml/badge.svg)](https://github.com/doubtfire-lms/doubtfire-api/actions/workflows/codeql.yml) [![RuboCop](https://github.com/doubtfire-lms/doubtfire-api/actions/workflows/rubocop.yml/badge.svg)](https://github.com/doubtfire-lms/doubtfire-api/actions/workflows/rubocop.yml) +# Doubtfire API [![test-doubtfire-api](https://github.com/doubtfire-lms/doubtfire-api/actions/workflows/push.yml/badge.svg)](https://github.com/doubtfire-lms/doubtfire-api/actions/workflows/push.yml) [![CodeQL](https://github.com/doubtfire-lms/doubtfire-api/actions/workflows/codeql.yml/badge.svg)](https://github.com/doubtfire-lms/doubtfire-api/actions/workflows/codeql.yml) [![RuboCop](https://github.com/doubtfire-lms/doubtfire-api/actions/workflows/rubocop.yml/badge.svg)](https://github.com/doubtfire-lms/doubtfire-api/actions/workflows/rubocop.yml) Doubtfire is a feedback-driven learning support system. @@ -27,34 +27,35 @@ See [Doubtfire Deploy](https://github.com/doubtfire-lms/doubtfire-deploy) for in Doubtfire requires multiple environment variables that help define settings about the Doubtfire instance running. Whilst these will default to other values, you may want to override them in production. - -| Key | Description | Default | -|-------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------| -| `DF_AUTH_METHOD` | The authentication method you would like Doubtfire to use. Possible values are `database` for standard authentication with the database, `ldap` for [LDAP](https://www.freebsd.org/doc/en/articles/ldap-auth/), `aaf` for [AAF Rapid Connect](https://rapid.aaf.edu.au/), or `SAML2` for [SAML2.0 auth](https://en.wikipedia.org/wiki/SAML_2.0). | `database` | -| `DF_STUDENT_WORK_DIR` | The directory to store uploaded student work for processing. | `student_work` | -| `DF_INSTITUTION_NAME` | The name of your institution running Doubtfire. | _University of Foo_ | -| `DF_INSTITUTION_EMAIL_DOMAIN` | The email domain from which emails are sent to and from in your institution. | `doubtfire.com` | -| `DF_INSTITUTION_HOST` | The host running the Doubtfire instance. | `localhost:3000` | -| `DF_INSTITUTION_PRODUCT_NAME` | The name of the product (i.e. Doubtfire) at your institution. | _Doubtfire_ | -| `DF_SECRET_KEY_BASE` | The Rails secret key. | Default key provided. | -| `DF_SECRET_KEY_ATTR` | The secret key to encrypt certain database fields. | Default key provided. | -| `DF_SECRET_KEY_DEVISE` | The secret key provided to Devise. | Default key provided. | -| `DF_SECRET_KEY_MOSS` | The secret key provided to [Moss](http://theory.stanford.edu/~aiken/moss/) for plagiarism detection. This value will need to be set to run `rake submission:check_plagiarism` (otherwise you **won't** need it). You will need to register for a Moss account to use this. | No default. | -| `DF_INSTITUTION_PRIVACY` | A statement related to the need for students to submit their own work, and that this work may be uploaded to 3rd parties for the purpose of plagiarism detection. | Default statement provided | -| `DF_INSTITUTION_PLAGIARISM` | A statement clarifying the terms plagiarism and collusion. | Default statement provided | -| `DF_INSTITUTION_SETTINGS_RB` | The path of the institution specific settings rb code - used to map student imports from institutional exports to a format understood by Doubtfire. | No default | -| `DF_FFMPEG_PATH` | The path of to the ffmpeg binary for audio processing. | ffmpeg | +| Key | Description | Default | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------- | +| `DF_AUTH_METHOD` | The authentication method you would like Doubtfire to use. Possible values are `database` for standard authentication with the database, `ldap` for [LDAP](https://www.freebsd.org/doc/en/articles/ldap-auth/), `aaf` for [AAF Rapid Connect](https://rapid.aaf.edu.au/), or `SAML2` for [SAML2.0 auth](https://en.wikipedia.org/wiki/SAML_2.0). | `database` | +| `DF_STUDENT_WORK_DIR` | The directory to store uploaded student work for processing. | `student_work` | +| `DF_INSTITUTION_NAME` | The name of your institution running Doubtfire. | _University of Foo_ | +| `DF_INSTITUTION_EMAIL_DOMAIN` | The email domain from which emails are sent to and from in your institution. | `doubtfire.com` | +| `DF_INSTITUTION_HOST` | The host running the Doubtfire instance. | `localhost:3000` | +| `DF_INSTITUTION_PRODUCT_NAME` | The name of the product (i.e. Doubtfire) at your institution. | _Doubtfire_ | +| `DF_SECRET_KEY_BASE` | The Rails secret key. | Default key provided. | +| `DF_SECRET_KEY_ATTR` | The secret key to encrypt certain database fields. | Default key provided. | +| `DF_SECRET_KEY_DEVISE` | The secret key provided to Devise. | Default key provided. | +| `DF_SECRET_KEY_MOSS` | The secret key provided to [Moss](http://theory.stanford.edu/~aiken/moss/) for plagiarism detection. This value will need to be set to run `rake submission:check_plagiarism` (otherwise you **won't** need it). You will need to register for a Moss account to use this. | No default. | +| `DF_INSTITUTION_PRIVACY` | A statement related to the need for students to submit their own work, and that this work may be uploaded to 3rd parties for the purpose of plagiarism detection. | Default statement provided | +| `DF_INSTITUTION_PLAGIARISM` | A statement clarifying the terms plagiarism and collusion. | Default statement provided | +| `DF_INSTITUTION_SETTINGS_RB` | The path of the institution specific settings rb code - used to map student imports from institutional exports to a format understood by Doubtfire. | No default | +| `DF_FFMPEG_PATH` | The path of to the ffmpeg binary for audio processing. | ffmpeg | +| `DF_REDIS_CACHE_URL` | The redis URL for rails used for development and production, ignored in the test env. | `redis://localhost:6379/0` | +| `DF_REDIS_SIDEKIQ_URL` | The redis URL for sidekiq. A working redis server is **mandatory** for sidekiq in all environments. | `redis://localhost:6379/1` | If you have chosen to use AAF Rapid Connect authentication, then you will also need to provide the following: | Key | Description | Default | -|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------| +| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | | `DF_AAF_ISSUER_URL` | The URL of the AFF issuer, either `https://rapid.test.aaf.edu.au` for testing or `https://rapid.aaf.edu.au` for production. | `https://rapid.test.aaf.edu.au` | | `DF_AAF_AUDIENCE_URL` | The URL of the AAF registered application. | No default - required | | `DF_AAF_CALLBACK_URL` | The secure endpoint within your application that AAF Rapid Connect should POST responses to. It **must end with `/api/auth/jwt`** to access the Doubtfire JWT authentication endpoint. | No default - required | | `DF_AAF_UNIQUE_URL` | The unique URL provided by AAF Rapid Connect used for redirection out of Doubtfire. | No default - required | | `DF_AAF_IDENTITY_PROVIDER_URL` | The URL of the AAF-registered identity provider. | No default - required | -| `DF_AAF_AUTH_SIGNOUT_URL` | The URL to redirect to on sign out in order to log out of AAF Rapid Connect. | No default - required | +| `DF_AAF_AUTH_SIGNOUT_URL` | The URL to redirect to on sign out in order to log out of AAF Rapid Connect. | No default - required | | `DF_SECRET_KEY_AAF` | The secret used to register your application with AAF. | `secretsecret12345` | You may choose to keep your environment variables inside a `.env` file using key-value pairs: @@ -66,7 +67,6 @@ DF_INSTITUTION_NAME="University of Foo" You can also keep multiple `.env` files for different environments, e.g.: `.env.production` is different to `.env.develoment`. Doubtfire uses the [dotenv](https://github.com/bkeepers/dotenv) gem to make this happen. - ### Get it up and running! Once you've installed using either in install script or the manual install steps. diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index d01d7a270..a4c9ab0ce 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -1,9 +1,9 @@ Sidekiq.configure_server do |config| - config.redis = { url: 'redis://localhost:6379/1' } + config.redis = { url: ENV.fetch('DF_REDIS_SIDEKIQ_URL', 'redis://localhost:6379/1') } config.logger = Rails.logger end Sidekiq.configure_client do |config| - config.redis = { url: 'redis://localhost:6379/1' } + config.redis = { url: ENV.fetch('DF_REDIS_SIDEKIQ_URL', 'redis://localhost:6379/1') } config.logger = Rails.logger end diff --git a/dependabot.yml b/dependabot.yml new file mode 100644 index 000000000..0f96f8de9 --- /dev/null +++ b/dependabot.yml @@ -0,0 +1,9 @@ +# Set update schedule for GitHub Actions + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" diff --git a/lib/helpers/database_populator.rb b/lib/helpers/database_populator.rb index 06d5c1123..eaef8322c 100644 --- a/lib/helpers/database_populator.rb +++ b/lib/helpers/database_populator.rb @@ -563,14 +563,25 @@ def echo_line *args # Generates tasks for the given unit # def generate_tasks_for_unit(unit, unit_details) - if File.exist? Rails.root.join('test_files', "#{unit.code}-Tasks.csv") - echo "----> Importing tasks from CSV" - unit.import_tasks_from_csv File.open(Rails.root.join('test_files', "#{unit.code}-Tasks.csv")) - unit.import_task_files_from_zip Rails.root.join('test_files', "#{unit.code}-Tasks.zip") + csv_to_import = Rails.root.join('test_files', "#{unit.code}-Tasks.csv") + zip_to_import = Rails.root.join('test_files', "#{unit.code}-Tasks.zip") + + if (File.exist? csv_to_import) && (File.exist? zip_to_import) + echo "----> CSV file found, importing tasks from #{csv_to_import} \n" + result = unit.import_tasks_from_csv File.open(csv_to_import) + unless result[:errors].empty? + raise("----> Task import from CSV failed with the following errors: #{result[:errors]} \n") + end + + echo "----> Importing task files from #{zip_to_import} \n" + result = unit.import_task_files_from_zip zip_to_import + unless result[:errors].empty? + raise("----> Task files import failed with the following errors: #{result[:errors]} \n") + end return end - echo "----> Generating #{unit_details[:num_tasks]} tasks" + echo "----> Task csv and/or zip file not found, generating #{unit_details[:num_tasks]} tasks \n" unit_details[:num_tasks].times do |count| up_reqs = [] @@ -602,7 +613,7 @@ def generate_tasks_for_unit(unit, unit_details) # def generate_and_align_ilos_for_unit(unit, unit_details) # Create the ILOs - echo "----> Adding #{unit_details[:ilos]} ILOs" + echo "----> Adding #{unit_details[:ilos]} ILOs\n" if File.exist? Rails.root.join('test_files', "#{unit.code}-Outcomes.csv") unit.import_outcomes_from_csv File.open(Rails.root.join('test_files', "#{unit.code}-Outcomes.csv"))