diff --git a/.gitignore b/.gitignore index abffb6d..d5615f7 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,6 @@ lib/assets/npm-debug.log lib/assets/.eslintcache !.keep +.byebug_history +lib/data_cleanup/rules/org/fix_blank_abbreviation.yml +.rspec diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..43d23b3 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,3 @@ +inherit_gem: + rubocop-dmp_roadmap: + - config/default.yml diff --git a/.travis.yml b/.travis.yml index 94cf431..e3da155 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,20 +1,62 @@ -language: ruby -rvm: - - 2.4.4 -before_install: - - curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - && sudo apt-get install -y nodejs -before_script: - - cd lib/assets && npm install && npm run bundle -- -p && cd - - - cp config/database_example.yml config/database.yml - - cp config/secrets_example.yml config/secrets.yml - - cp config/branding_example.yml config/branding.yml - - cp config/initializers/devise.rb.example config/initializers/devise.rb - - cp config/initializers/recaptcha.rb.example config/initializers/recaptcha.rb - - cp config/initializers/wicked_pdf.rb.example config/initializers/wicked_pdf.rb - - bundle exec rake db:drop RAILS_ENV=test - - bundle exec rake db:create RAILS_ENV=test - - bundle exec rake db:schema:load RAILS_ENV=test - - bundle exec rake db:migrate RAILS_ENV=test +sudo: false +# Ruby is the main language of the project. +language: ruby + +bundler_args: --with development,ci + +# Cache third party dependencies for faster builds +cache: + apt: true + bundler: true + directories: + # Cache NPM packages + - lib/assets/node_modules + - $HOME/.npm + +addons: + chrome: stable + apt: + packages: + - nodejs + +matrix: + fast_finish: true + include: + +rvm: + # Use 2.4.1, since this is installed by default on Travis (1st Aug, 2018) + - 2.4.1 + +# These env variables will set up a separate testing environment for each +# combination of variables. +env: + # Run specs once with each database adapter we support + - DB_ADAPTER=postgresql + - DB_ADAPTER=mysql2 + +# Main test script script: - - bundle exec rake test + # Copy over config files needed for setup, and create DB + - bin/setup + # Precompile the assets + - bundle exec rake assets:precompile + # Default test stage: Run all specs, listing the 10 slowest. + - bundle exec rspec spec --profile=10 + +# Run these stages in this order: +stages: + - security + - test + +# Define each stage (test is already defined automatically) +jobs: + include: + # Run Brakeman check with warning level 2, except these two checks: + - stage: security + name: "Brakeman check" + script: bundle exec brakeman -w2 --except=Redirect,CrossSiteScripting + + - stage: security + name: "Bundle audit" + script: bundle exec bundle-audit check --update diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 27731cc..2c30573 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,10 +17,10 @@ On your local machine, add a remote that points to the original DMPRoadmap codebase. This will allow you to pull down the latest changes and sync up your forked repository -Run the following from your local clone of the repository to setup a remote that will allow you to pull down the latest changes from DMPRoadmap. Then pull down the contributions branch: +Run the following from your local clone of the repository to setup a remote that will allow you to pull down the latest changes from DMPRoadmap. Then pull down the development branch: ```bash git remote add upstream https://github.com/DMPRoadmap/roadmap.git -git fetch contributions +git fetch development ``` ### Pulling down the latest changes from DMPRoadmap into your fork @@ -31,12 +31,12 @@ ### Create a new feature/bug fix/translations branch -You should always base your new branch off of the contributions branch. We keep this branch up to date with the latest release. Checkout the contributions branch, sync it with DMPRoadmap and then push the latest up to your own fork: +You should always base your new branch off of the development branch. We keep this branch up to date with the latest release. Checkout the development branch, sync it with DMPRoadmap and then push the latest up to your own fork: ```bash -git checkout contributions -git pull upstream contributions -git push origin contributions +git checkout development +git pull upstream development +git push origin development git checkout -b [my-branch] ``` @@ -50,7 +50,7 @@ When you are finished making changes, we ask that all contributors squash their commits into a single git commit. This helps us keep the git history clean and makes it easier to revert any changes if necessary. -_Note that if this is your first time rebasing a branch we recommend making a buckup of the branch first since a rebase creates the potential for you to lose your changes if its done incorrectly: `git checkout -b [feature branch]-bak && git checkout [feature branch]`_ +_Note that if this is your first time rebasing a branch we recommend making a backup of the branch first since a rebase creates the potential for you to lose your changes if its done incorrectly: `git checkout -b [feature branch]-bak && git checkout [feature branch]`_ To rebase your feature branch you should follow this example: @@ -130,7 +130,7 @@ Once your changes are complete, push your branch up to your fork, `git push origin [my-branch]` -Then login to Github and go to your fork. Select your branch from the list and click 'New Pull Request'. On the page that opens, select the 'contributions' branch on the DMPRoadmap section. +Then login to Github and go to your fork. Select your branch from the list and click 'New Pull Request'. On the page that opens, select the 'development' branch on the DMPRoadmap section. Then review your code and provide us with detailed comments about what the changes are doing (e.g. adding a new feature, fixing a recorded bug, etc.). If you are working off of one of our Github issues, then please note that in the PR message with a `Fixes #1234`. @@ -142,11 +142,11 @@ ### Acceptence of your PR -Once your code has been approved a member of the core development team will merge it into the contributions branch and then into development when we are ready to include it in a sprint. +Once your code has been approved a member of the core development team will merge it into the development branchand include it in an upcoming release. At this point its a good idea to delete the branch from your fork in Github and also delete it from your local machine via: ```bash -git checkout contributions +git checkout development git branch -D [my-branch] ``` diff --git a/Gemfile b/Gemfile index 314e27e..109bd9c 100644 --- a/Gemfile +++ b/Gemfile @@ -1,87 +1,220 @@ source 'https://rubygems.org' -ruby '>= 2.4.4' +ruby '>= 2.4.0' # ------------------------------------------------ -# RAILS +# RAILS +# Full-stack web application framework. (http://www.rubyonrails.org) gem 'rails', '~> 4.2.10' + +# Tools for creating, working with, and running Rails applications. (http://www.rubyonrails.org) gem 'railties' +# GEMS ADDED TO HELP HANDLE RAILS MIGRATION FROM 3.x to 4.2 +# THESE GEMS HELP SUPPORT DEPRACATED FUNCTIONALITY AND WILL LOSE SUPPORT IN +# FUTURE VERSIONS WE SHOULD CONSIDER BRINGING THE CODE UP TO DATE INSTEAD -# GEMS ADDED TO HELP HANDLE RAILS MIGRATION FROM 3.x to 4.2 -# THESE GEMS HELP SUPPORT DEPRACATED FUNCTIONALITY AND WILL LOSE SUPPORT IN FUTURE VERSIONS -# WE SHOULD CONSIDER BRINGING THE CODE UP TO DATE INSTEAD -gem 'protected_attributes', '~> 1.1.3' # Provides attr_accessor functions -gem 'responders', '~> 2.0' # Allows use of respond_with and respond_to in controllers +# A set of Rails responders to dry up your application (http://github.com/plataformatec/responders) +gem 'responders', '~> 2.0' # ------------------------------------------------ # DATABASE/SERVER + +# A simple, fast Mysql library for Ruby, binding to libmysql +# (http://github.com/brianmario/mysql2) +# A simple, fast Mysql library for Ruby, binding to libmysql (http://github.com/brianmario/mysql2) gem 'mysql2', '~> 0.4.10' + +# Pg is the Ruby interface to the {PostgreSQL +# RDBMS}[http://www.postgresql.org/](https://bitbucket.org/ged/ruby-pg) +# Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/] (https://bitbucket.org/ged/ruby-pg) gem 'pg', '~> 0.19.0' + +# Bit fields for ActiveRecord (https://github.com/pboling/flag_shih_tzu) gem 'flag_shih_tzu' # Allows for bitfields in activereccord # ------------------------------------------------ # JSON DSL - USED BY API +# Create JSON structures via a Builder-style DSL (https://github.com/rails/jbuilder) gem 'jbuilder', '~> 2.6.0' -# ------------------------------------------------ +# ------------------------------------------------ # USERS # devise for user authentication +# Flexible authentication solution for Rails with Warden (https://github.com/plataformatec/devise) gem 'devise' + +# An invitation strategy for Devise (https://github.com/scambra/devise_invitable) gem 'devise_invitable' + +# A generalized Rack framework for multiple-provider authentication. (https://github.com/omniauth/omniauth) gem 'omniauth' + +# OmniAuth Shibboleth strategies for OmniAuth 1.x gem 'omniauth-shibboleth' + +# ORCID OAuth 2.0 Strategy for OmniAuth 1.0 (https://github.com/datacite/omniauth-orcid) gem 'omniauth-orcid' + +# Pure Ruby implementation of Array#dig and Hash#dig for Ruby < 2.3. (https://github.com/Invoca/ruby_dig) gem 'ruby_dig' # for omniauth-orcid # Gems for repository integration +# OO authorization for Rails (https://github.com/elabs/pundit) gem 'pundit' # ------------------------------------------------ -# SETTINGS FOR TEMPLATES AND PLANS (FONTS, COLUMN LAYOUTS, ETC) +# SETTINGS FOR TEMPLATES AND PLANS (FONTS, COLUMN LAYOUTS, ETC) + +# Ruby gem to handle settings for ActiveRecord instances by storing them as serialized Hash in a separate database table. Namespaces and defaults included. (https://github.com/ledermann/rails-settings) gem 'ledermann-rails-settings' # ------------------------------------------------ -# VIEWS +# VIEWS + +# Gem providing simple Contact Us functionality with a Rails 3+ Engine. (https://github.com/jdutil/contact_us) gem 'contact_us' # COULD BE EASILY REPLACED WITH OUR OWN CODE + +# Helpers for the reCAPTCHA API (http://github.com/ambethia/recaptcha) gem 'recaptcha' -gem 'dragonfly' # LOGO UPLOAD + +# Ideal gem for handling attachments in Rails, Sinatra and Rack applications. (http://github.com/markevans/dragonfly) +gem 'dragonfly' # ------------------------------------------------ -# EXPORTING +# EXPORTING +# Provides binaries for WKHTMLTOPDF project in an easily accessible package. gem 'wkhtmltopdf-binary' + +# A thin and fast web server (http://code.macournoyer.com/thin/) gem 'thin' + +# PDF generator (from HTML) gem for Ruby on Rails (https://github.com/mileszs/wicked_pdf) gem 'wicked_pdf' + +# This simple gem allows you to create MS Word docx documents from simple html documents. This makes it easy to create dynamic reports and forms that can be downloaded by your users as simple MS Word docx files. (http://github.com/karnov/htmltoword) gem 'htmltoword' + +# A feed fetching and parsing library (http://feedjira.com) gem 'feedjira' # ------------------------------------------------ -# INTERNATIONALIZATION +# INTERNATIONALIZATION +# Simple FastGettext Rails integration. (http://github.com/grosser/gettext_i18n_rails) gem 'gettext_i18n_rails' + +# Extends gettext_i18n_rails making your .po files available to client side javascript as JSON (https://github.com/webhippie/gettext_i18n_rails_js) gem 'gettext_i18n_rails_js' -gem 'gettext', :require => false, :group => :development + +# Gettext is a pure Ruby libary and tools to localize messages. (http://ruby-gettext.github.com/) +gem 'gettext', require: false, group: :development # ------------------------------------------------ -# PAGINATION +# PAGINATION +# A pagination engine plugin for Rails 4+ and other modern frameworks (https://github.com/kaminari/kaminari) gem 'kaminari' -# ------------------------------------------------ -# ENVIRONMENT SPECIFIC DEPENDENCIES +# ------------------------------------------------ +# ENVIRONMENT SPECIFIC DEPENDENCIES group :development, :test do + # Ruby fast debugger - base + CLI (http://github.com/deivid-rodriguez/byebug) gem "byebug" + + # RSpec for Rails (https://github.com/rspec/rspec-rails) + gem "rspec-rails" + + # factory_bot_rails provides integration between factory_bot and rails 3 or newer (http://github.com/thoughtbot/factory_bot_rails) + # rspec-collection_matchers-1.1.3 (https://github.com/rspec/rspec-collection_matchers) + gem "rspec-collection_matchers" + + # factory_bot_rails provides integration between factory_bot and rails 3 or newer (http://github.com/thoughtbot/factory_bot_rails) + gem "factory_bot_rails" + + # Easily generate fake data (https://github.com/stympy/faker) + gem "faker" + + # the instafailing RSpec progress bar formatter (https://github.com/thekompanee/fuubar) + gem "fuubar" + + # Guard keeps an eye on your file modifications (http://guardgem.org) + gem "guard" + + # Guard gem for RSpec (https://github.com/guard/guard-rspec) + gem "guard-rspec" end group :test do - gem 'minitest-reporters' - gem 'rack-test' + # Library for stubbing HTTP requests in Ruby. (http://github.com/bblimke/webmock) gem 'webmock' - gem 'sqlite3' + + # Code coverage for Ruby 1.9+ with a powerful configuration library and automatic merging of coverage across test suites (http://github.com/colszowka/simplecov) gem 'simplecov', require: false + + # Strategies for cleaning databases. Can be used to ensure a clean state for testing. (http://github.com/DatabaseCleaner/database_cleaner) + gem 'database_cleaner', require: false + + # Making tests easy on the fingers and eyes (https://github.com/thoughtbot/shoulda) + gem "shoulda", require: false + + # Mocking and stubbing library (http://gofreerange.com/mocha/docs) + gem "mocha", require: false + + # Rails application preloader (https://github.com/rails/spring) + gem "spring" + + # rspec command for spring (https://github.com/jonleighton/spring-commands-rspec) + gem "spring-commands-rspec" + + # rspec-collection_matchers-1.1.3 (https://github.com/rspec/rspec-collection_matchers) + gem "rspec-collection_matchers" + + # Capybara aims to simplify the process of integration testing Rack applications, such as Rails, Sinatra or Merb (https://github.com/teamcapybara/capybara) + gem "capybara" + + # Automatically create snapshots when Cucumber steps fail with Capybara and Rails (http://github.com/mattheworiordan/capybara-screenshot) + gem "capybara-screenshot" + + # The next generation developer focused tool for automated testing of webapps (https://github.com/SeleniumHQ/selenium) + gem 'selenium-webdriver', '>= 3.13.1' + + # Easy installation and use of chromedriver. (https://github.com/flavorjones/chromedriver-helper) + gem 'chromedriver-helper', ">= 1.2.0" +end + +group :ci, :development do + # Security vulnerability scanner for Ruby on Rails. (http://brakemanscanner.org) + gem "brakeman" + + # Automatic Ruby code style checking tool. (https://github.com/rubocop-hq/rubocop) + gem "rubocop-dmp_roadmap" + + # Helper gem to require bundler-audit (http://github.com/stewartmckee/bundle-audit) + gem "bundle-audit" end group :development do + # Better error page for Rails and other Rack apps (https://github.com/charliesome/better_errors) gem "better_errors" + + # Retrieve the binding of a method's caller. Can also retrieve bindings even further up the stack. (http://github.com/banister/binding_of_caller) gem "binding_of_caller" + + # A debugging tool for your Ruby on Rails applications. (https://github.com/rails/web-console) gem 'web-console' + + # Profiles loading speed for rack applications. (http://miniprofiler.com) gem 'rack-mini-profiler' + + # Annotates Rails Models, routes, fixtures, and others based on the database schema. (http://github.com/ctran/annotate_models) + gem "annotate" + + # Add comments to your Gemfile with each dependency's description. (https://github.com/ivantsepp/annotate_gem) + gem "annotate_gem" + + # help to kill N+1 queries and unused eager loading. (https://github.com/flyerhzm/bullet) + gem "bullet" + + gem "yard" + + gem "yard-tomdoc" end diff --git a/Gemfile.lock b/Gemfile.lock index 29f1a92..827f65f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -35,9 +35,17 @@ minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) - addressable (2.4.0) - ansi (1.5.0) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) + annotate (2.7.4) + activerecord (>= 3.2, < 6.0) + rake (>= 10.4, < 13.0) + annotate_gem (0.0.12) + bundler (~> 1.1) + archive-zip (0.11.0) + io-like (~> 0.3.0) arel (6.0.4) + ast (2.4.0) bcrypt (3.1.11) better_errors (2.4.0) coderay (>= 1.0.0) @@ -45,8 +53,32 @@ rack (>= 0.9.0) binding_of_caller (0.8.0) debug_inspector (>= 0.0.1) + brakeman (4.3.1) builder (3.2.3) + bullet (5.7.5) + activesupport (>= 3.0.0) + uniform_notifier (~> 1.11.0) + bundle-audit (0.1.0) + bundler-audit + bundler-audit (0.6.0) + bundler (~> 1.2) + thor (~> 0.18) byebug (10.0.2) + capybara (3.4.2) + addressable + mini_mime (>= 0.1.3) + nokogiri (~> 1.8) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + xpath (~> 3.1) + capybara-screenshot (1.0.21) + capybara (>= 1.0, < 4) + launchy + childprocess (0.9.0) + ffi (~> 1.0, >= 1.0.11) + chromedriver-helper (1.2.0) + archive-zip (~> 0.10) + nokogiri (~> 1.8) coderay (1.1.2) concurrent-ruby (1.0.5) contact_us (1.2.0) @@ -55,6 +87,7 @@ safe_yaml (~> 1.0.0) crass (1.0.4) daemons (1.2.6) + database_cleaner (1.7.0) debug_inspector (0.0.3) devise (4.4.3) bcrypt (~> 3.0) @@ -65,6 +98,7 @@ devise_invitable (1.7.4) actionmailer (>= 4.1.0) devise (>= 4.0.0) + diff-lcs (1.3) docile (1.3.0) dragonfly (1.1.5) addressable (~> 2.3) @@ -73,6 +107,13 @@ erubi (1.7.1) erubis (2.7.0) eventmachine (1.2.6) + factory_bot (4.10.0) + activesupport (>= 3.0.0) + factory_bot_rails (4.10.0) + factory_bot (~> 4.10.0) + railties (>= 3.0.0) + faker (1.9.1) + i18n (>= 0.7) faraday (0.9.2) multipart-post (>= 1.2, < 3) faraday_middleware (0.12.2) @@ -83,7 +124,12 @@ faraday_middleware (>= 0.9) loofah (>= 2.0) sax-machine (>= 1.0) + ffi (1.9.25) flag_shih_tzu (0.3.19) + formatador (0.2.5) + fuubar (2.3.1) + rspec-core (~> 3.0) + ruby-progressbar (~> 1.4) gettext (3.2.9) locale (>= 2.0.5) text (>= 1.3.0) @@ -96,6 +142,20 @@ rails (>= 3.2.0) globalid (0.4.1) activesupport (>= 4.2.0) + guard (2.14.2) + formatador (>= 0.2.4) + listen (>= 2.7, < 4.0) + lumberjack (>= 1.0.12, < 2.0) + nenv (~> 0.1) + notiffany (~> 0.0) + pry (>= 0.9.12) + shellany (~> 0.0) + thor (>= 0.18.1) + guard-compat (1.2.1) + guard-rspec (4.7.3) + guard (~> 2.1) + guard-compat (~> 1.1) + rspec (>= 2.99.0, < 4.0) hashdiff (0.3.7) hashie (3.5.7) htmltoword (1.0.0) @@ -104,6 +164,8 @@ rubyzip (>= 1.0) i18n (0.9.5) concurrent-ruby (~> 1.0) + io-like (0.3.0) + jaro_winkler (1.5.1) jbuilder (2.6.0) activesupport (>= 3.0.0, < 5.1) multi_json (~> 1.2) @@ -121,28 +183,38 @@ activerecord kaminari-core (= 1.1.1) kaminari-core (1.1.1) + launchy (2.4.3) + addressable (~> 2.3) ledermann-rails-settings (2.4.2) activerecord (>= 3.1) + listen (3.1.5) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + ruby_dep (~> 1.2) locale (2.1.2) loofah (2.2.2) crass (~> 1.0.2) nokogiri (>= 1.5.9) + lumberjack (1.0.13) mail (2.7.0) mini_mime (>= 0.1.1) + metaclass (0.0.4) + method_source (0.9.0) mini_mime (1.0.0) mini_portile2 (2.3.0) minitest (5.11.3) - minitest-reporters (1.2.0) - ansi - builder - minitest (>= 5.0) - ruby-progressbar + mocha (1.5.0) + metaclass (~> 0.0.1) multi_json (1.13.1) multi_xml (0.6.0) multipart-post (2.0.0) mysql2 (0.4.10) - nokogiri (1.8.2) + nenv (0.3.0) + nokogiri (1.8.4) mini_portile2 (~> 2.3.0) + notiffany (0.1.1) + nenv (~> 0.1) + shellany (~> 0.0) oauth2 (1.4.0) faraday (>= 0.8, < 0.13) jwt (~> 1.0) @@ -161,11 +233,17 @@ omniauth-shibboleth (1.3.0) omniauth (>= 1.0.0) orm_adapter (0.5.0) + parallel (1.12.1) + parser (2.5.1.2) + ast (~> 2.4.0) pg (0.19.0) po_to_json (1.0.1) json (>= 1.6.0) - protected_attributes (1.1.3) - activemodel (>= 4.0.1, < 5.0) + powerpack (0.1.2) + pry (0.11.3) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + public_suffix (3.0.2) pundit (1.1.0) activesupport (>= 3.0.0) rack (1.6.10) @@ -197,29 +275,87 @@ activesupport (= 4.2.10) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) + rainbow (3.0.0) rake (12.3.1) + rb-fsevent (0.10.3) + rb-inotify (0.9.10) + ffi (>= 0.5.0, < 2) recaptcha (4.8.0) json responders (2.3.0) railties (>= 4.2.0, < 5.1) + rspec (3.7.0) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-collection_matchers (1.1.3) + rspec-expectations (>= 2.99.0.beta1) + rspec-core (3.7.1) + rspec-support (~> 3.7.0) + rspec-expectations (3.7.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.7.0) + rspec-mocks (3.7.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.7.0) + rspec-rails (3.7.2) + actionpack (>= 3.0) + activesupport (>= 3.0) + railties (>= 3.0) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-support (~> 3.7.0) + rspec-support (3.7.1) + rubocop (0.58.2) + jaro_winkler (~> 1.5.1) + parallel (~> 1.10) + parser (>= 2.5, != 2.5.1.1) + powerpack (~> 0.1) + rainbow (>= 2.2.2, < 4.0) + ruby-progressbar (~> 1.7) + unicode-display_width (~> 1.0, >= 1.0.1) + rubocop-dmp_roadmap (1.0.0) + rubocop (>= 0.58.2) + rubocop-rails_config (>= 0.2.2) + rubocop-rspec (>= 1.27.0) + rubocop-rails_config (0.2.2) + railties (>= 3.0) + rubocop (~> 0.56) + rubocop-rspec (1.27.0) + rubocop (>= 0.56.0) ruby-progressbar (1.9.0) + ruby_dep (1.5.0) ruby_dig (0.0.2) rubyzip (1.2.1) safe_yaml (1.0.4) sax-machine (1.3.2) + selenium-webdriver (3.13.1) + childprocess (~> 0.5) + rubyzip (~> 1.2) + shellany (0.0.1) + shoulda (3.6.0) + shoulda-context (~> 1.0, >= 1.0.1) + shoulda-matchers (~> 3.0) + shoulda-context (1.2.2) + shoulda-matchers (3.1.2) + activesupport (>= 4.0.0) simplecov (0.16.1) docile (~> 1.1) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.2) - sprockets (3.7.1) + spring (2.0.2) + activesupport (>= 4.2) + spring-commands-rspec (1.0.4) + spring (>= 0.9.1) + sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-rails (3.2.1) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - sqlite3 (1.3.13) text (1.3.1) thin (1.7.2) daemons (~> 1.0, >= 1.0.9) @@ -227,8 +363,11 @@ rack (>= 1, < 3) thor (0.20.0) thread_safe (0.3.6) + tomparse (0.4.2) tzinfo (1.2.5) thread_safe (~> 0.1) + unicode-display_width (1.4.0) + uniform_notifier (1.11.0) warden (1.2.7) rack (>= 1.0) web-console (3.3.0) @@ -241,49 +380,76 @@ hashdiff wicked_pdf (1.1.0) wkhtmltopdf-binary (0.12.3.1) + xpath (3.1.0) + nokogiri (~> 1.8) + yard (0.9.14) + yard-tomdoc (0.7.1) + tomparse (>= 0.4.0) + yard PLATFORMS ruby DEPENDENCIES + annotate + annotate_gem better_errors binding_of_caller + brakeman + bullet + bundle-audit byebug + capybara + capybara-screenshot + chromedriver-helper (>= 1.2.0) contact_us + database_cleaner devise devise_invitable dragonfly + factory_bot_rails + faker feedjira flag_shih_tzu + fuubar gettext gettext_i18n_rails gettext_i18n_rails_js + guard + guard-rspec htmltoword jbuilder (~> 2.6.0) kaminari ledermann-rails-settings - minitest-reporters + mocha mysql2 (~> 0.4.10) omniauth omniauth-orcid omniauth-shibboleth pg (~> 0.19.0) - protected_attributes (~> 1.1.3) pundit rack-mini-profiler - rack-test rails (~> 4.2.10) railties recaptcha responders (~> 2.0) + rspec-collection_matchers + rspec-rails + rubocop-dmp_roadmap + rubocop-rspec ruby_dig + selenium-webdriver (>= 3.13.1) + shoulda simplecov - sqlite3 + spring + spring-commands-rspec thin web-console webmock wicked_pdf wkhtmltopdf-binary + yard + yard-tomdoc RUBY VERSION ruby 2.4.4p296 diff --git a/Guardfile b/Guardfile new file mode 100644 index 0000000..2303c69 --- /dev/null +++ b/Guardfile @@ -0,0 +1,56 @@ +# A sample Guardfile +# More info at https://github.com/guard/guard#readme + +# Note: The cmd option is now required due to the increasing number of ways +# rspec may be run, below are examples of the most common uses. +# * bundler: 'bundle exec rspec' +# * bundler binstubs: 'bin/rspec' +# * spring: 'bin/rspec' (This will use spring if running and you have +# installed the spring binstubs per the docs) +# * zeus: 'zeus rspec' (requires the server to be started separately) +# * 'just' rspec: 'rspec' + +guard :rspec, cmd: "bundle exec rspec" do + require "guard/rspec/dsl" + dsl = Guard::RSpec::Dsl.new(self) + + # Feel free to open issues for suggestions and improvements + + # RSpec files + rspec = dsl.rspec + watch(rspec.spec_helper) { rspec.spec_dir } + watch(rspec.spec_support) { rspec.spec_dir } + watch(rspec.spec_files) + + # Ruby files + ruby = dsl.ruby + dsl.watch_spec_files_for(ruby.lib_files) + + # Rails files + rails = dsl.rails(view_extensions: %w(erb haml slim)) + dsl.watch_spec_files_for(rails.app_files) + dsl.watch_spec_files_for(rails.views) + + watch(rails.controllers) do |m| + [ + rspec.spec.call("routing/#{m[1]}_routing"), + rspec.spec.call("controllers/#{m[1]}_controller"), + rspec.spec.call("acceptance/#{m[1]}") + ] + end + + # Rails config changes + watch(rails.spec_helper) { rspec.spec_dir } + watch(rails.routes) { "#{rspec.spec_dir}/routing" } + watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" } + + # Capybara features specs + watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") } + watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") } + + # Turnip features and steps + watch(%r{^spec/acceptance/(.+)\.feature$}) + watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m| + Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance" + end +end diff --git a/app/controllers/answers_controller.rb b/app/controllers/answers_controller.rb index 28b3fe3..fd991ff 100644 --- a/app/controllers/answers_controller.rb +++ b/app/controllers/answers_controller.rb @@ -56,7 +56,7 @@ } }).find(p_params[:plan_id]) @question = @answer.question - @section = @plan.get_section(@question.section_id) + @section = @plan.sections.find_by(id: @question.section_id) template = @section.phase.template render json: { diff --git a/app/controllers/api/v0/base_controller.rb b/app/controllers/api/v0/base_controller.rb index c8edddd..a3c3490 100644 --- a/app/controllers/api/v0/base_controller.rb +++ b/app/controllers/api/v0/base_controller.rb @@ -49,34 +49,39 @@ end private - # returns the resource from the created instance variable - # @return [Object] + + # The resource from the created instance variable + # + # Returns Object def get_resource instance_variable_get("@#{resource_name}") end - # Returns the allowed parameters for searching - # Override this method in each API controller - # to permit additional parameters to search on - # @return [Hash] + # The allowed parameters for searching. Override this method in each API + # controller to permit additional parameters to search on + # + # Returns Hash def query_params {} end - # Returns the allowed parameters for pagination - # @return [Hash] + # The allowed parameters for pagination + # + # Returns Hash def page_params params.permit(:page, :page_size) end # The resource class based on the controller - # @return [Class] + # + # Returns Object def resource_class @resource_class ||= resource_name.classify.constantize end # The singular name for the resource class based on the controller - # @return [String] + # + # Returns String def resource_name @resource_name ||= self.controller_name.singularize end diff --git a/app/controllers/api/v0/statistics_controller.rb b/app/controllers/api/v0/statistics_controller.rb index 8d84bd4..1223121 100644 --- a/app/controllers/api/v0/statistics_controller.rb +++ b/app/controllers/api/v0/statistics_controller.rb @@ -4,11 +4,12 @@ before_action :authenticate # GET /api/v0/statistics/users_joined?start_date=&end_date=&org_id= + # # Returns the number of users joined for the user's org. # If start_date is passed, only counts those with created_at is >= than start_date # If end_date is passed, only counts those with created_at is <= than end_date are - # If org_id is passed and user has super_admin privileges that counter is performed against org_id param instead of user's org - # @return + # If org_id is passed and user has super_admin privileges that counter is performed + # against org_id param instead of user's org def users_joined raise Pundit::NotAuthorizedError unless Api::V0::StatisticsPolicy.new(@user, :statistics).users_joined? @@ -124,9 +125,9 @@ end ## - # GET - # @return the number of DMPs using the specified template between the optional specified dates - # ensures that the template is owned/created by the caller's organisation + # Displays the number of DMPs using the specified template between the optional + # specified dates ensures that the template is owned/created by the caller's + # organisation def using_template org_templates = @user.org.templates.where(customization_of: nil) raise Pundit::NotAuthorizedError unless Api::V0::StatisticsPolicy.new(@user, org_templates.first).using_template? @@ -152,9 +153,9 @@ ## # GET - # @return a list of templates with their titles, ids, and uses between the optional specified dates - # the uses are restricted to DMPs created by users of the same organisation - # as the user who ititiated the call + # Renders a list of templates with their titles, ids, and uses between the optional + # specified dates the uses are restricted to DMPs created by users of the same + # organisation as the user who ititiated the call. def plans_by_template raise Pundit::NotAuthorizedError unless Api::V0::StatisticsPolicy.new(@user, :statistics).plans_by_template? org_projects = [] @@ -181,11 +182,11 @@ respond_with @templates end - ## # GET - # @return a list of DMPs metadata, provided the DMPs were created between the optional specified dates - # DMPs must be owned by a user who's organisation is the same as the user - # who generates the call + # + # Renders a list of DMPs metadata, provided the DMPs were created between the + # optional specified dates DMPs must be owned by a user who's organisation is the + # same as the user who generates the call. def plans raise Pundit::NotAuthorizedError unless Api::V0::StatisticsPolicy.new(@user, :statistics).plans? @org_plans = [] @@ -202,32 +203,34 @@ private - ## - # takes in an array of active_reccords and restricts the range of dates - # to those specified in the params - # - # @param objects [Array] any active_reccord reccords which - # have the "created_at" field specified - # @return [Array] filtered list of objects - def restrict_date_range( objects ) - # set start_date to either passed param, or beginning of time - start_date = params[:start_date].blank? ? Date.new(0) : Date.strptime(params[:start_date], "%Y-%m-%d") - # set end_date to either passed param or now - end_date = params[:end_date].blank? ? Date.today : Date.strptime(params[:end_date], "%Y-%m-%d") - filtered = [] - objects.each do |obj| - # apperantly things can have nil created_at - if obj.created_at.blank? - if params[:start_date].blank? && params[:end_date].blank? - filtered += [obj] - end - elsif start_date <= obj.created_at.to_date && end_date >= obj.created_at.to_date + ## + # Takes in an array of active_reccords and restricts the range of dates + # to those specified in the params + # + # objects - any active_reccord reccords which have the "created_at" field specified + # + # Returns Array + def restrict_date_range( objects ) + # set start_date to either passed param, or beginning of time + start_date = params[:start_date].blank? ? Date.new(0) : Date.strptime(params[:start_date], "%Y-%m-%d") + # set end_date to either passed param or now + end_date = params[:end_date].blank? ? Date.today : Date.strptime(params[:end_date], "%Y-%m-%d") + + filtered = [] + objects.each do |obj| + # apperantly things can have nil created_at + if obj.created_at.blank? + if params[:start_date].blank? && params[:end_date].blank? filtered += [obj] end + elsif start_date <= obj.created_at.to_date && end_date >= obj.created_at.to_date + filtered += [obj] end - return filtered end + return filtered + end + end end end diff --git a/app/controllers/api/v0/templates_controller.rb b/app/controllers/api/v0/templates_controller.rb index 03999f8..b1dea9f 100644 --- a/app/controllers/api/v0/templates_controller.rb +++ b/app/controllers/api/v0/templates_controller.rb @@ -4,9 +4,9 @@ before_action :authenticate - ## # GET - # @return a list of templates ordered by organisation + # + # Renders a list of templates ordered by organisation def index # check if the user has permissions to use the templates API raise Pundit::NotAuthorizedError unless Api::V0::TemplatePolicy.new(@user, :guidance_group).index? diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d8e5aad..adaa154 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -96,64 +96,53 @@ "#{_('Successfully %{action} your %{object}.') % {object: obj_name, action: action}}" end - # Check whether the string is a valid array of JSON objects - def is_json_array_of_objects?(string) - if string.present? - begin - json = JSON.parse(string) - return (json.is_a?(Array) && json.all?{ |o| o.is_a?(Hash) }) - rescue JSON::ParserError - return false + private + + # Override rails default render action to look for a branded version of a + # template instead of using the default one. If no override exists, the + # default version in ./app/views/[:controller]/[:action] will be used + # + # The path in the app/views/branded/ directory must match the the file it is + # replacing. For example: + # app/views/branded/layouts/_header.html.erb -> app/views/layouts/_header.html.erb + def prepend_view_paths + prepend_view_path "app/views/branded" + end + + def errors_to_s(obj) + if obj.errors.count > 0 + msg = "
" + obj.errors.each do |e,m| + if m.include?('empty') || m.include?('blank') + msg += "#{_(e)} - #{_(m)}
" + else + msg += "'#{obj[e]}' - #{_(m)}
" + end end + msg end end - private - # Override rails default render action to look for a branded version of a - # template instead of using the default one. If no override exists, the - # default version in ./app/views/[:controller]/[:action] will be used - # - # The path in the app/views/branded/ directory must match the the file it is - # replacing. For example: - # app/views/branded/layouts/_header.html.erb -> app/views/layouts/_header.html.erb - def prepend_view_paths - prepend_view_path "app/views/branded" + ## + # Sign out of Shibboleth SP local session too. + # ------------------------------------------------------------- + def after_sign_out_path_for(resource_or_scope) + if Rails.application.config.shibboleth_enabled + return Rails.application.config.shibboleth_logout_url + root_url + super + else + super end + end + # ------------------------------------------------------------- - def errors_to_s(obj) - if obj.errors.count > 0 - msg = "
" - obj.errors.each do |e,m| - if m.include?('empty') || m.include?('blank') - msg += "#{_(e)} - #{_(m)}
" - else - msg += "'#{obj[e]}' - #{_(m)}
" - end - end - msg - end + def from_external_domain? + if request.referer.present? + referer = URI.parse(request.referer) + home = URI.parse(root_url) + referer.host != home.host + else + false end - - ## - # Sign out of Shibboleth SP local session too. - # ------------------------------------------------------------- - def after_sign_out_path_for(resource_or_scope) - if Rails.application.config.shibboleth_enabled - return Rails.application.config.shibboleth_logout_url + root_url - super - else - super - end - end - # ------------------------------------------------------------- - - def from_external_domain? - if request.referer.present? - referer = URI.parse(request.referer) - home = URI.parse(root_url) - referer.host != home.host - else - false - end - end + end end diff --git a/app/controllers/concerns/conditional_user_mailer.rb b/app/controllers/concerns/conditional_user_mailer.rb index aa7622b..560b93f 100644 --- a/app/controllers/concerns/conditional_user_mailer.rb +++ b/app/controllers/concerns/conditional_user_mailer.rb @@ -1,35 +1,25 @@ -module ConditionalUserMailer - extend ActiveSupport::Concern +# frozen_string_literal: true - # Adds following methods as class methods for the class that include this module - included do - # Executes a given block passed if the recipient user has the preference email key enabled - # @param recipients {User | Enumerable } User object or any object that includes Enumerable class - # @param key {String} - A key (dot notation) whose value is true/false and belongs to prefences.email (see config/branding.yml) - def deliver_if(recipients: [], key:) - raise(ArgumentError, 'key must be String') unless key.is_a?(String) - if block_given? - split_key = key.split('.') - if !recipients.respond_to?(:each) - recipients = Array(recipients) - end - recipients.each do |r| - if r.respond_to?(:get_preferences) - email_hash = r.get_preferences('email') - should_deliver = split_key.reduce(email_hash) do |m,o| - if m.is_a?(Hash) - m[o.to_sym] - else - break - end - end - yield r if should_deliver.is_a?(TrueClass) - end - end - true - else # Block not given - false - end +module ConditionalUserMailer + + # Executes a given block passed if the recipient user has the preference + # email key enabled + # + # recipients - User or Enumerable object or any object that includes Enumerable class + # key - A key (dot notation) whose value is true/false and belongs to + # prefences.email (see config/branding.yml) + # + # Returns Boolean + def deliver_if(recipients: [], key:, &block) + return false unless block_given? + + Array(recipients).each do |recipient| + email_hash = recipient.get_preferences('email').with_indifferent_access + preference_value = !!email_hash.dig(*key.to_s.split('.')) + block.call(recipient) if preference_value end + + true end -end \ No newline at end of file + +end diff --git a/app/controllers/concerns/versionable.rb b/app/controllers/concerns/versionable.rb index 676d4d2..c1893e9 100644 --- a/app/controllers/concerns/versionable.rb +++ b/app/controllers/concerns/versionable.rb @@ -1,106 +1,124 @@ module Versionable - extend ActiveSupport::Concern - included do - ## - # Takes in a Template, phase, Section, Question, or Annotaion - # IF the template is published, generates a new template - # finds the passed object in the new template - # @param obj - Template, Phase, Section, Question, Annotation - # @return type_of(obj) - def get_modifiable(obj) - if obj.respond_to?(:template) - template = obj.template - elsif obj.is_a?(Template) - template = obj - else - raise ArgumentError, _('obj should be a Template, Phase, Section, Question, or Annotation') - end - - new_template = Template.find_or_generate_version!(template) # raises RuntimeError if template is not latest - - if new_template != template - if obj.is_a?(Template) - obj = new_template - else - obj = find_in_space(obj,new_template.phases) - end - end - return obj - end - ## - # Takes in a phase, Section, Question, or Annotation which is newly - # generated and returns a modifiable version of that object - # NOTE: the obj passed is still not saved however it should belongs to a parent already - def get_new(obj) - raise ArgumentError, _('obj should be a Phase, Section, Question, or Annotation') unless obj.respond_to?(:template) - + # Takes in a Template, phase, Section, Question, or Annotaion + # IF the template is published, generates a new template + # finds the passed object in the new template + # + # obj - Template, Phase, Section, Question, Annotation + # + # Returns ActiveRecord::Base + def get_modifiable(obj) + if obj.respond_to?(:template) template = obj.template - new_template = Template.find_or_generate_version!(template) # raises RuntimeError if template is not latest - - if new_template != template # Copied version - case obj - when Phase - belongs = :template - when Section - belongs = :phase - when Question - belongs = :section - when Annotation - belongs = :question - else - raise ArgumentError, _('obj should be a Phase, Section, Question, or Annotation') - end - - if belongs == :template - obj = obj.send(:deep_copy) - obj.template = new_template - else - found = find_in_space(obj.send(belongs), new_template.phases) - obj = obj.send(:deep_copy) - obj.send("#{belongs}=", found) - end - end - return obj + elsif obj.is_a?(Template) + template = obj + else + raise ArgumentError, + _('obj should be a Template, Phase, Section, Question, or Annotation') end + + # raises RuntimeError if template is not latest + new_template = Template.find_or_generate_version!(template) + + if new_template != template + if obj.is_a?(Template) + obj = new_template + else + obj = find_in_space(obj,new_template.phases) + end + end + return obj + end + + ## + # Takes in a phase, Section, Question, or Annotation which is newly + # generated and returns a modifiable version of that object + # NOTE: the obj passed is still not saved however it should belongs to a + # parent already + def get_new(obj) + unless obj.respond_to?(:template) + raise ArgumentError, + _('obj should be a Phase, Section, Question, or Annotation') + end + + template = obj.template + # raises RuntimeError if template is not latest + new_template = Template.find_or_generate_version!(template) + + if new_template != template # Copied version + case obj + when Phase + belongs = :template + when Section + belongs = :phase + when Question + belongs = :section + when Annotation + belongs = :question + else + raise ArgumentError, + _('obj should be a Phase, Section, Question, or Annotation') + end + + if belongs == :template + obj = obj.send(:deep_copy) + obj.template = new_template + else + found = find_in_space(obj.send(belongs), new_template.phases) + obj = obj.send(:deep_copy) + obj.send("#{belongs}=", found) + end + end + return obj end private - # Locates an object (e.g. phase, section, question, annotation) in a search_space - # (e.g. phases/sections/questions/annotations) by comparing either the number method or - # the org_id and text for annotations - def find_in_space(obj, search_space) - raise ArgumentError, _('The search_space does not respond to each') unless search_space.respond_to?(:each) - raise ArgumentError, _('The search space does not have elements associated') unless search_space.length > 0 - if obj.is_a?(search_space.first.class) - if obj.respond_to?(:number) # object is an instance of Phase, Section or Question - return search_space.find{ |search| search.number == obj.number } - elsif obj.respond_to?(:org_id) && obj.respond_to?(:text) # object is an instance of Annotation - return search_space.find{ |annotation| annotation.org_id == obj.org_id && annotation.text == obj.text } + # Locates an object (e.g. phase, section, question, annotation) in a + # search_space + # (e.g. phases/sections/questions/annotations) by comparing either the number + # method or the org_id and text for annotations + def find_in_space(obj, search_space) + unless search_space.respond_to?(:each) + raise ArgumentError, _('The search_space does not respond to each') + end + unless search_space.length > 0 + raise ArgumentError, + _('The search space does not have elements associated') + end + + if obj.is_a?(search_space.first.class) + # object is an instance of Phase, Section or Question + if obj.respond_to?(:number) + return search_space.find{ |search| search.number == obj.number } + # object is an instance of Annotation + elsif obj.respond_to?(:org_id) && obj.respond_to?(:text) + return search_space.find do |annotation| + annotation.org_id == obj.org_id && annotation.text == obj.text end - return nil - end - - case search_space.first - when Phase - number = obj.phase.number - relation = :sections - when Section - number = obj.section.number - relation = :questions - when Question - number = obj.question.number - relation = :annotations - else - return nil - end - - search_space = search_space.find{ |search| search.number == number } - - if search_space.present? - return find_in_space(obj, search_space.send(relation)) end return nil end + + case search_space.first + when Phase + number = obj.phase.number + relation = :sections + when Section + number = obj.section.number + relation = :questions + when Question + number = obj.question.number + relation = :annotations + else + return nil + end + + search_space = search_space.find{ |search| search.number == number } + + if search_space.present? + return find_in_space(obj, search_space.send(relation)) + end + return nil + end end diff --git a/app/controllers/guidance_groups_controller.rb b/app/controllers/guidance_groups_controller.rb index 9bbfae9..74db0c5 100644 --- a/app/controllers/guidance_groups_controller.rb +++ b/app/controllers/guidance_groups_controller.rb @@ -19,7 +19,7 @@ # POST /guidance_groups # POST /guidance_groups.json def admin_create - @guidance_group = GuidanceGroup.new(params[:guidance_group]) + @guidance_group = GuidanceGroup.new(guidance_group_params) authorize @guidance_group @guidance_group.org_id = current_user.org_id if params[:save_publish] @@ -49,8 +49,8 @@ @guidance_group.org_id = current_user.org_id @guidance_group.published = true unless params[:save_publish].nil? - if @guidance_group.update_attributes(params[:guidance_group]) - redirect_to admin_index_guidance_path(params[:guidance_group]), notice: success_message(_('guidance group'), _('saved')) + if @guidance_group.update(guidance_group_params) + redirect_to admin_index_guidance_path(guidance_group_params), notice: success_message(_('guidance group'), _('saved')) else flash[:alert] = failed_update_error(@guidance_group, _('guidance group')) render 'admin_edit' @@ -93,4 +93,10 @@ end end + private + + def guidance_group_params + params.require(:guidance_group) + .permit(:org_id, :name, :optional_subset, :published, :org, :guidances) + end end \ No newline at end of file diff --git a/app/controllers/notes_controller.rb b/app/controllers/notes_controller.rb index 32211b3..061152b 100644 --- a/app/controllers/notes_controller.rb +++ b/app/controllers/notes_controller.rb @@ -6,31 +6,30 @@ def create @note = Note.new - @note.user_id = params[:note][:user_id] - - # create answer if we don't have one already - @answer = nil # if defined within the transaction block, was not accessable afterward + @note.user_id = note_params[:user_id] # ensure user has access to plan BEFORE creating/finding answer - raise Pundit::NotAuthorizedError unless Plan.find(params[:note][:plan_id]).readable_by?(@note.user_id) + unless Plan.find_by(id: note_params[:plan_id]).readable_by?(@note.user_id) + raise Pundit::NotAuthorizedError + end Answer.transaction do - @answer = Answer.find_by(plan_id: params[:note][:plan_id], question_id: params[:note][:question_id]) + @answer = Answer.find_by(plan_id: note_params[:plan_id], question_id: note_params[:question_id]) if @answer.blank? - @answer = Answer.new - @answer.plan_id = params[:note][:plan_id] - @answer.question_id = params[:note][:question_id] - @answer.user_id = @note.user_id + @answer = Answer.new + @answer.plan_id = note_params[:plan_id] + @answer.question_id = note_params[:question_id] + @answer.user_id = @note.user_id @answer.save! end end @note.answer = @answer - @note.text = params[:note][:text] + @note.text = note_params[:text] authorize @note @plan = @answer.plan - @question = Question.find(params[:note][:question_id]) + @question = Question.find(note_params[:question_id]) if @note.save @status = true @@ -43,11 +42,11 @@ @notice = success_message(_('comment'), _('created')) render(json: { "notes" => { - "id" => params[:note][:question_id], + "id" => note_params[:question_id], "html" => render_to_string(partial: 'layout', locals: {plan: @plan, question: @question, answer: @answer }, formats: [:html]) }, "title" => { - "id" => params[:note][:question_id], + "id" => note_params[:question_id], "html" => render_to_string(partial: 'title', locals: { answer: @answer}, formats: [:html]) } }.to_json, status: :created) @@ -63,7 +62,7 @@ def update @note = Note.find(params[:id]) authorize @note - @note.text = params[:note][:text] + @note.text = note_params[:text] @answer = @note.answer @question = @answer.question @@ -71,7 +70,7 @@ question_id = @note.answer.question_id.to_s - if @note.update_attributes(params[:note]) + if @note.update(note_params) @notice = success_message(_('comment'), _('saved')) render(json: { "notes" => { @@ -103,7 +102,7 @@ question_id = @note.answer.question_id.to_s - if @note.update_attributes(params[:note]) + if @note.update(note_params) @notice = success_message(_('comment'), _('removed')) render(json: { "notes" => { @@ -122,4 +121,12 @@ }.to_json, status: :bad_request end end + + private + + def note_params + params.require(:note) + .permit(:text, :archived_by, :user_id, :answer_id, :plan_id, + :question_id) + end end diff --git a/app/controllers/org_admin/phases_controller.rb b/app/controllers/org_admin/phases_controller.rb index ed47690..1b39b6e 100644 --- a/app/controllers/org_admin/phases_controller.rb +++ b/app/controllers/org_admin/phases_controller.rb @@ -1,7 +1,7 @@ module OrgAdmin class PhasesController < ApplicationController include Versionable - + after_action :verify_authorized # GET /org_admin/templates/:template_id/phases/[:id] @@ -13,11 +13,13 @@ end section = params.fetch(:section, nil) render('container', - locals: { + locals: { partial_path: 'show', template: phase.template, phase: phase, - sections: phase.sections.order(:number).select(:id, :title, :modifiable), + prefix_section: phase.prefix_section, + sections: phase.template_sections.order(:number), + suffix_sections: phase.suffix_sections.order(:number), current_section: section.present? ? Section.find_by(id: section, phase_id: phase.id) : nil }) end @@ -32,39 +34,41 @@ redirect_to org_admin_template_phase_path(template_id: phase.template, id: phase.id, section: section) else render('container', - locals: { + locals: { partial_path: 'edit', template: phase.template, phase: phase, + prefix_section: phase.prefix_section, sections: phase.sections.order(:number).select(:id, :title, :modifiable), + suffix_sections: phase.suffix_sections.order(:number), current_section: section.present? ? Section.find_by(id: section, phase_id: phase.id) : nil }) end end - #preview a phase + # preview a phase # GET /org_admin/phases/[:id]/preview def preview phase = Phase.includes(:template).find(params[:id]) authorize phase - render('/org_admin/phases/preview', + render('/org_admin/phases/preview', locals: { template: phase.template, phase: phase }) end - #add a new phase to a passed template + # add a new phase to a passed template # GET /org_admin/phases/new def new template = Template.includes(:phases).find(params[:template_id]) if template.latest? nbr = template.phases.maximum(:number) - phase = Phase.new({ + phase = Phase.new( template: template, modifiable: true, number: (nbr.present? ? nbr + 1 : 1) - }) + ) authorize phase render('/org_admin/templates/container', locals: { @@ -77,8 +81,8 @@ render org_admin_templates_path, alert: _('You canot add a phase to a historical version of a template.') end end - - #create a phase + + # create a phase # POST /org_admin/phases def create phase = Phase.new(phase_params) @@ -102,7 +106,7 @@ end end - #update a phase of a template + # update a phase of a template # PUT /org_admin/phases/[:id] def update phase = Phase.find(params[:id]) @@ -120,7 +124,14 @@ redirect_to edit_org_admin_template_phase_path(template_id: phase.template.id, id: phase.id) end - #delete a phase + def sort + @phase = Phase.find(params[:id]) + authorize @phase + Section.update_numbers!(*params.fetch(:sort_order, []), parent: @phase) + head :ok + end + + # delete a phase # DELETE org_admin/phases/[:id] def destroy phase = Phase.includes(:template).find(params[:id]) @@ -136,7 +147,7 @@ rescue StandardError => e flash[:alert] = _('Unable to create a new version of this template.') end - + if flash[:alert].present? redirect_to org_admin_template_phase_path(template.id, phase.id) else @@ -149,4 +160,4 @@ params.require(:phase).permit(:title, :description, :number) end end -end \ No newline at end of file +end diff --git a/app/controllers/org_admin/plans_controller.rb b/app/controllers/org_admin/plans_controller.rb index b8bd89d..6d1e9a6 100644 --- a/app/controllers/org_admin/plans_controller.rb +++ b/app/controllers/org_admin/plans_controller.rb @@ -4,31 +4,31 @@ def index # Test auth directly and throw Pundit error sincePundit is unaware of namespacing raise Pundit::NotAuthorizedError unless current_user.present? && current_user.can_org_admin? - + vals = Role.access_values_for(:reviewer) @feedback_plans = Plan.joins(:roles).where('roles.user_id = ? and roles.access IN (?)', current_user.id, vals) @plans = current_user.org.plans end - + # GET org_admin/plans/:id/feedback_complete def feedback_complete plan = Plan.find(params[:id]) # Test auth directly and throw Pundit error sincePundit is unaware of namespacing raise Pundit::NotAuthorizedError unless current_user.present? && current_user.can_org_admin? raise Pundit::NotAuthorizedError unless plan.reviewable_by?(current_user.id) - + if plan.complete_feedback(current_user) redirect_to org_admin_plans_path, notice: _('%{plan_owner} has been notified that you have finished providing feedback') % { plan_owner: plan.owner.name(false) } else redirect_to org_admin_plans_path, alert: _('Unable to notify user that you have finished providing feedback.') end end - + # GET /org_admin/download_plans def download_plans # Test auth directly and throw Pundit error sincePundit is unaware of namespacing raise Pundit::NotAuthorizedError unless current_user.present? && current_user.can_org_admin? - + org = current_user.org file_name = org.name.gsub(/ /, "_") header_cols = [ @@ -40,20 +40,20 @@ "#{_('Updated')}", "#{_('Visibility')}" ] - + plans = CSV.generate do |csv| csv << header_cols org.plans.includes(template: :org).order(updated_at: :desc).each do |plan| owner = plan.owner csv << [ - "#{plan.title}", - "#{plan.template.title}", - "#{plan.owner.org.present? ? plan.owner.org.name : ''}", + "#{plan.title}", + "#{plan.template.title}", + "#{plan.owner.org.present? ? plan.owner.org.name : ''}", "#{plan.owner.name(false)}", "#{plan.owner.email}", "#{l(plan.latest_update.to_date, formats: :short)}", - "#{Plan.visibility_message(plan.visibility.to_sym).capitalize}" - ] + "#{Plan::VISIBILITY_MESSAGE[plan.visibility.to_sym].capitalize}" + ] end end diff --git a/app/controllers/org_admin/questions_controller.rb b/app/controllers/org_admin/questions_controller.rb index 69d008b..f2d7ea3 100644 --- a/app/controllers/org_admin/questions_controller.rb +++ b/app/controllers/org_admin/questions_controller.rb @@ -122,32 +122,33 @@ end private - def question_params - params.require(:question).permit(:number, :text, :question_format_id, :option_comment_display, :default_value, question_options_attributes: [:id, :number, :text, :is_default, :_destroy], annotations_attributes: [:id, :text, :org_id, :org, :type], theme_ids: []) - end - # When a template gets versioned by changes to one of its questions we need to loop - # through the incoming params and ensure that the annotations and question_options - # get attached to the new question - def transfer_associations(question) - attrs = question_params - if attrs[:annotations_attributes].present? - attrs[:annotations_attributes].each_key do |key| - old_annotation = question.annotations.select do |a| - a.org_id.to_s == attrs[:annotations_attributes][key][:org_id] && a.type.to_s == attrs[:annotations_attributes][key][:type] - end - attrs[:annotations_attributes][key][:id] = old_annotation.first.id unless old_annotation.empty? + def question_params + params.require(:question).permit(:number, :text, :question_format_id, :option_comment_display, :default_value, question_options_attributes: [:id, :number, :text, :is_default, :_destroy], annotations_attributes: [:id, :text, :org_id, :org, :type], theme_ids: []) + end + + # When a template gets versioned by changes to one of its questions we need to loop + # through the incoming params and ensure that the annotations and question_options + # get attached to the new question + def transfer_associations(question) + attrs = question_params + if attrs[:annotations_attributes].present? + attrs[:annotations_attributes].each_key do |key| + old_annotation = question.annotations.select do |a| + a.org_id.to_s == attrs[:annotations_attributes][key][:org_id] && a.type.to_s == attrs[:annotations_attributes][key][:type] end + attrs[:annotations_attributes][key][:id] = old_annotation.first.id unless old_annotation.empty? end - # TODO: This question_options id swap feel fragile. We cannot really match on any of the - # data elements because the user may have changed them so we rely on its position - # within the array/query since they should be equivalent. - if attrs[:question_options_attributes].present? - attrs[:question_options_attributes].each_key do |key| - attrs[:question_options_attributes][key][:id] = question.question_options[key.to_i].id.to_s if question.question_options[key.to_i].present? - end - end - attrs end + # TODO: This question_options id swap feel fragile. We cannot really match on any of the + # data elements because the user may have changed them so we rely on its position + # within the array/query since they should be equivalent. + if attrs[:question_options_attributes].present? + attrs[:question_options_attributes].each_key do |key| + attrs[:question_options_attributes][key][:id] = question.question_options[key.to_i].id.to_s if question.question_options[key.to_i].present? + end + end + attrs + end end end diff --git a/app/controllers/org_admin/sections_controller.rb b/app/controllers/org_admin/sections_controller.rb index 38bfd62..19467e0 100644 --- a/app/controllers/org_admin/sections_controller.rb +++ b/app/controllers/org_admin/sections_controller.rb @@ -1,7 +1,7 @@ module OrgAdmin class SectionsController < ApplicationController include Versionable - + respond_to :html after_action :verify_authorized @@ -10,14 +10,16 @@ authorize Section.new phase = Phase.includes(:template, :sections).find(params[:phase_id]) edit = phase.template.latest? && (current_user.can_modify_templates? && (phase.template.org_id == current_user.org_id)) - render partial: 'index', - locals: { - template: phase.template, - phase: phase, - sections: phase.sections, + render partial: 'index', + locals: { + template: phase.template, + phase: phase, + prefix_section: phase.prefix_section, + sections: phase.sections, + suffix_sections: phase.suffix_sections, current_section: phase.sections.first, modifiable: edit, - edit: edit + edit: edit } end @@ -26,21 +28,21 @@ section = Section.find(params[:id]) authorize section section = Section.includes(questions: [:annotations, :question_options]).find(params[:id]) - render partial: 'show', locals: { + render partial: 'show', locals: { template: Template.find(params[:template_id]), - section: section + section: section } end # GET /org_admin/templates/[:template_id]/phases/[:phase_id]/sections/[:id]/edit def edit - section = Section.includes({phase: :template}, questions: [:question_options, { annotations: :org }]).find(params[:id]) + section = Section.includes({ phase: :template }, { questions: [:question_options, { annotations: :org }] }).find(params[:id]) authorize section # User cannot edit a section if its not modifiable or the template is not the latest redirect to show - render partial: (section.modifiable? && section.phase.template.latest? ? 'edit' : 'show'), - locals: { - template: section.phase.template, - phase: section.phase, + render partial: (section.modifiable? && section.phase.template.latest? ? 'edit' : 'show'), + locals: { + template: section.phase.template, + phase: section.phase, section: section } end @@ -49,7 +51,7 @@ def create phase = Phase.find(params[:phase_id]) if phase.present? - section = Section.new(section_params.merge({ phase_id: phase.id })) + section = Section.new(section_params.merge(phase_id: phase.id)) authorize section begin section = get_new(section) @@ -84,7 +86,7 @@ rescue StandardError => e flash[:alert] = _('Unable to create a new version of this template.') end - + if flash[:alert].present? redirect_to edit_org_admin_template_phase_path(template_id: section.phase.template.id, id: section.phase.id, section: section.id) else @@ -107,17 +109,18 @@ rescue StandardError => e flash[:alert] = _('Unable to create a new version of this template.') end - + if flash[:alert].present? redirect_to(edit_org_admin_template_phase_path(template_id: phase.template.id, id: phase.id)) else redirect_to(edit_org_admin_template_phase_path(template_id: phase.template.id, id: phase.id)) end end - + private - def section_params - params.require(:section).permit(:title, :description, :number) - end + + def section_params + params.require(:section).permit(:title, :description) + end end -end \ No newline at end of file +end diff --git a/app/controllers/orgs_controller.rb b/app/controllers/orgs_controller.rb index ccc0c8d..135fc5c 100644 --- a/app/controllers/orgs_controller.rb +++ b/app/controllers/orgs_controller.rb @@ -9,7 +9,7 @@ authorize org languages = Language.all.order("name") org.links = {"org": []} unless org.links.present? - render 'admin_edit', locals: {org: org, languages: languages, method: 'PUT', + render 'admin_edit', locals: {org: org, languages: languages, method: 'PUT', url: admin_update_org_path(org) } end @@ -22,23 +22,23 @@ @org.logo = attrs[:logo] if attrs[:logo] tab = (attrs[:feedback_enabled].present? ? 'feedback' : 'profile') if params[:org_links].present? - @org.links = JSON.parse(params[:org_links]) + @org.links = JSON.parse(params[:org_links]) end - + begin # Only allow super admins to change the org types and shib info - if current_user.can_super_admin? + if current_user.can_super_admin? # Handle Shibboleth identifiers if that is enabled - if Rails.application.config.shibboleth_use_filtered_discovery_service + if Rails.application.config.shibboleth_use_filtered_discovery_service && params[:shib_id].present? shib = IdentifierScheme.find_by(name: 'shibboleth') shib_settings = @org.org_identifiers.select{ |ids| ids.identifier_scheme == shib}.first - if params[:shib_id].present? || params[:shib_domain].present? + if !params[:shib_id].blank? shib_settings = OrgIdentifier.new(org: @org, identifier_scheme: shib) unless shib_settings.present? shib_settings.identifier = params[:shib_id] shib_settings.attrs = {domain: params[:shib_domain]} shib_settings.save - else + else if shib_settings.present? # The user cleared the shib values so delete the object shib_settings.destroy @@ -46,7 +46,7 @@ end end end - + if @org.update_attributes(attrs) flash[:notice] = success_message(_('organisation'), _('saved')) redirect_to "#{admin_edit_org_path(@org)}\##{tab}" @@ -63,14 +63,14 @@ # ---------------------------------------------------------------- def shibboleth_ds redirect_to root_path unless current_user.nil? - + @user = User.new - # Display the custom Shibboleth discovery service page. + # Display the custom Shibboleth discovery service page. @orgs = Org.joins(:identifier_schemes).where('identifier_schemes.name = ?', 'shibboleth').sort{|x,y| x.name <=> y.name } - + if @orgs.empty? flash[:alert] = _('No organisations are currently registered.') - redirect_to user_shibboleth_omniauth_authorize_path + redirect_to user_shibboleth_omniauth_authorize_path end end @@ -82,12 +82,12 @@ scheme = IdentifierScheme.find_by(name: 'shibboleth') shib_entity = OrgIdentifier.where(org_id: params['shib-ds'][:org_id], identifier_scheme: scheme) - + if !shib_entity.empty? # Force SSL url = "#{request.base_url.gsub('http:', 'https:')}#{Rails.application.config.shibboleth_login}" target = "#{user_shibboleth_omniauth_callback_url.gsub('http:', 'https:')}" - + #initiate shibboleth login sequence redirect_to "#{url}?target=#{target}&entityID=#{shib_entity.first.identifier}" else @@ -104,6 +104,6 @@ private def org_params params.require(:org).permit(:name, :abbreviation, :logo, :contact_email, :contact_name, :remove_logo, :org_type, - :feedback_enabled, :feedback_email_subject, :feedback_email_msg) + :feedback_enabled, :feedback_email_msg) end end \ No newline at end of file diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 9116fb1..fc23f4b 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -1,17 +1,18 @@ class PasswordsController < Devise::PasswordsController - + protected - + def after_resetting_password_path_for(resource) root_path end - + ## # Override Devise default behaviour by sending user to the home page # after the password reset email has been sent # - # @resource_name [String] The user's email address - # --------------------------------------------------------------------- + # resource_name - The user's email address + # + # Returns String def after_sending_reset_password_instructions_path_for(resource_name) root_path end diff --git a/app/controllers/plans_controller.rb b/app/controllers/plans_controller.rb index 8c5c943..ad44829 100644 --- a/app/controllers/plans_controller.rb +++ b/app/controllers/plans_controller.rb @@ -3,6 +3,8 @@ include ConditionalUserMailer helper PaginableHelper helper SettingsTemplateHelper + include FeedbacksHelper + after_action :verify_authorized, except: [:overview] def index @@ -112,7 +114,11 @@ # GET /plans/show def show - @plan = Plan.eager_load(params[:id]) + @plan = Plan.includes( + template: { phases: { sections: { questions: :answers }}}, + plans_guidance_groups: { guidance_group: :guidances } + ).find(params[:id]) + authorize @plan @visibility = @plan.visibility.present? ? @plan.visibility.to_s : Rails.application.config.default_plan_visibility @@ -120,7 +126,7 @@ @editing = (!params[:editing].nil? && @plan.administerable_by?(current_user.id)) # Get all Guidance Groups applicable for the plan and group them by org - @all_guidance_groups = @plan.get_guidance_group_options + @all_guidance_groups = @plan.guidance_group_options @all_ggs_grouped_by_org = @all_guidance_groups.sort.group_by(&:org) @selected_guidance_groups = @plan.guidance_groups @@ -153,25 +159,14 @@ def edit plan = Plan.find(params[:id]) authorize plan - + plan, phase = Plan.load_for_phase(params[:id], params[:phase_id]) - - readonly = !plan.editable_by?(current_user.id) - + guidance_groups = GuidanceGroup.where(published: true, id: plan.guidance_group_ids) - # Since the answers have been pre-fetched through plan (see Plan.load_for_phase) - # we create a hash whose keys are question id and value is the answer associated - answers = plan.answers.reduce({}){ |m, a| m[a.question_id] = a; m } - - render('/phases/edit', locals: { - base_template_org: phase.template.base_org, - plan: plan, phase: phase, readonly: readonly, - question_guidance: plan.guidance_by_question_as_hash, - guidance_groups: guidance_groups, - answers: answers, - guidance_service: GuidanceService.new(plan) }) + + render_phases_edit(plan, phase, guidance_groups) end - + # PUT /plans/1 # PUT /plans/1.json def update @@ -185,19 +180,19 @@ guidance_group_ids = params[:guidance_group_ids].blank? ? [] : params[:guidance_group_ids].map(&:to_i).uniq @plan.guidance_groups = GuidanceGroup.where(id: guidance_group_ids) @plan.save - + if @plan.update_attributes(attrs) format.html { redirect_to overview_plan_path(@plan), notice: success_message(_('plan'), _('saved')) } format.json {render json: {code: 1, msg: success_message(_('plan'), _('saved'))}} else flash[:alert] = failed_update_error(@plan, _('plan')) - format.html { render action: "edit" } + format.html { render_phases_edit(@plan, @plan.phases.first, @plan.guidance_groups) } format.json {render json: {code: 0, msg: flash[:alert]}} end - + rescue Exception flash[:alert] = failed_update_error(@plan, _('plan')) - format.html { render action: "edit" } + format.html { render_phases_edit(@plan, @plan.phases.first, @plan.guidance_groups) } format.json {render json: {code: 0, msg: flash[:alert]}} end end @@ -230,16 +225,6 @@ end end - # GET /status/1.json - # only returns json, why is this here? - def status - @plan = Plan.find(params[:id]) - authorize @plan - respond_to do |format| - format.json { render json: @plan.status } - end - end - def answer @plan = Plan.find(params[:id]) authorize @plan @@ -268,9 +253,13 @@ @plan = Plan.includes(:answers).find(params[:id]) authorize @plan + @selected_phase = @plan.phases.find(params[:phase_id]) + @show_coversheet = params[:export][:project_details].present? @show_sections_questions = params[:export][:question_headings].present? @show_unanswered = params[:export][:unanswered_questions].present? + @show_custom_sections = params[:export][:custom_sections].present? + @public_plan = false @hash = @plan.as_pdf(@show_coversheet) @@ -280,7 +269,7 @@ respond_to do |format| format.html { render layout: false } - format.csv { send_data @plan.as_csv(@show_sections_questions), filename: "#{file_name}.csv" } + format.csv { send_data @plan.as_csv(@show_sections_questions, @show_unanswered, @selected_phase, @show_custom_sections), filename: "#{file_name}.csv" } format.text { send_data render_to_string(partial: 'shared/export/plan_txt'), filename: "#{file_name}.txt" } format.docx { render docx: "#{file_name}.docx", content: render_to_string(partial: 'shared/export/plan') } format.pdf do @@ -348,24 +337,27 @@ end def request_feedback - plan = Plan.find(params[:id]) - authorize plan + @plan = Plan.find(params[:id]) + authorize @plan alert = _('Unable to submit your request for feedback at this time.') begin - if plan.request_feedback(current_user) - redirect_to share_plan_path(plan), notice: _('Your request for feedback has been submitted.') + if @plan.request_feedback(current_user) + redirect_to share_plan_path(@plan), + notice: _(request_feedback_flash_notice) else - redirect_to share_plan_path(plan), alert: alert + redirect_to share_plan_path(@plan), alert: alert end rescue Exception - redirect_to share_plan_path(plan), alert: alert + redirect_to share_plan_path(@plan), alert: alert end end def overview begin - plan = Plan.overview(params[:id]) + plan = Plan.includes(:phases, :sections, :questions, template: [ :org ]) + .find(params[:id]) + authorize plan render(:overview, locals: { plan: plan }) rescue ActiveRecord::RecordNotFound @@ -375,11 +367,15 @@ end private + def plan_params - params.require(:plan).permit(:org_id, :org_name, :funder_id, :funder_name, :template_id, :title, :visibility, - :grant_number, :description, :identifier, :principal_investigator, - :principal_investigator_email, :principal_investigator_identifier, - :data_contact, :data_contact_email, :data_contact_phone, :guidance_group_ids) + params.require(:plan) + .permit(:org_id, :org_name, :funder_id, :funder_name, :template_id, + :title, :visibility, :grant_number, :description, :identifier, + :principal_investigator_phone, :principal_investigator, + :principal_investigator_email, :data_contact, + :principal_investigator_identifier, :data_contact_email, + :data_contact_phone, :guidance_group_ids) end @@ -402,25 +398,6 @@ groups.values end - - def fixup_hash(plan) - rollup(plan, "notes", "answer_id", "answers") - rollup(plan, "answers", "question_id", "questions") - rollup(plan, "questions", "section_id", "sections") - rollup(plan, "sections", "phase_id", "phases") - - plan["template"]["phases"] = plan.delete("phases") - - ghash = {} - plan["guidance_groups"].map{|g| ghash[g["id"]] = g} - plan["plans_guidance_groups"].each do |pgg| - pgg["guidance_group"] = ghash[ pgg["guidance_group_id"] ] - end - - plan["template"]["org"] = Org.find(plan["template"]["org_id"]).serializable_hash() - end - - # find all object under src_plan_key # merge them into the items under obj_plan_key using # super_id = id @@ -445,4 +422,34 @@ end plan.delete(src_plan_key) end + + # Flash notice for successful feedback requests + # + # Returns String + def request_feedback_flash_notice + # Use the generic feedback confirmation message unless the Org has + # specified one + text = current_user.org.feedback_email_msg || + feedback_confirmation_default_message + feedback_constant_to_text(text, current_user, @plan, current_user.org) + end + + private + + # ============================ + # = Private instance methods = + # ============================ + + def render_phases_edit(plan, phase, guidance_groups) + readonly = !plan.editable_by?(current_user.id) + # Since the answers have been pre-fetched through plan (see Plan.load_for_phase) + # we create a hash whose keys are question id and value is the answer associated + answers = plan.answers.reduce({}){ |m, a| m[a.question_id] = a; m } + render('/phases/edit', locals: { + base_template_org: phase.template.base_org, + plan: plan, phase: phase, readonly: readonly, + guidance_groups: guidance_groups, + answers: answers, + guidance_service: GuidanceService.new(plan) }) + end end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index aa0380d..0231a97 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -5,8 +5,8 @@ @user = current_user @prefs = @user.get_preferences(:email) @languages = Language.sorted_by_abbreviation - @orgs = Org.where(parent_id: nil).order("name") - @other_organisations = Org.where(parent_id: nil, is_other: true).pluck(:id) + @orgs = Org.order("name") + @other_organisations = Org.where(is_other: true).pluck(:id) @identifier_schemes = IdentifierScheme.where(active: true).order(:name) @default_org = current_user.org @@ -66,14 +66,14 @@ if other_org.nil? redirect_to(after_sign_up_error_path_for(resource), alert: _('You cannot be assigned to other organisation since that option does not exist in the system. Please contact your system administrators.')) and return end - params[:user][:org_id] = other_org.id + params[:user][:org_id] = other_org.id end build_resource(sign_up_params) if resource.save if resource.active_for_authentication? set_flash_message :notice, :signed_up if is_navigational_format? sign_up(resource_name, resource) - UserMailer.welcome_notification(current_user).deliver + UserMailer.welcome_notification(current_user).deliver_now unless oauth.nil? # The OAuth provider could not be determined or there was no unique UID! unless oauth['provider'].nil? || oauth['uid'].nil? @@ -102,9 +102,9 @@ def update if user_signed_in? then @prefs = @user.get_preferences(:email) - @orgs = Org.where(parent_id: nil).order("name") + @orgs = Org.order("name") @default_org = current_user.org - @other_organisations = Org.where(parent_id: nil, is_other: true).pluck(:id) + @other_organisations = Org.where(is_other: true).pluck(:id) @identifier_schemes = IdentifierScheme.where(active: true).order(:name) @languages = Language.sorted_by_abbreviation if params[:skip_personal_details] == "true" diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb index 913bc2a..6752ef5 100644 --- a/app/controllers/roles_controller.rb +++ b/app/controllers/roles_controller.rb @@ -9,7 +9,7 @@ authorize @role access_level = params[:role][:access_level].to_i - @role.set_access_level(access_level) + @role.access_level = access_level message = '' if params[:user].present? if @role.plan.owner.present? && @role.plan.owner.email == params[:user] @@ -51,7 +51,7 @@ @role = Role.find(params[:id]) authorize @role access_level = params[:role][:access_level].to_i - @role.set_access_level(access_level) + @role.access_level = access_level if @role.update_attributes(role_params) deliver_if(recipients: @role.user, key: 'users.added_as_coowner') do |r| UserMailer.permissions_change_notification(@role, current_user).deliver_now diff --git a/app/controllers/super_admin/users_controller.rb b/app/controllers/super_admin/users_controller.rb index c0640a3..9cddb77 100644 --- a/app/controllers/super_admin/users_controller.rb +++ b/app/controllers/super_admin/users_controller.rb @@ -8,37 +8,37 @@ if user.present? authorize user languages = Language.sorted_by_abbreviation - orgs = Org.where(parent_id: nil).order("name") + orgs = Org.order("name") identifier_schemes = IdentifierScheme.where(active: true).order(:name) - - render 'super_admin/users/edit', - locals: { user: user, + + render 'super_admin/users/edit', + locals: { user: user, languages: languages, - orgs: orgs, - identifier_schemes: identifier_schemes, + orgs: orgs, + identifier_schemes: identifier_schemes, default_org: user.org } else redirect_to admin_index_users_path, alert: _('User not found.') end end - + def update user = User.find(params[:id]) if user.present? authorize user topic = _('%{username}\'s profile') % { username: user.name(false) } if user.update_attributes(user_params) - redirect_to edit_super_admin_user_path(user), + redirect_to edit_super_admin_user_path(user), notice: _('Successfully updated %{username}') % { username: topic } else - redirect_to edit_super_admin_user_path(user), + redirect_to edit_super_admin_user_path(user), alert: _('Unable to update %{username}') % { username: topic } end else redirect_to edit_super_admin_user_path(user), alert: _('User not found.') end end - + private def user_params params.require(:user).permit(:email, :firstname, :surname, :org_id, :language_id, :other_organisation) diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 38719f9..c9aa43e 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -8,27 +8,26 @@ handle_omniauth(scheme) end end - - ## - # Processes callbacks from an omniauth provider and directs the user to + + # Processes callbacks from an omniauth provider and directs the user to # the appropriate page: # Not logged in and uid had no match ---> Sign Up page # Not logged in and uid had a match ---> Sign In and go to Home Page # Signed in and uid had no match --> Save the uid and go to the Profile Page # Signed in and uid had a match --> Go to the Home Page # - # @scheme [IdentifierScheme] The IdentifierScheme for the provider - # ------------------------------------------------------------- + # scheme - The IdentifierScheme for the provider + # def handle_omniauth(scheme) user = User.from_omniauth(request.env["omniauth.auth"].nil? ? request.env : request.env["omniauth.auth"]) - + # If the user isn't logged in - if current_user.nil? + if current_user.nil? # If the uid didn't have a match in the system send them to register if user.nil? session["devise.#{scheme.name.downcase}_data"] = request.env["omniauth.auth"] redirect_to new_user_registration_url - + # Otherwise sign them in else # Until ORCID becomes supported as a login method @@ -40,20 +39,20 @@ redirect_to new_user_registration_url end end - + # The user is already logged in and just registering the uid with us else # If the user could not be found by that uid then attach it to their record if user.nil? - if UserIdentifier.create(identifier_scheme: scheme, + if UserIdentifier.create(identifier_scheme: scheme, identifier: request.env["omniauth.auth"].uid, user: current_user) - + flash[:notice] = _('Your account has been successfully linked to %{scheme}.') % { scheme: scheme.description } else flash[:alert] = _('Unable to link your account to %{scheme}.') % { scheme: scheme.description } end - + else # If a user was found but does NOT match the current user then the identifier has # already been attached to another account (likely the user has 2 accounts) @@ -61,8 +60,8 @@ if identifier.user.id != current_user.id flash[:alert] = _("The current #{scheme.description} iD has been already linked to a user with email #{identifier.user.email}") end - - # Otherwise, the identifier was found and it matches the one already associated + + # Otherwise, the identifier was found and it matches the one already associated # with the current user so nothing else needs to be done end @@ -71,7 +70,6 @@ end end - # ------------------------------------------------------------- def failure redirect_to root_path end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a2a6012..ea0077a 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,5 +1,5 @@ module ApplicationHelper - + def resource_name :user end @@ -13,11 +13,6 @@ def devise_mapping @devise_mapping ||= Devise.mappings[:user] end - - # --------------------------------------------------------------------------- - def hash_to_js_json_variable(obj_name, hash) - "".html_safe - end # Determines whether or not the URL path passed matches with the full path (including params) of the last URL requested. # see http://api.rubyonrails.org/classes/ActionDispatch/Request.html#method-i-fullpath for details @@ -30,10 +25,6 @@ end end - def is_integer?(string) - return string.present? && string.match(/^(\d)+$/) - end - def fingerprinted_asset(name) Rails.env.production? ? "#{name}-#{ASSET_FINGERPRINT}" : name end diff --git a/app/helpers/feedbacks_helper.rb b/app/helpers/feedbacks_helper.rb new file mode 100644 index 0000000..15fca1b --- /dev/null +++ b/app/helpers/feedbacks_helper.rb @@ -0,0 +1,18 @@ +module FeedbacksHelper + def feedback_confirmation_default_subject + _('%{application_name}: Your plan has been submitted for feedback') + end + + def feedback_confirmation_default_message + _('

Hello %{user_name}.

'\ + '

Your plan "%{plan_name}" has been submitted for feedback from an administrator at your organisation. '\ + 'If you have questions pertaining to this action, please contact us at %{organisation_email}.

') + end + + def feedback_constant_to_text(text, user, plan, org) + _("#{text}") % {application_name: Rails.configuration.branding[:application][:name], + user_name: user.name, + plan_name: plan.title, + organisation_email: org.contact_email} + end +end diff --git a/app/helpers/mailer_helper.rb b/app/helpers/mailer_helper.rb index a7ee2df..d837693 100644 --- a/app/helpers/mailer_helper.rb +++ b/app/helpers/mailer_helper.rb @@ -1,21 +1,5 @@ module MailerHelper include PermsHelper - def feedback_confirmation_default_subject - _('%{application_name}: Your plan has been submitted for feedback') - end - - def feedback_confirmation_default_message - _('

Hello %{user_name}.

'\ - '

Your plan "%{plan_name}" has been submitted for feedback from an administrator at your organisation. '\ - 'If you have questions pertaining to this action, please contact us at %{organisation_email}.

') - end - - def feedback_constant_to_text(text, user, plan, org) - _("#{text}") % {application_name: Rails.configuration.branding[:application][:name], - user_name: user.name, - plan_name: plan.title, - organisation_email: org.contact_email} - end # Returns an unordered HTML list with the permissions associated to the user passed def privileges_list(user) @@ -23,7 +7,7 @@ names = name_and_text r= "" end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 9ee9a66..34d20de 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,6 +1,8 @@ module NotificationsHelper - # Return FA html class depending on Notification level - # @return [String] Font Awesome HTML class + + # FA html class depending on Notification level + # + # Returns String def fa_classes(notification) case notification.level when 'warning' diff --git a/app/helpers/orgs_helper.rb b/app/helpers/orgs_helper.rb new file mode 100644 index 0000000..a2be874 --- /dev/null +++ b/app/helpers/orgs_helper.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module OrgsHelper + + DEFAULT_EMAIL = '%{organisation_email}' + + # Tooltip string for Org feedback form. + # + # org - The current Org we're updating feedback form for. + # + # Returns String + def tooltip_for_org_feedback_form(org) + email = org.contact_email.presence || DEFAULT_EMAIL + _("SAMPLE MESSAGE: A data librarian from %{org_name} will respond to your request within 48 + hours. If you have questions pertaining to this action please contact us + at %{organisation_email}.") % { + organisation_email: email, + org_name: org.name + } + end +end diff --git a/app/helpers/plans_helper.rb b/app/helpers/plans_helper.rb index 1b9cb6d..f761b11 100644 --- a/app/helpers/plans_helper.rb +++ b/app/helpers/plans_helper.rb @@ -1,27 +1,10 @@ module PlansHelper - # Shows whether the user has default, template-default or custom settings - # for the given plan. - # -------------------------------------------------------- - def plan_settings_indicator(plan) - plan_settings = plan.super_settings(:export) - template_settings = plan.template.try(:settings, :export) - - key = if plan_settings.try(:value?) - plan_settings.formatting == template_settings.formatting ? "template_formatting" : "custom_formatting" - elsif template_settings.try(:value?) - "template_formatting" - else - "default_formatting" - end - - content_tag(:small, t("helpers.settings.plans.#{key}")) - end # display the role of the user for a given plan def display_role(role) if role.creator? access = _('Owner') - + else case role.access_level when 3 @@ -48,7 +31,7 @@ return "#{_('Private')}" # Test Plans end end - + def visibility_tooltip(val) case val when 'organisationally_visible' @@ -59,4 +42,15 @@ return _('Private: restricted to me and people I invite.') end end + + def download_plan_page_title(plan, phase, hash) + # If there is more than one phase show the plan title and phase title + return hash[:phases].many? ? "#{plan.title} - #{phase[:title]}" : plan.title + end + + def display_questions_and_section_headings(section, show_sections_questions, show_custom_sections) + # Return true if show_sections_questions is true and either section not customised, or section is customised + # and show_custom_sections is true + return show_sections_questions && (!section[:modifiable] || (show_custom_sections && section[:modifiable])) + end end diff --git a/app/helpers/sections_helper.rb b/app/helpers/sections_helper.rb new file mode 100644 index 0000000..6b16842 --- /dev/null +++ b/app/helpers/sections_helper.rb @@ -0,0 +1,21 @@ +module SectionsHelper + # HREF attribute value for headers in the section partials. If the section + # is modifiable, returns the section path, otherwise the edit section path. + # + # section - The section to return a URL for + # phase - The phase that section belongs + # template - The template that phase belongs to + # + # Returns String + def header_path_for_section(section, phase, template) + if section.modifiable? + edit_org_admin_template_phase_section_path(template_id: template.id, + phase_id: phase.id, + id: section.id) + else + org_admin_template_phase_section_path(template_id: template.id, + phase_id: phase.id, + id: section.id) + end + end +end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 13cab1e..525773b 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -1,6 +1,8 @@ class UserMailer < ActionMailer::Base include MailerHelper helper MailerHelper + helper FeedbacksHelper + default from: Rails.configuration.branding[:organisation][:email] def welcome_notification(user) @@ -106,8 +108,8 @@ end end - # @param commenter - User who wrote the comment - # @param plan - Plan for which the comment is associated to + # commenter - User who wrote the comment + # plan - Plan for which the comment is associated to def new_comment(commenter, plan) if commenter.is_a?(User) && plan.is_a?(Plan) owner = plan.owner diff --git a/app/models/annotation.rb b/app/models/annotation.rb index ccc1121..6c7d7b5 100644 --- a/app/models/annotation.rb +++ b/app/models/annotation.rb @@ -1,39 +1,85 @@ +# == Schema Information +# +# Table name: annotations +# +# id :integer not null, primary key +# text :text +# type :integer default(0), not null +# created_at :datetime +# updated_at :datetime +# org_id :integer +# question_id :integer +# +# Indexes +# +# index_annotations_on_question_id (question_id) +# +# Foreign Keys +# +# fk_rails_... (org_id => orgs.id) +# fk_rails_... (question_id => questions.id) +# + class Annotation < ActiveRecord::Base - enum type: [ :example_answer, :guidance] + include ValidationMessages + ## - # Associations + # I liked type as the name for the enum so overriding inheritance column + self.inheritance_column = nil + + enum type: [ :example_answer, :guidance] + + # ================ + # = Associations = + # ================ + belongs_to :org belongs_to :question has_one :section, through: :question has_one :phase, through: :question has_one :template, through: :question - ## - # I liked type as the name for the enum so overriding inheritance column - self.inheritance_column = nil + # =============== + # = Validations = + # =============== - validates :question, :org, presence: {message: _("can't be blank")} + validates :text, presence: { message: PRESENCE_MESSAGE } - ## - # returns the text from the annotation + validates :org, presence: { message: PRESENCE_MESSAGE } + + validates :question, presence: { message: PRESENCE_MESSAGE } + + validates :type, presence: { message: PRESENCE_MESSAGE }, + uniqueness: { message: UNIQUENESS_MESSAGE, + scope: [:question_id, :org_id] } + + + # ================= + # = Class Methods = + # ================= + + # Deep copy the given annotation and all it's associations # - # @return [String] the text from the annotation - def to_s - "#{text}" - end - - - ## - # deep copy the given annotation and all it's associations + # annotation - To be deep copied # - # @params [Annotation] annotation to be deep copied - # @return [Annotation] the saved, copied annotation + # Returns Annotation def self.deep_copy(annotation) annotation_copy = annotation.dup annotation_copy.save! return annotation_copy end + # =========================== + # = Public instance methods = + # =========================== + + # The text from the annotation + # + # Returns String + def to_s + "#{text}" + end + def deep_copy(**options) copy = self.dup copy.question_id = options.fetch(:question_id, nil) diff --git a/app/models/answer.rb b/app/models/answer.rb index 4e8a3e5..738dc5d 100644 --- a/app/models/answer.rb +++ b/app/models/answer.rb @@ -1,134 +1,150 @@ -class Answer < ActiveRecord::Base - - after_save do |answer| - if answer.plan_id.present? - plan = answer.plan - complete = plan.no_questions_matches_no_answers? - if plan.complete != complete - plan.complete = complete - plan.save! - else - plan.touch # Force updated_at changes if nothing changed since save only saves if changes were made to the record - end - end - end - - ## - # Associations - belongs_to :question - belongs_to :user - belongs_to :plan - has_many :notes, dependent: :destroy - has_and_belongs_to_many :question_options, join_table: "answers_question_options" - - has_many :notes - - ## - # Possibly needed for active_admin - # -relies on protected_attributes gem as syntax depricated in rails 4.2 - attr_accessible :text, :plan_id, :lock_version, :question_id, :user_id, :question_option_ids, - :question, :user, :plan, :question_options, :notes, :note_ids, :id, - :as => [:default, :admin] - - ## - # Validations -# validates :user, :plan, :question, presence: true -# -# # Make sure there is only one answer per question! -# validates :question, uniqueness: {scope: [:plan], -# message: I18n.t('helpers.answer.only_one_per_question')} -# -# # The answer MUST have a text value if the question is NOT option based or a question_option if -# # it is option based. -# validates :text, presence: true, if: Proc.new{|a| -# (a.question.nil? ? false : !a.question.question_format.option_based?) -# } -# validates :question_options, presence: true, if: Proc.new{|a| -# (a.question.nil? ? false : a.question.question_format.option_based?) -# } -# -# # Make sure the plan and question are associated with the same template! -# validates :plan, :question, answer_for_correct_template: true - - ## - # deep copy the given answer - # - # @params [Answer] question_option to be deep copied - # @return [Answer] the saved, copied answer - def self.deep_copy(answer) - answer_copy = answer.dup - answer.question_options.each do |opt| - answer_copy.question_options << opt - end - answer_copy.save! - return answer_copy - end - - # This method helps to decide if an answer option (:radiobuttons, :checkbox, etc ) in form views should be checked or not - # Returns true if the given option_id is present in question_options, otherwise returns false - def has_question_option(option_id) - self.question_option_ids.include?(option_id) - end - - # Returns true if the answer is valid and false otherwise. If the answer's question is option_based, it is checked if exist - # any question_option selected. For non option_based (e.g. textarea or textfield), it is checked the presence of text - def is_valid? - if self.question.present? - if self.question.question_format.option_based? - return !self.question_options.empty? - else # (e.g. textarea or textfield question formats) - return self.text.present? - end - end - return false - end - # Returns answer notes whose archived is blank sorted by updated_at in descending order - def non_archived_notes - return notes.select{ |n| n.archived.blank? }.sort!{ |x,y| y.updated_at <=> x.updated_at } - end - - ## - # Returns True if answer text is blank, false otherwise - # specificly we want to remove empty hml tags and check - # - # @return [Boolean] is the answer's text blank - def is_blank? - if self.text.present? - return self.text.gsub(/<\/?p>/, '').gsub(//, '').chomp.blank? - end - # no text so blank - return true - end - - ## - # Returns the parsed JSON hash for the current answer object - # Generates a new hash if none exists for rda_questions - # - # @return [Hash] the parsed hash of the answer. - # Should have keys 'standards', 'text' - # 'standards' is a list of : pairs - # 'text' is the text from the comments box - def answer_hash - default = {'standards' => {}, 'text' => ''} - begin - h = self.text.nil? ? default : JSON.parse(self.text) - rescue JSON::ParserError => e - h = default - end - return h - end - - ## - # Given a hash of standards and a comment value, this updates answer - # text for rda_questions - # - # @param [standards] a hash of standards - # @param [text] option comment text - # nothing returned, but the status of the text field of the answer is changed - def update_answer_hash(standards={},text="") - h = {} - h['standards'] = standards - h['text'] = text - self.text = h.to_json - end -end +# == Schema Information +# +# Table name: answers +# +# id :integer not null, primary key +# lock_version :integer default(0) +# text :text +# created_at :datetime +# updated_at :datetime +# plan_id :integer +# question_id :integer +# user_id :integer +# +# Indexes +# +# index_answers_on_plan_id (plan_id) +# index_answers_on_question_id (question_id) +# +# Foreign Keys +# +# fk_rails_... (plan_id => plans.id) +# fk_rails_... (question_id => questions.id) +# fk_rails_... (user_id => users.id) +# + +class Answer < ActiveRecord::Base + include ValidationMessages + + after_save do |answer| + if answer.plan_id.present? + plan = answer.plan + complete = plan.no_questions_matches_no_answers? + if plan.complete != complete + plan.complete = complete + plan.save! + else + plan.touch # Force updated_at changes if nothing changed since save only saves if changes were made to the record + end + end + end + + ## + # Associations + belongs_to :question + belongs_to :user + belongs_to :plan + has_many :notes, dependent: :destroy + has_and_belongs_to_many :question_options, join_table: "answers_question_options" + + has_many :notes + + + # =============== + # = Validations = + # =============== + + validates :plan, presence: { message: PRESENCE_MESSAGE } + + validates :user, presence: { message: PRESENCE_MESSAGE } + + validates :question, presence: { message: PRESENCE_MESSAGE }, + uniqueness: { message: UNIQUENESS_MESSAGE, + scope: :plan_id } + + ## + # deep copy the given answer + # + # answer - question_option to be deep copied + # + # Returns Answer + def self.deep_copy(answer) + answer_copy = answer.dup + answer.question_options.each do |opt| + answer_copy.question_options << opt + end + answer_copy + end + + # This method helps to decide if an answer option (:radiobuttons, :checkbox, etc ) in + # form views should be checked or not + # + # Returns Boolean + def has_question_option(option_id) + self.question_option_ids.include?(option_id) + end + + # If the answer's question is option_based, it is checked if exist any question_option + # selected. For non option_based (e.g. textarea or textfield), it is checked the + # presence of text + # + # Returns Boolean + def is_valid? + if self.question.present? + if self.question.question_format.option_based? + return !self.question_options.empty? + else # (e.g. textarea or textfield question formats) + return self.text.present? + end + end + return false + end + + # Answer notes whose archived is blank sorted by updated_at in descending order + # + # Returns Array + def non_archived_notes + return notes.select{ |n| n.archived.blank? }.sort!{ |x,y| y.updated_at <=> x.updated_at } + end + + # Returns True if answer text is blank, false otherwise specificly we want to remove + # empty hml tags and check. + # + # Returns Boolean + def is_blank? + if self.text.present? + return self.text.gsub(/<\/?p>/, '').gsub(/<br\s?\/?>/, '').chomp.blank? + end + # no text so blank + return true + end + + # The parsed JSON hash for the current answer object. Generates a new hash if none + # exists for rda_questions. + # + # Returns Hash + def answer_hash + default = {'standards' => {}, 'text' => ''} + begin + h = self.text.nil? ? default : JSON.parse(self.text) + rescue JSON::ParserError => e + h = default + end + return h + end + + ## + # Given a hash of standards and a comment value, this updates answer text for + # rda_questions + # + # standards - A Hash of standards + # text - A String with option comment text + # + # Returns String + def update_answer_hash(standards={},text="") + h = {} + h['standards'] = standards + h['text'] = text + self.text = h.to_json + end +end diff --git a/app/models/concerns/acts_as_sortable.rb b/app/models/concerns/acts_as_sortable.rb new file mode 100644 index 0000000..f5ebc6e --- /dev/null +++ b/app/models/concerns/acts_as_sortable.rb @@ -0,0 +1,44 @@ +module ActsAsSortable + extend ActiveSupport::Concern + + module ClassMethods + + def update_numbers!(*ids, parent:) + # Ensure only records belonging to this parent are included. + ids = ids.map(&:to_i) & parent.public_send("#{model_name.singular}_ids") + return if ids.empty? + case connection.adapter_name + when "PostgreSQL" then update_numbers_postgresql!(ids) + when "Mysql2" then update_numbers_mysql2!(ids) + else + update_numbers_sequentially!(ids) + end + end + + private + + def update_numbers_postgresql!(ids) + # Build an Array with each ID and its relative position in the Array + values = ids.each_with_index.map { |id, i| "(#{id}, #{i + 1})" }.join(",") + # Run a single UPDATE query for all records. + query = <<~SQL + UPDATE #{table_name} \ + SET number = svals.number \ + FROM (VALUES #{sanitize_sql(values)}) AS svals(id, number) \ + WHERE svals.id = #{table_name}.id; + SQL + connection.execute(query) + end + + def self.update_numbers_mysql2!(ids) + ids_string = ids.map { |id| "'#{id}'" }.join(",") + update_all(%Q{ number = FIELD(id, #{sanitize_sql(ids_string)}) }) + end + + def self.update_numbers_sequentially!(ids) + ids.each_with_index.map do |id, number| + find(id).update_attribute(:number, number + 1) + end + end + end +end diff --git a/app/models/concerns/exportable_plan.rb b/app/models/concerns/exportable_plan.rb index f4cec41..6a6fdde 100644 --- a/app/models/concerns/exportable_plan.rb +++ b/app/models/concerns/exportable_plan.rb @@ -1,122 +1,127 @@ module ExportablePlan - extend ActiveSupport::Concern - included do - - def as_pdf(coversheet = false) - prepare(coversheet) - end - - def as_csv(headings = true, unanswered = true) - hash = prepare(false) - - CSV.generate do |csv| - hdrs = (hash[:phases].length > 1 ? [_('Phase')] : []) - if headings - hdrs << [_('Section'),_('Question'),_('Answer')] - else - hdrs << [_('Answer')] - end - - csv << hdrs.flatten - hash[:phases].each do |phase| - phase[:sections].each do |section| - section[:questions].each do |question| - answer = self.answer(question[:id], false) - answer_text = answer.present? ? answer.text : (unanswered ? _('Not Answered') : '') - flds = (hash[:phases].length > 1 ? [phase[:title]] : []) - if headings - if question[:text].is_a? String - question_text = question[:text] - else - question_text = (question[:text].length > 1 ? question[:text].join(', ') : question[:text][0]) - end - flds << [ section[:title], sanitize_text(question_text), sanitize_text(answer_text) ] - else - flds << [ sanitize_text(answer_text) ] - end - - csv << flds.flatten - end - end - end - end - end - - private - def prepare(coversheet = false) - hash = coversheet ? prepare_coversheet : {} - template = Template.includes(phases: { sections: {questions: :question_format } }). - joins(phases: { sections: { questions: :question_format } }). - where(id: self.template_id).order('sections.number', 'questions.number').first - - hash[:title] = self.title - hash[:answers] = self.answers - - # add the relevant questions/answers - phases = [] - template.phases.each do |phase| - phs = { title: phase.title, number: phase.number, sections: [] } - phase.sections.each do |section| - sctn = { title: section.title, number: section.number, questions: [] } - section.questions.each do |question| - txt = question.text - sctn[:questions] << { id: question.id, text: txt, format: question.question_format } - end - phs[:sections] << sctn - end - phases << phs - end - hash[:phases] = phases - - record_plan_export(:pdf) - - hash - end - - def prepare_coversheet - hash = {} - # name of owner and any co-owners - attribution = self.owner.present? ? [self.owner.name(false)] : [] - self.roles.administrator.not_creator.each do |role| - attribution << role.user.name(false) - end - hash[:attribution] = attribution - - # Org name of plan owner's org - hash[:affiliation] = self.owner.present? ? self.owner.org.name : '' - - # set the funder name - hash[:funder] = self.funder_name.present? ? self.funder_name : (self.template.org.present? ? self.template.org.name : '') - - # set the template name and customizer name if applicable - hash[:template] = self.template.title - customizer = "" - cust_questions = self.questions.where(modifiable: true).pluck(:id) - # if the template is customized, and has custom answered questions - if self.template.customization_of.present? && Answer.where(plan_id: self.id, question_id: cust_questions).present? - customizer = _(" Customised By: ") + self.template.org.name - end - hash[:customizer] = customizer - hash - end - - def record_plan_export(format) - exported_plan = ExportedPlan.new.tap do |ep| - ep.plan = self - ep.phase_id = self.phases.first.id - ep.format = format - plan_settings = self.settings(:export) - - Settings::Template::DEFAULT_SETTINGS.each do |key, value| - ep.settings(:export).send("#{key}=", plan_settings.send(key)) - end - end - exported_plan.save - end - - def sanitize_text(text) - if (!text.nil?) then ActionView::Base.full_sanitizer.sanitize(text.gsub(/ /i,"")) end - end + def as_pdf(coversheet = false) + prepare(coversheet) end + + def as_csv(headings = true, unanswered = true, selected_phase = nil, show_custom_sections = true) + hash = prepare(false) + + CSV.generate do |csv| + hdrs = (hash[:phases].many? ? [_('Phase')] : []) + if headings + hdrs << [_('Section'),_('Question'),_('Answer')] + else + hdrs << [_('Answer')] + end + + csv << hdrs.flatten + hash[:phases].each do |phase| + if selected_phase.nil? || phase[:title] == selected_phase.title + phase[:sections].each do |section| + # Return true if either section not customised, or section is customised + # and unanswered is true + if !section[:modifiable] || (show_custom_sections && section[:modifiable]) + section[:questions].each do |question| + answer = self.answer(question[:id], false) + if answer.present? || (answer.blank? && unanswered) + answer_text = answer.present? ? answer.text : (unanswered ? _('Not Answered') : '') + flds = (hash[:phases].many? ? [phase[:title]] : []) + if headings + if question[:text].is_a? String + question_text = question[:text] + else + question_text = (question[:text].many? ? question[:text].join(', ') : question[:text][0]) + end + flds << [ section[:title], sanitize_text(question_text), sanitize_text(answer_text) ] + else + flds << [ sanitize_text(answer_text) ] + end + csv << flds.flatten + end + end + end + end + end + end + end + end + + private + + def prepare(coversheet = false) + hash = coversheet ? prepare_coversheet : {} + template = Template.includes(phases: { sections: {questions: :question_format } }). + joins(phases: { sections: { questions: :question_format } }). + where(id: self.template_id).order('sections.number', 'questions.number').first + + hash[:title] = self.title + hash[:answers] = self.answers + + # add the relevant questions/answers + phases = [] + template.phases.each do |phase| + phs = { title: phase.title, number: phase.number, sections: [] } + phase.sections.each do |section| + sctn = { title: section.title, number: section.number, questions: [], modifiable: section.modifiable } + section.questions.each do |question| + txt = question.text + sctn[:questions] << { id: question.id, text: txt, format: question.question_format } + end + phs[:sections] << sctn + end + phases << phs + end + hash[:phases] = phases + + record_plan_export(:pdf) + + hash + end + + def prepare_coversheet + hash = {} + # name of owner and any co-owners + attribution = self.owner.present? ? [self.owner.name(false)] : [] + self.roles.administrator.not_creator.each do |role| + attribution << role.user.name(false) + end + hash[:attribution] = attribution + + # Org name of plan owner's org + hash[:affiliation] = self.owner.present? ? self.owner.org.name : '' + + # set the funder name + hash[:funder] = self.funder_name.present? ? self.funder_name : (self.template.org.present? ? self.template.org.name : '') + + # set the template name and customizer name if applicable + hash[:template] = self.template.title + customizer = "" + cust_questions = self.questions.where(modifiable: true).pluck(:id) + # if the template is customized, and has custom answered questions + if self.template.customization_of.present? && Answer.where(plan_id: self.id, question_id: cust_questions).present? + customizer = _(" Customised By: ") + self.template.org.name + end + hash[:customizer] = customizer + hash + end + + def record_plan_export(format) + exported_plan = ExportedPlan.new.tap do |ep| + ep.plan = self + ep.phase_id = self.phases.first.id + ep.format = format + plan_settings = self.settings(:export) + + Settings::Template::DEFAULT_SETTINGS.each do |key, value| + ep.settings(:export).send("#{key}=", plan_settings.send(key)) + end + end + exported_plan.save + end + + def sanitize_text(text) + ActionView::Base.full_sanitizer.sanitize(text.to_s.gsub(/ /i,"")) + end + end diff --git a/app/models/concerns/json_link_validator.rb b/app/models/concerns/json_link_validator.rb index 8315a7d..739174c 100644 --- a/app/models/concerns/json_link_validator.rb +++ b/app/models/concerns/json_link_validator.rb @@ -1,32 +1,15 @@ module JSONLinkValidator - extend ActiveSupport::Concern - included do - # Parses a stringified JSON according to validate_links (e.g. [{ link: String, text: String}, ...]) - # param {String} the stringified JSON value - # Returns an Array of hashes after decoding/validating the stringified JSON passed, otherwise nil - def parse_links(value) - return nil unless value.is_a?(String) - begin - parsed_value = JSON.parse(value) - return valid_links?(parsed_value) ? parsed_value : nil - rescue JSON::ParserError - nil + # Validates whether or not the value passed is conforming to + # [{ link: String, text: String}, ...] + def valid_links?(value) + if value.is_a?(Array) + r = value.all? do |o| + o.is_a?(Hash) && o.key?('link') && o.key?('text') && + o['link'].is_a?(String) && o['text'].is_a?(String) end + return r end - # Validates whether or not the value passed is conforming to [{ link: String, text: String}, ...] - def valid_links?(value) - if value.is_a?(Array) - r = value.all? do |o| - o.is_a?(Hash) && - o.has_key?('link') && - o.has_key?('text') && - o['link'].is_a?(String) && - o['text'].is_a?(String) - end - return r - end - false - end + false end end \ No newline at end of file diff --git a/app/models/concerns/validation_messages.rb b/app/models/concerns/validation_messages.rb new file mode 100644 index 0000000..f0f711f --- /dev/null +++ b/app/models/concerns/validation_messages.rb @@ -0,0 +1,10 @@ +module ValidationMessages + # frozen_string_literal: true + + PRESENCE_MESSAGE = _("can't be blank") + + UNIQUENESS_MESSAGE = _("must be unique") + + INCLUSION_MESSAGE = _("isn't a valid value") + +end diff --git a/app/models/concerns/validation_values.rb b/app/models/concerns/validation_values.rb new file mode 100644 index 0000000..dff74c3 --- /dev/null +++ b/app/models/concerns/validation_values.rb @@ -0,0 +1,3 @@ +module ValidationValues + BOOLEAN_VALUES = [true, false].freeze +end \ No newline at end of file diff --git a/app/models/exported_plan.rb b/app/models/exported_plan.rb index c1825e2..24a854a 100644 --- a/app/models/exported_plan.rb +++ b/app/models/exported_plan.rb @@ -1,23 +1,29 @@ +# == Schema Information +# +# Table name: exported_plans +# +# id :integer not null, primary key +# format :string +# created_at :datetime not null +# updated_at :datetime not null +# phase_id :integer +# plan_id :integer +# user_id :integer +# + class ExportedPlan < ActiveRecord::Base + include ValidationMessages include GlobalHelpers include SettingsTemplateHelper -# TODO: REMOVE AND HANDLE ATTRIBUTE SECURITY IN THE CONTROLLER! - attr_accessible :plan_id, :user_id, :format, :user, :plan, :as => [:default, :admin] #associations between tables belongs_to :plan belongs_to :user - VALID_FORMATS = ['csv', 'html', 'pdf', 'text', 'docx'] + validates :plan, presence: { message: PRESENCE_MESSAGE } - validates :format, inclusion: { - in: VALID_FORMATS, - message: -> (object, data) do - _('%{value} is not a valid format') % { :value => data[:value] } - end - } - validates :plan, :format, presence: {message: _("can't be blank")} + validates :format, presence: { message: PRESENCE_MESSAGE } # Store settings with the exported plan so it can be recreated later # if necessary (otherwise the settings associated with the plan at a diff --git a/app/models/guidance.rb b/app/models/guidance.rb index 08a5cb7..602e7ad 100644 --- a/app/models/guidance.rb +++ b/app/models/guidance.rb @@ -1,3 +1,27 @@ +# Guidance provides information from organisations to Users, helping them when +# answering questions. (e.g. "Here's how to think about your data +# protection responsibilities...") +# +# == Schema Information +# +# Table name: guidances +# +# id :integer not null, primary key +# published :boolean +# text :text +# created_at :datetime not null +# updated_at :datetime not null +# guidance_group_id :integer +# +# Indexes +# +# index_guidances_on_guidance_group_id (guidance_group_id) +# +# Foreign Keys +# +# fk_rails_... (guidance_group_id => guidance_groups.id) +# + # [+Project:+] DMPRoadmap # [+Description:+] # This class keeps the information organisations enter to support users when answering questions. @@ -5,27 +29,30 @@ # [+Created:+] 07/07/2014 # [+Copyright:+] Digital Curation Centre and California Digital Library - - class Guidance < ActiveRecord::Base include GlobalHelpers + include ValidationMessages + include ValidationValues - ## - # Associations + # ================ + # = Associations = + # ================ + belongs_to :guidance_group + has_and_belongs_to_many :themes, join_table: "themes_in_guidance" -# depricated, but required for migration "single_group_for_guidance" - #has_and_belongs_to_many :guidance_groups, join_table: "guidance_in_group" - # EVALUATE CLASS AND INSTANCE METHODS BELOW - # - # What do they do? do they do it efficiently, and do we need them? + # =============== + # = Validations = + # =============== + validates :text, presence: { message: PRESENCE_MESSAGE } + validates :guidance_group, presence: { message: PRESENCE_MESSAGE } - - validates :text, presence: {message: _("can't be blank")} + validates :published, inclusion: { message: INCLUSION_MESSAGE, + in: BOOLEAN_VALUES} # Retrieves every guidance associated to an org scope :by_org, -> (org) { @@ -34,48 +61,26 @@ scope :search, -> (term) { search_pattern = "%#{term}%" - joins(:guidance_group).where("guidances.text LIKE ? OR guidance_groups.name LIKE ?", search_pattern, search_pattern) + joins(:guidance_group) + .where("guidances.text LIKE ? OR guidance_groups.name LIKE ?", + search_pattern, + search_pattern) } - ## - # Determine if a guidance is in a group which belongs to a specified organisation - # - # @param org_id [Integer] the integer id for an organisation - # @return [Boolean] true if this guidance is in a group belonging to the specified organisation, false otherwise - def in_group_belonging_to?(org_id) - unless guidance_group.nil? - if guidance_group.org.id == org_id - return true - end - end - return false - end - ## - # returns all templates belgonging to a specified guidance group - # - # @param guidance_group [Integer] the integer id for an guidance_group - # @return [Array<Templates>] list of templates - def get_guidance_group_templates? (guidance_group) - # DISCUSS - here we have yet another way of finding a specific or group of - # an object. Would it make sense to standardise the project by only using - # either finders or where, or alteast the same syntax within the where statement. - # Also why is this a ? method... it dosent return a boolean - # Additionally, shouldnt this be a function of guidance group, not guidance? - # and finally, it should be a self.method, as it dosent care about the guidance it's acting on - templates = guidancegroups.where("guidance_group_id (?)", guidance_group.id).template - return templates - end + # ================= + # = Class methods = + # ================= - ## # Returns whether or not a given user can view a given guidance # we define guidances viewable to a user by those owned by a guidance group: # owned by the managing curation center # owned by a funder organisation # owned by an organisation, of which the user is a member # - # @param id [Integer] the integer id for a guidance - # @param user [User] a user object - # @return [Boolean] true if the specified user can view the specified guidance, false otherwise + # id - The Integer id for a guidance + # user - A User object + # + # Returns Boolean def self.can_view?(user, id) guidance = Guidance.find_by(id: id) viewable = false @@ -86,7 +91,8 @@ if guidance.guidance_group.org == user.org viewable = true end - # guidance groups are viewable if they are owned by the Managing Curation Center + # guidance groups are viewable if they are owned by the Managing + # Curation Center if Org.managing_orgs.include?(guidance.guidance_group.org) viewable = true end @@ -101,26 +107,47 @@ return viewable end - ## # Returns a list of all guidances which a specified user can view # we define guidances viewable to a user by those owned by a guidance group: # owned by the Managing Curation Center # owned by a funder organisation # owned by an organisation, of which the user is a member # - # @param user [User] a user object - # @return [Array<Guidance>] a list of all "viewable" guidances to a user + # user - A User object + # + # Returns Array def self.all_viewable(user) - managing_groups = Org.includes(guidance_groups: :guidances).managing_orgs.collect{|o| o.guidance_groups} + managing_groups = Org.includes(guidance_groups: :guidances) + .managing_orgs.collect{|o| o.guidance_groups} # find all groups owned by a Funder organisation - funder_groups = Org.includes(guidance_groups: :guidances).funder.collect{|org| org.guidance_groups} + funder_groups = Org.includes(guidance_groups: :guidances) + .funder.collect{|org| org.guidance_groups} # find all groups owned by any of the user's organisations organisation_groups = user.org.guidance_groups # find all guidances belonging to any of the viewable groups - all_viewable_groups = (managing_groups + funder_groups + organisation_groups).flatten - all_viewable_guidances = all_viewable_groups.collect{|group| group.guidances} + all_viewable_groups = (managing_groups + + funder_groups + + organisation_groups).flatten + all_viewable_guidances = all_viewable_groups.collect do |group| + group.guidances + end # pass the list of viewable guidances to the view return all_viewable_guidances.flatten end + + # Determine if a guidance is in a group which belongs to a specified + # organisation + # + # org_id - The Integer id for an organisation + # + # Returns Boolean + def in_group_belonging_to?(org_id) + unless guidance_group.nil? + if guidance_group.org.id == org_id + return true + end + end + return false + end end diff --git a/app/models/guidance_group.rb b/app/models/guidance_group.rb index b898ded..3a63430 100644 --- a/app/models/guidance_group.rb +++ b/app/models/guidance_group.rb @@ -1,21 +1,56 @@ +# Set of Guidances that pertain to a certain category of Users (e.g. Maths +# department, vs Biology department) +# +# == Schema Information +# +# Table name: guidance_groups +# +# id :integer not null, primary key +# name :string +# optional_subset :boolean default(FALSE), not null +# published :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# org_id :integer +# +# Indexes +# +# index_guidance_groups_on_org_id (org_id) +# +# Foreign Keys +# +# fk_rails_... (org_id => orgs.id) +# + class GuidanceGroup < ActiveRecord::Base include GlobalHelpers - ## - # Associations + include ValidationValues + include ValidationMessages + + # ================ + # = Associations = + # ================ + belongs_to :org + has_many :guidances, dependent: :destroy + has_and_belongs_to_many :plans, join_table: :plans_guidance_groups - # depricated but needed for migration "single_group_for_guidance" - # has_and_belongs_to_many :guidances, join_table: "guidance_in_group" + # =============== + # = Validations = + # =============== - ## - # Possibly needed for active_admin - # -relies on protected_attributes gem as syntax depricated in rails 4.2 - attr_accessible :org_id, :name, :optional_subset, :published, :org, :guidances, - :as => [:default, :admin] + validates :name, presence: { message: PRESENCE_MESSAGE }, + uniqueness: { message: UNIQUENESS_MESSAGE, scope: :org_id } - validates :name, :org, presence: {message: _("can't be blank")} + validates :org, presence: { message: PRESENCE_MESSAGE } + + validates :optional_subset, inclusion: { in: BOOLEAN_VALUES, + message: INCLUSION_MESSAGE } + + validates :published, inclusion: { in: BOOLEAN_VALUES, + message: INCLUSION_MESSAGE } # EVALUATE CLASS AND INSTANCE METHODS BELOW @@ -23,100 +58,71 @@ # What do they do? do they do it efficiently, and do we need them? # Retrieves every guidance group associated to an org - scope :by_org, -> (org) { - where(org_id: org.id) - } - scope :search, -> (term) { + scope :by_org, ->(org) { where(org_id: org.id) } + + scope :search, lambda { |term| search_pattern = "%#{term}%" where("name LIKE ?", search_pattern) } - ## - # Converts the current guidance group to a string containing the display name. - # If it's organisation has no other guidance groups, then the name is simply - # the name of the parent organisation, otherwise it returns the name of the - # organisation followed by the name of the guidance group. - # - # @return [String] the display name for the guidance group - def display_name - if org.guidance_groups.count > 1 - return "#{org.name}: #{name}" - else - return org.name - end - end + scope :published, -> { where(published: true) } - ## - # Returns the list of all guidance groups not coming from the given organisations - # - # @param excluded_orgs [Array<Organisation>] a list of organisations to exclude in the result - # @return [Array<GuidanceGroup>] a list of guidance groups - def self.guidance_groups_excluding(excluded_orgs) - excluded_org_ids = Array.new + # ================= + # = Class methods = + # ================= - if excluded_orgs.is_a?(Array) - excluded_orgs.each do |org| - excluded_org_ids << org.id - end - else - excluded_org_ids << excluded_orgs - end - - return_orgs = GuidanceGroup.where("org_id NOT IN (?)", excluded_org_ids) - return return_orgs - end - - ## - # Returns whether or not a given user can view a given guidance group + # Whether or not a given user can view a given guidance group # we define guidances viewable to a user by those owned by: # the managing curation center # a funder organisation # an organisation, of which the user is a member # - # @param id [Integer] the integer id for a guidance group - # @param user [User] a user object - # @return [Boolean] true if the specified user can view the specified guidance group, false otherwise + # id - The integer id for a guidance group + # user - A User object + # + # Returns Boolean def self.can_view?(user, guidance_group) viewable = false # groups are viewable if they are owned by any of the user's organisations - if guidance_group.org == user.org - viewable = true - end + viewable = true if guidance_group.org == user.org # groups are viewable if they are owned by the managing curation center Org.managing_orgs.each do |managing_group| - if guidance_group.org.id == managing_group.id - viewable = true - end + viewable = true if guidance_group.org.id == managing_group.id end # groups are viewable if they are owned by a funder - if guidance_group.org.funder? - viewable = true - end + viewable = true if guidance_group.org.funder? - return viewable + viewable end - ## - # Returns a list of all guidance groups which a specified user can view - # we define guidance groups viewable to a user by those owned by: - # the Managing Curation Center - # a funder organisation - # an organisation, of which the user is a member - # - # @param user [User] a user object - # @return [Array<GuidanceGroup>] a list of all "viewable" guidance groups to a user + + # A list of all guidance groups which a specified user can view + # we define guidance groups viewable to a user by those owned by: + # the Managing Curation Center + # a funder organisation + # an organisation, of which the user is a member + # + # user - A User object + # + # Returns Array def self.all_viewable(user) # first find all groups owned by the Managing Curation Center - managing_org_groups = Org.includes(guidance_groups: [guidances: :themes]).managing_orgs.collect{|org| org.guidance_groups} + managing_org_groups = Org.includes(guidance_groups: [guidances: :themes]) + .managing_orgs.collect(&:guidance_groups) # find all groups owned by a Funder organisation - funder_groups = Org.includes(:guidance_groups).funder.collect{|org| org.guidance_groups} + funder_groups = Org.includes(:guidance_groups) + .funder + .collect(&:guidance_groups) organisation_groups = [user.org.guidance_groups] - # pass this organisation guidance groups to the view with respond_with @all_viewable_groups - all_viewable_groups = managing_org_groups + funder_groups + organisation_groups + # pass this organisation guidance groups to the view with respond_with + # all_viewable_groups + all_viewable_groups = managing_org_groups + + funder_groups + + organisation_groups all_viewable_groups = all_viewable_groups.flatten.uniq - return all_viewable_groups + all_viewable_groups end end diff --git a/app/models/identifier_scheme.rb b/app/models/identifier_scheme.rb index d07aafc..9243b11 100644 --- a/app/models/identifier_scheme.rb +++ b/app/models/identifier_scheme.rb @@ -1,6 +1,37 @@ +# == Schema Information +# +# Table name: identifier_schemes +# +# id :integer not null, primary key +# active :boolean +# description :string +# logo_url :text +# name :string +# user_landing_url :text +# created_at :datetime +# updated_at :datetime +# + class IdentifierScheme < ActiveRecord::Base + include ValidationMessages + include ValidationValues + + ## + # The maximum length for a name + NAME_MAXIMUM_LENGTH = 30 + has_many :user_identifiers has_many :users, through: :user_identifiers - - validates :name, uniqueness: {message: _("must be unique")}, presence: {message: _("can't be blank")} -end \ No newline at end of file + + # =============== + # = Validations = + # =============== + + validates :name, uniqueness: { message: UNIQUENESS_MESSAGE }, + presence: { message: PRESENCE_MESSAGE }, + length: { maximum: NAME_MAXIMUM_LENGTH } + + validates :active, inclusion: { message: INCLUSION_MESSAGE, + in: BOOLEAN_VALUES } + +end diff --git a/app/models/language.rb b/app/models/language.rb index 1d86727..6ee7e9b 100644 --- a/app/models/language.rb +++ b/app/models/language.rb @@ -1,16 +1,60 @@ -class Language < ActiveRecord::Base - ## - # Associations - has_many :users - has_many :orgs - - ## - # Validations - # Cannot do FastGettext translations here because we constantize LANGUAGES in initializers/constants.rb - validates :abbreviation, presence: {message: "can't be blank"}, uniqueness: {message: "must be unique"} - - scope :sorted_by_abbreviation, -> { all.order(:abbreviation) } - scope :default, -> { where(default_language: true).first } - # Retrieves the id for a given abbreviation of a language - scope :id_for, -> (abbreviation) { where(abbreviation: abbreviation).pluck(:id).first } -end \ No newline at end of file +# == Schema Information +# +# Table name: languages +# +# id :integer not null, primary key +# abbreviation :string +# default_language :boolean +# description :string +# name :string +# + +class Language < ActiveRecord::Base + # frozen_string_literal: true + + include ValidationValues + + # ============= + # = Constants = + # ============= + + ABBREVIATION_MAXIMUM_LENGTH = 5 + + ABBREVIATION_FORMAT = /\A[a-z]{2}(\_[A-Z]{2})?\Z/ + + NAME_MAXIMUM_LENGTH = 20 + + # ================ + # = Associations = + # ================ + + has_many :users + + has_many :orgs + + + # =============== + # = Validations = + # =============== + + validates :name, presence: { message: "can't be blank" }, + length: { maximum: NAME_MAXIMUM_LENGTH } + + validates :abbreviation, presence: { message: "can't be blank" }, + uniqueness: { message: "must be unique" }, + length: { maximum: ABBREVIATION_MAXIMUM_LENGTH }, + format: { with: ABBREVIATION_FORMAT } + + validates :default_language, inclusion: { in: BOOLEAN_VALUES } + + # ========== + # = Scopes = + # ========== + + scope :sorted_by_abbreviation, -> { all.order(:abbreviation) } + scope :default, -> { where(default_language: true).first } + # Retrieves the id for a given abbreviation of a language + scope :id_for, -> (abbreviation) { + where(abbreviation: abbreviation).pluck(:id).first + } +end diff --git a/app/models/note.rb b/app/models/note.rb index 4341c2a..f63d5a6 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -1,14 +1,49 @@ +# == Schema Information +# +# Table name: notes +# +# id :integer not null, primary key +# archived :boolean default(FALSE), not null +# archived_by :integer +# text :text +# created_at :datetime +# updated_at :datetime +# answer_id :integer +# user_id :integer +# +# Indexes +# +# index_notes_on_answer_id (answer_id) +# +# Foreign Keys +# +# fk_rails_... (answer_id => answers.id) +# fk_rails_... (user_id => users.id) +# + class Note < ActiveRecord::Base - ## - # Associations + include ValidationMessages + include ValidationValues + + # ================ + # = Associations = + # ================ + belongs_to :answer + belongs_to :user - ## - # Possibly needed for active_admin - # -relies on protected_attributes gem as syntax depricated in rails 4.2 - attr_accessible :text, :user_id, :answer_id, :archived, :archived_by, - :answer, :user, :as => [:default, :admin] - - validates :text, :answer, :user, presence: {message: _("can't be blank")} + # =============== + # = Validations = + # =============== + + validates :text, presence: { message: PRESENCE_MESSAGE } + + validates :answer, presence: { message: PRESENCE_MESSAGE } + + validates :user, presence: { message: PRESENCE_MESSAGE } + + validates :archived, inclusion: { in: BOOLEAN_VALUES, + message: INCLUSION_MESSAGE } + end diff --git a/app/models/notification.rb b/app/models/notification.rb index c320f01..dd6a17d 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -1,11 +1,58 @@ +# == Schema Information +# +# Table name: notifications +# +# id :integer not null, primary key +# body :text +# dismissable :boolean +# expires_at :date +# level :integer +# notification_type :integer +# starts_at :date +# title :string +# created_at :datetime not null +# updated_at :datetime not null +# + class Notification < ActiveRecord::Base + include ValidationMessages + include ValidationValues + enum level: %i[info warning danger] enum notification_type: %i[global] - has_and_belongs_to_many :users, dependent: :destroy, join_table: 'notification_acknowledgements' + # ================ + # = Associations = + # ================ - validates :notification_type, :title, :level, :starts_at, :expires_at, :body, presence: true - validate :valid_dates + has_and_belongs_to_many :users, dependent: :destroy, + join_table: 'notification_acknowledgements' + + + # =============== + # = Validations = + # =============== + + validates :notification_type, presence: { message: PRESENCE_MESSAGE } + + validates :title, presence: { message: PRESENCE_MESSAGE } + + validates :level, presence: { message: PRESENCE_MESSAGE } + + validates :body, presence: { message: PRESENCE_MESSAGE } + + validates :dismissable, inclusion: { in: BOOLEAN_VALUES } + + validates :starts_at, presence: { message: PRESENCE_MESSAGE }, + after: { date: Date.today, on: :create } + + validates :expires_at, presence: { message: PRESENCE_MESSAGE }, + after: { date: Date.tomorrow, on: :create } + + + # ========== + # = Scopes = + # ========== scope :active, (lambda do where('starts_at <= :now and :now < expires_at', now: Time.now) @@ -22,19 +69,9 @@ # Has the Notification been acknowledged by the given user ? # If no user is given, currently logged in user (if any) is the default - # @return [Boolean] is the Notification acknowledged ? + # + # Returns Boolean def acknowledged?(user) - users.include?(user) if user.present? && dismissable? - end - - # Validate Notification dates - def valid_dates - return false if starts_at.blank? || expires_at.blank? - errors.add(:starts_at, _('Should be today or later')) if starts_at < Date.today - errors.add(:expires_at, _('Should be tomorrow or later')) if expires_at < Date.tomorrow - if starts_at > expires_at - errors.add(:starts_at, _('Should be before expiration date')) - errors.add(:expires_at, _('Should be after start date')) - end + dismissable? && user.present? && users.include?(user) end end diff --git a/app/models/org.rb b/app/models/org.rb index d095160..f97042c 100644 --- a/app/models/org.rb +++ b/app/models/org.rb @@ -1,46 +1,109 @@ +# == Schema Information +# +# Table name: orgs +# +# id :integer not null, primary key +# abbreviation :string +# contact_email :string +# contact_name :string +# feedback_email_msg :text +# feedback_email_subject :string +# feedback_enabled :boolean default(FALSE) +# is_other :boolean default(FALSE), not null +# links :text +# logo_name :string +# logo_uid :string +# name :string +# org_type :integer default(0), not null +# sort_name :string +# target_url :string +# created_at :datetime not null +# updated_at :datetime not null +# language_id :integer +# region_id :integer +# +# Foreign Keys +# +# fk_rails_... (language_id => languages.id) +# fk_rails_... (region_id => regions.id) +# + class Org < ActiveRecord::Base + include ValidationMessages + include ValidationValues include GlobalHelpers include FlagShihTzu extend Dragonfly::Model::Validations validates_with OrgLinksValidator + LOGO_FORMATS = %w[jpeg png gif jpg bmp].freeze + # Stores links as an JSON object: { org: [{"link":"www.example.com","text":"foo"}, ...] } # The links are validated against custom validator allocated at validators/template_links_validator.rb serialize :links, JSON - ## - # Associations -# belongs_to :organisation_type # depricated, but cannot be removed until migration run + + # ================ + # = Associations = + # ================ + belongs_to :language - has_many :guidance_groups + + belongs_to :region + + has_many :guidance_groups, dependent: :destroy + has_many :templates + has_many :users + has_many :annotations has_and_belongs_to_many :token_permission_types, join_table: "org_token_permissions", unique: true has_many :org_identifiers + has_many :identifier_schemes, through: :org_identifiers - ## - # Possibly needed for active_admin - # -relies on protected_attributes gem as syntax depricated in rails 4.2 - attr_accessible :abbreviation, :logo, :remove_logo, - :logo_file_name, :name, :links, - :organisation_type_id, :wayfless_entity, :parent_id, :sort_name, - :token_permission_type_ids, :language_id, :contact_email, :contact_name, - :language, :org_type, :region, :token_permission_types, - :guidance_group_ids, :is_other, :region_id, :logo_uid, :logo_name, - :feedback_enabled, :feedback_email_subject, :feedback_email_msg - ## - # Validators - validates :name, presence: {message: _("can't be blank")}, uniqueness: {message: _("must be unique")} + + # =============== + # = Validations = + # =============== + + validates :name, presence: { message: PRESENCE_MESSAGE }, + uniqueness: { message: UNIQUENESS_MESSAGE } + + validates :abbreviation, presence: { message: PRESENCE_MESSAGE } + + validates :is_other, inclusion: { in: BOOLEAN_VALUES, + message: INCLUSION_MESSAGE } + + validates :language, presence: { message: PRESENCE_MESSAGE } + + validates :contact_email, email: { allow_nil: true }, + presence: { message: PRESENCE_MESSAGE, + if: :feedback_enabled } + + validates :org_type, presence: { message: PRESENCE_MESSAGE } + + validates :feedback_enabled, inclusion: { in: BOOLEAN_VALUES, + message: INCLUSION_MESSAGE } + + validates :feedback_email_subject, presence: { message: PRESENCE_MESSAGE, + if: :feedback_enabled } + + validates :feedback_email_msg, presence: { message: PRESENCE_MESSAGE, + if: :feedback_enabled } + + validates_property :format, of: :logo, in: LOGO_FORMATS, + message: _("must be one of the following formats: jpeg, jpg, png, gif, bmp") + + validates_size_of :logo, maximum: 500.kilobytes, message: _("can't be larger than 500KB") + # allow validations for logo upload dragonfly_accessor :logo do after_assign :resize_image end - validates_property :format, of: :logo, in: ['jpeg', 'png', 'gif','jpg','bmp'], message: _("must be one of the following formats: jpeg, jpg, png, gif, bmp") - validates_size_of :logo, maximum: 500.kilobytes, message: _("can't be larger than 500KB") ## # Define Bit Field values @@ -54,7 +117,9 @@ column: 'org_type' # Predefined queries for retrieving the managain organisation and funders - scope :managing_orgs, -> { where(abbreviation: Rails.configuration.branding[:organisation][:abbreviation]) } + scope :managing_orgs, -> do + where(abbreviation: Branding.fetch(:organisation, :abbreviation)) + end scope :search, -> (term) { search_pattern = "%#{term}%" @@ -68,7 +133,9 @@ # What do they do? do they do it efficiently, and do we need them? # Determines the locale set for the organisation - # @return String or nil + # + # Returns String + # Returns nil def get_locale if !self.language.nil? return self.language.abbreviation @@ -77,16 +144,14 @@ end end -# TODO: Should these be hardcoded? Also, an Org can currently be multiple org_types at one time. -# For example you can do: funder = true; project = true; school = true -# Calling type in the above scenario returns "Funder" which is a bit misleading -# Is FlagShihTzu's Bit flag the appropriate structure here or should we use an enum? -# Tests are setup currently to work with this issue. - ## - # returns the name of the type of the organisation as a string - # defaults to none if no org type present + # TODO: Should these be hardcoded? Also, an Org can currently be multiple org_types at + # one time. For example you can do: funder = true; project = true; school = true # - # @return [String] + # Calling type in the above scenario returns "Funder" which is a bit misleading + # Is FlagShihTzu's Bit flag the appropriate structure here or should we use an enum? + # Tests are setup currently to work with this issue. + # + # Returns String def org_type_to_s ret = [] ret << "Institution" if self.institution? @@ -103,17 +168,17 @@ end ## - # returns the name of the organisation + # The name of the organisation # - # @return [String] + # Returns String def to_s name end ## - # returns the abbreviation for the organisation if it exists, or the name if not + # The abbreviation for the organisation if it exists, or the name if not # - # @return [String] name or abbreviation of the organisation + # Returns String def short_name if abbreviation.nil? then return name @@ -123,20 +188,11 @@ end ## - # returns all published templates belonging to the organisation + # All published templates belonging to the organisation # - # @return [Array<Template>] published templates - def published_templates - return templates.where("published = ?", true) - end - - def check_api_credentials - if token_permission_types.count == 0 - users.each do |user| - user.api_token = "" - user.save! - end - end + # Returns ActiveRecord::Relation + def published_templates + return templates.where("published = ?", true) end def org_admins @@ -154,20 +210,25 @@ end private - ## - # checks size of logo and resizes if necessary - # - def resize_image - unless logo.nil? - if logo.height != 100 - self.logo = logo.thumb('x100') # resize height and maintain aspect ratio - end + + ## + # checks size of logo and resizes if necessary + # + def resize_image + unless logo.nil? + if logo.height != 100 + self.logo = logo.thumb('x100') # resize height and maintain aspect ratio end end + end - # creates a dfefault Guidance Group on create on the Org - def create_guidance_group - GuidanceGroup.create(name: self.abbreviation? ? self.abbreviation : self.name , org_id: self.id) - end - + # creates a dfefault Guidance Group on create on the Org + def create_guidance_group + GuidanceGroup.create!({ + name: abbreviation? ? self.abbreviation : self.name , + org: self, + optional_subset: false, + published: false, + }) + end end diff --git a/app/models/org_identifier.rb b/app/models/org_identifier.rb index db61fd6..618ed2a 100644 --- a/app/models/org_identifier.rb +++ b/app/models/org_identifier.rb @@ -1,13 +1,51 @@ +# == Schema Information +# +# Table name: org_identifiers +# +# id :integer not null, primary key +# attrs :string +# identifier :string +# created_at :datetime +# updated_at :datetime +# identifier_scheme_id :integer +# org_id :integer +# +# Foreign Keys +# +# fk_rails_... (identifier_scheme_id => identifier_schemes.id) +# fk_rails_... (org_id => orgs.id) +# + class OrgIdentifier < ActiveRecord::Base + include ValidationMessages + + # ================ + # = Associations = + # ================ + belongs_to :org belongs_to :identifier_scheme - + + # =============== + # = Validations = + # =============== + # Should only be able to have one identifier per scheme! - validates_uniqueness_of :identifier_scheme, scope: :org - - validates :identifier, :org, :identifier_scheme, presence: {message: _("can't be blank")} - + validates :identifier_scheme_id, uniqueness: { scope: :org_id, + message: UNIQUENESS_MESSAGE } + + validates :identifier, presence: { message: PRESENCE_MESSAGE }, + uniqueness: { message: UNIQUENESS_MESSAGE } + + validates :org, presence: { message: PRESENCE_MESSAGE } + + validates :identifier_scheme, presence: { message: PRESENCE_MESSAGE } + + # =========================== + # = Public instance methods = + # =========================== + def attrs=(hash) write_attribute(:attrs, (hash.is_a?(Hash) ? hash.to_json.to_s : '{}')) end -end \ No newline at end of file +end diff --git a/app/models/perm.rb b/app/models/perm.rb index fcc429f..a2fdced 100644 --- a/app/models/perm.rb +++ b/app/models/perm.rb @@ -1,21 +1,63 @@ +# == Schema Information +# +# Table name: perms +# +# id :integer not null, primary key +# name :string +# created_at :datetime not null +# updated_at :datetime not null +# + class Perm < ActiveRecord::Base - ## - # Associations - has_and_belongs_to_many :users, :join_table => :users_perms + include ValidationMessages - ## - # Possibly needed for active_admin - # -relies on protected_attributes gem as syntax depricated in rails 4.2 - #attr_accessible :name, :as => [:default, :admin] + # ================ + # = Associations = + # ================ - validates :name, presence: {message: _("can't be blank")}, uniqueness: {message: _("must be unique")} - - scope :add_orgs, -> {Perm.find_by(name: 'add_organisations')} - scope :change_affiliation, -> {Perm.find_by(name: 'change_org_affiliation')} - scope :grant_permissions, -> {Perm.find_by(name: 'grant_permissions')} - scope :modify_templates, -> {Perm.find_by(name: 'modify_templates')} - scope :modify_guidance, -> {Perm.find_by(name: 'modify_guidance')} - scope :use_api, -> {Perm.find_by(name: 'use_api')} - scope :change_org_details, -> {Perm.find_by(name: 'change_org_details')} - scope :grant_api, -> {Perm.find_by(name: 'grant_api_to_orgs')} + has_and_belongs_to_many :users, join_table: :users_perms + + # =============== + # = Validations = + # =============== + + validates :name, presence: { message: PRESENCE_MESSAGE }, + uniqueness: { message: UNIQUENESS_MESSAGE } + + + # ================= + # = Class methods = + # ================= + + def self.add_orgs + Perm.find_by(name: 'add_organisations') + end + + def self.change_affiliation + Perm.find_by(name: 'change_org_affiliation') + end + + def self.grant_permissions + Perm.find_by(name: 'grant_permissions') + end + + def self.modify_templates + Perm.find_by(name: 'modify_templates') + end + + def self.modify_guidance + Perm.find_by(name: 'modify_guidance') + end + + def self.use_api + Perm.find_by(name: 'use_api') + end + + def self.change_org_details + Perm.find_by(name: 'change_org_details') + end + + def self.grant_api + Perm.find_by(name: 'grant_api_to_orgs') + end end diff --git a/app/models/phase.rb b/app/models/phase.rb index 2612f3e..58a466d 100644 --- a/app/models/phase.rb +++ b/app/models/phase.rb @@ -1,54 +1,105 @@ +# == Schema Information +# +# Table name: phases +# +# id :integer not null, primary key +# description :text +# modifiable :boolean +# number :integer +# title :string +# created_at :datetime +# updated_at :datetime +# template_id :integer +# +# Indexes +# +# index_phases_on_template_id (template_id) +# +# Foreign Keys +# +# fk_rails_... (template_id => templates.id) +# + # [+Project:+] DMPRoadmap # [+Description:+] This model describes informmation about the phase of a plan, it's title, order of display and which template it belongs to. # # [+Created:+] 03/09/2014 # [+Copyright:+] Digital Curation Centre and University of California Curation Center class Phase < ActiveRecord::Base + include ValidationMessages + include ValidationValues + include ActsAsSortable + ## # Sort order: Number ASC default_scope { order(number: :asc) } - ## - # Associations - belongs_to :template - has_many :sections, -> { order(:number => :asc) }, dependent: :destroy + # ================ + # = Associations = + # ================ + belongs_to :template, touch: true - ## - # Possibly needed for active_admin - # -relies on protected_attributes gem as syntax depricated in rails 4.2 - attr_accessible :description, :number, :title, :template_id, - :template, :sections, :modifiable, :as => [:default, :admin] + belongs_to :plan - validates :title, :number, :template, presence: {message: _("can't be blank")} + has_one :prefix_section, -> (phase) { + modifiable.where("number < ?", + phase.sections.not_modifiable.minimum(:number)) + }, class_name: "Section" + + has_many :sections, dependent: :destroy + + has_many :template_sections, -> { + not_modifiable + }, class_name: "Section" + + + has_many :suffix_sections, -> (phase) { + modifiable.where(<<~SQL, phase_id: phase.id, modifiable: false) + sections.number > (SELECT MAX(number) FROM sections + WHERE sections.modifiable = :modifiable + AND sections.phase_id = :phase_id) + SQL + }, class_name: "Section" + + + # =============== + # = Validations = + # =============== + + validates :title, presence: { message: PRESENCE_MESSAGE } + + validates :number, presence: { message: PRESENCE_MESSAGE }, + uniqueness: { message: UNIQUENESS_MESSAGE, + scope: :template_id } + + validates :template, presence: { message: PRESENCE_MESSAGE } + + validates :modifiable, inclusion: { in: BOOLEAN_VALUES, + message: INCLUSION_MESSAGE } + + # ========== + # = Scopes = + # ========== scope :titles, -> (template_id) { Phase.where(template_id: template_id).select(:id, :title) } - -# TODO: Remove after implementing new template versioning logic - # Callbacks - after_save do |phase| - # Updates the template.updated_at attribute whenever a phase has been created/updated - phase.template.touch if template.present? - end def deep_copy(**options) copy = self.dup copy.modifiable = options.fetch(:modifiable, self.modifiable) copy.template_id = options.fetch(:template_id, nil) copy.save!(validate:false) if options.fetch(:save, false) - options[:phase_id] = id + options[:phase_id] = copy.id self.sections.each{ |section| copy.sections << section.deep_copy(options) } return copy end -# TODO: Move this to Plan model as `num_answered_questions(phase=nil)` + # TODO: Move this to Plan model as `num_answered_questions(phase=nil)` # Returns the number of answered question for the phase. def num_answered_questions(plan) return 0 if plan.nil? - return sections.reduce(0) do |m, s| - m + s.num_answered_questions(plan) - end + sections.to_a.sum { |s| s.num_answered_questions(plan) } end # Returns the number of questions for a phase. Note, this method becomes useful @@ -58,6 +109,6 @@ self.sections.each do |s| n+= s.questions.size() end - return n + n end end diff --git a/app/models/plan.rb b/app/models/plan.rb index 0a5915f..434cbbe 100644 --- a/app/models/plan.rb +++ b/app/models/plan.rb @@ -1,63 +1,148 @@ +# frozen_string_literal: true + +# The central model object within this domain. Represents a Data Management +# Plan for a research project. +# +# == Schema Information +# +# Table name: plans +# +# id :integer not null, primary key +# complete :boolean default(FALSE) +# data_contact :string +# data_contact_email :string +# data_contact_phone :string +# description :text +# feedback_requested :boolean default(FALSE) +# funder_name :string +# grant_number :string +# identifier :string +# principal_investigator :string +# principal_investigator_email :string +# principal_investigator_identifier :string +# principal_investigator_phone :string +# title :string +# visibility :integer default(3), not null +# created_at :datetime +# updated_at :datetime +# template_id :integer +# +# Indexes +# +# index_plans_on_template_id (template_id) +# +# Foreign Keys +# +# fk_rails_... (template_id => templates.id) +# + class Plan < ActiveRecord::Base include ConditionalUserMailer include ExportablePlan - before_validation :set_creation_defaults + include ValidationMessages + include ValidationValues - ## - # Associations + # ============= + # = Constants = + # ============= + + + # Returns visibility message given a Symbol type visibility passed, otherwise + # nil + VISIBILITY_MESSAGE = { + organisationally_visible: _('organisational'), + publicly_visible: _('public'), + is_test: _('test'), + privately_visible: _('private') + } + + # ============== + # = Attributes = + # ============== + + # public is a Ruby keyword so using publicly + enum visibility: %i[organisationally_visible publicly_visible + is_test privately_visible] + + + alias_attribute :name, :title + + + # ================ + # = Associations = + # ================ + belongs_to :template + has_many :phases, through: :template + has_many :sections, through: :phases + has_many :questions, through: :sections + has_many :themes, through: :questions + + has_many :guidances, through: :themes + + has_many :guidance_group_options, -> { GuidanceGroup.published }, + through: :guidances, + source: :guidance_group, + class_name: "GuidanceGroup" + has_many :answers, dependent: :destroy + has_many :notes, through: :answers + has_many :roles, dependent: :destroy + has_many :users, through: :roles + has_and_belongs_to_many :guidance_groups, join_table: :plans_guidance_groups - accepts_nested_attributes_for :template has_many :exported_plans has_many :roles -# COMMENTED OUT THE DIRECT CONNECTION HERE TO Users to prevent assignment of users without an access_level specified (currently defaults to creator) -# has_many :users, through: :roles + # ===================== + # = Nested Attributes = + # ===================== - ## - # Possibly needed for active_admin - # -relies on protected_attributes gem as syntax depricated in rails 4.2 - attr_accessible :locked, :project_id, :version_id, :version, :plan_sections, - :exported_plans, :project, :title, :template, :grant_number, - :identifier, :principal_investigator, :principal_investigator_identifier, - :description, :data_contact, :funder_name, :visibility, :exported_plans, - :roles, :users, :org, :data_contact_email, :data_contact_phone, :feedback_requested, - :principal_investigator_email, :as => [:default, :admin] + accepts_nested_attributes_for :template + accepts_nested_attributes_for :roles - # public is a Ruby keyword so using publicly - enum visibility: [:organisationally_visible, :publicly_visible, :is_test, :privately_visible] - #TODO: work out why this messes up plan creation : - # briley: Removed reliance on :users, its really on :roles (shouldn't have a plan without at least a creator right?) It should be ok like this though now -# validates :template, :title, presence: true + # =============== + # = Validations = + # =============== - ## - # Constants - A4_PAGE_HEIGHT = 297 #(in mm) - A4_PAGE_WIDTH = 210 #(in mm) - ROUNDING = 5 #round estimate up to nearest 5% - FONT_HEIGHT_CONVERSION_FACTOR = 0.35278 #convert font point size to mm - FONT_WIDTH_HEIGHT_RATIO = 0.4 #Assume glyph width averages 2/5 the height + validates :title, presence: { message: PRESENCE_MESSAGE } - # Scope queries - # Note that in ActiveRecord::Enum the mappings are exposed through a class method with the pluralized attribute name (e.g visibilities rather than visibility) - scope :publicly_visible, -> { includes(:template).where(:visibility => visibilities[:publicly_visible]) } + validates :template, presence: { message: PRESENCE_MESSAGE } - # Retrieves any plan in which the user has an active role and it is not a reviewer - scope :active, -> (user) { - includes([:template, :roles]).where({ "roles.active": true, "roles.user_id": user.id }).where(Role.not_reviewer_condition) + validates :feedback_requested, inclusion: { in: BOOLEAN_VALUES } + + validates :complete, inclusion: { in: BOOLEAN_VALUES } + + + # ============= + # = Callbacks = + # ============= + + before_validation :set_creation_defaults + + + # ========== + # = Scopes = + # ========== + + # Retrieves any plan in which the user has an active role and it is not a + # reviewer + scope :active, lambda { |user| + includes(%i[template roles]) + .where("roles.active": true, "roles.user_id": user.id) + .where(Role.not_reviewer_condition) } # Retrieves any plan organisationally or publicly visible for a given org id @@ -70,111 +155,111 @@ .where(['NOT EXISTS (SELECT 1 FROM roles WHERE plan_id = plans.id AND user_id = ?)', user.id]) } - scope :search, -> (term) { + scope :search, lambda { |term| search_pattern = "%#{term}%" - joins(:template).where("plans.title LIKE ? OR templates.title LIKE ?", search_pattern, search_pattern) + joins(:template) + .where("plans.title LIKE ? OR templates.title LIKE ?", + search_pattern, search_pattern) } # Retrieves plan, template, org, phases, sections and questions - scope :overview, -> (id) { - Plan.includes(:phases, :sections, :questions, template: [ :org ]).find(id) + scope :overview, lambda { |id| + includes(:phases, :sections, :questions, template: [:org]).find(id) } + ## # Settings for the template has_settings :export, class_name: 'Settings::Template' do |s| s.key :export, defaults: Settings::Template::DEFAULT_SETTINGS end - alias_method :super_settings, :settings + alias super_settings settings - ## - # Proxy through to the template settings (or defaults if this plan doesn't have - # an associated template) if there are no settings stored for this plan. - # `key` is required by rails-settings, so it's required here, too. - # - # @param key [Key] a key required by rails - # @return [Settings] settings for this plan's template - def settings(key) - self_settings = self.super_settings(key) - return self_settings if self_settings.value? - self.template.settings(key) unless self.template.nil? + # ================= + # = Class methods = + # ================= + + # Pre-fetched a plan phase together with its sections and questions + # associated. It also pre-fetches the answers and notes associated to the plan + def self.load_for_phase(plan_id, phase_id) + # Preserves the default order defined in the model relationships + plan = Plan.joins(template: { phases: { sections: :questions } }) + .preload(template: { phases: { sections: :questions } }) + .where(id: plan_id, phases: { id: phase_id }) + .merge(Plan.includes(answers: :notes)).first + phase = plan.template.phases.find { |p| p.id == phase_id.to_i } + + [plan, phase] end - ## - # returns the most recent answer to the given question id - # optionally can create an answer if none exists + # deep copy the given plan and all of it's associations # - # @param qid [Integer] the id for the question to find the answer for - # @param create_if_missing [Boolean] if true, will genereate a default answer to the question - # @return [Answer,nil] the most recent answer to the question, or a new question with default value, or nil + # plan - Plan to be deep copied + # + # Returns Plan + def self.deep_copy(plan) + plan_copy = plan.dup + plan_copy.title = "Copy of " + plan.title + plan_copy.save! + plan.answers.each do |answer| + answer_copy = Answer.deep_copy(answer) + answer_copy.plan_id = plan_copy.id + answer_copy.save! + end + plan.guidance_groups.each do |guidance_group| + plan_copy.guidance_groups << guidance_group if guidance_group.present? + end + plan_copy + end + + # =========================== + # = Public instance methods = + # =========================== + + ## + # Proxy through to the template settings (or defaults if this plan doesn't + # have an associated template) if there are no settings stored for this plan. + # + # TODO: Update this comment below. AFAIK `key` has nothing to do with Rails. + # key - Is required by rails-settings, so it's required here, too. + # + # Returns Hash + def settings(key) + self_settings = super_settings(key) + return self_settings if self_settings.value? + template&.settings(key) + end + + # The most recent answer to the given question id optionally can create an answer if + # none exists. + # + # qid - The id for the question to find the answer for + # create_if_missing - If true, will genereate a default answer + # to the question (defaults: true). + # + # Returns Answer + # Returns nil def answer(qid, create_if_missing = true) - answer = answers.where(:question_id => qid).order("created_at DESC").first + answer = answers.where(question_id: qid).order("created_at DESC").first question = Question.find(qid) - if answer.nil? && create_if_missing then - answer = Answer.new - answer.plan_id = id + if answer.nil? && create_if_missing + answer = Answer.new + answer.plan_id = id answer.question_id = qid - answer.text = question.default_value - default_options = Array.new + answer.text = question.default_value + default_options = [] question.question_options.each do |option| - if option.is_default - default_options << option - end + default_options << option if option.is_default end answer.question_options = default_options end - return answer + answer end -# TODO: This just retrieves all of the guidance associated with the themes within the template -# so why are we transferring it here to the plan? - ## - # returns all of the sections for this version of the plan, and for the project's organisation - # - # @return [Array<Section>,nil] either a list of sections, or nil if none were found - def set_possible_guidance_groups - # find all the themes in this plan - # and get the guidance groups they belong to - ggroups = [] - self.template.phases.each do |phase| - phase.sections.each do |section| - section.questions.each do |question| - question.themes.each do |theme| - theme.guidances.each do |guidance| - ggroups << guidance.guidance_group if guidance.guidance_group.published - # only show published guidance groups - end - end - end - end - end + alias get_guidance_group_options guidance_group_options - self.guidance_groups = ggroups.uniq - end - - ## - # returns all of the possible guidance groups for the plan (all options to - # be selected by the user to display) - # - # @return Array<Guidance> - def get_guidance_group_options - # find all the themes in this plan - # and get the guidance groups they belong to - ggroups = [] - Template.includes(phases: [sections: [questions: [themes: [guidances: [guidance_group: :org]]]]]).find(self.template_id).phases.each do |phase| - phase.sections.each do |section| - section.questions.each do |question| - question.themes.each do |theme| - theme.guidances.each do |guidance| - ggroups << guidance.guidance_group if guidance.guidance_group.published - # only show published guidance groups - end - end - end - end - end - return ggroups.uniq - end + deprecate :get_guidance_group_options, + deprecator: Cleanup::Deprecators::GetDeprecator.new ## # Sets up the plan for feedback: @@ -186,26 +271,22 @@ begin val = Role.access_values_for(:reviewer, :commenter).min self.feedback_requested = true - # Share the plan with each org admin as the reviewer role admins = user.org.org_admins admins.each do |admin| - self.roles << Role.new(user: admin, access: val) + roles << Role.new(user: admin, access: val) end - - if self.save! - # Send an email confirmation to the owners and co-owners - owners = User.joins(:roles).where('roles.plan_id =? AND roles.access IN (?)', self.id, Role.access_values_for(:administrator)) - deliver_if(recipients: owners, key: 'users.feedback_requested') do |r| - UserMailer.feedback_confirmation(r, self, user).deliver_now - end + if save! # Send an email to the org-admin contact if user.org.contact_email.present? - contact = User.new(email: user.org.contact_email, firstname: user.org.contact_name) + contact = User.new(email: user.org.contact_email, + firstname: user.org.contact_name) UserMailer.feedback_notification(contact, self, user).deliver_now end true else + puts "save was false" + false end rescue Exception => e @@ -216,10 +297,9 @@ end ## - # Finalizes the feedback for the plan: - # emails confirmation messages to owners - # sets flag on plans.feedback_requested to false - # removes org admins from the 'reviewer' Role for the Plan + # Finalizes the feedback for the plan: Emails confirmation messages to owners + # sets flag on plans.feedback_requested to false removes org admins from the + # 'reviewer' Role for the Plan. def complete_feedback(org_admin) Plan.transaction do begin @@ -227,11 +307,14 @@ # Remove the org admins reviewer role from the plan vals = Role.access_values_for(:reviewer) - self.roles.delete(Role.where(plan: self, access: vals)) + roles.delete(Role.where(plan: self, access: vals)) - if self.save! + if save! # Send an email confirmation to the owners and co-owners - owners = User.joins(:roles).where('roles.plan_id =? AND roles.access IN (?)', self.id, Role.access_values_for(:administrator)) + owners = User.joins(:roles) + .where('roles.plan_id =? AND roles.access IN (?)', + id, Role.access_values_for(:administrator)) + deliver_if(recipients: owners, key: 'users.feedback_provided') do |r| UserMailer.feedback_complete(r, self, org_admin).deliver_now end @@ -239,112 +322,30 @@ else false end - rescue Exception => e + rescue ArgumentError => e Rails.logger.error e false end end end - # Returns all of the plan's available guidance by question as a hash for use on the write plan page - # { - # QUESTION: { - # GUIDANCE_GROUP: { - # THEME: [GUIDANCE, GUIDANCE], - # THEME: [GUIDANCE] - # } - # } - # } - def guidance_by_question_as_hash - # Get all of the selected guidance groups for the plan - guidance_groups_ids = self.guidance_groups.collect(&:id) - guidance_groups = GuidanceGroup.joins(:org).where("guidance_groups.published = ? AND guidance_groups.id IN (?)", - true, guidance_groups_ids) - - # Gather all of the Themes used in the plan as a hash - # { - # QUESTION: [THEME, THEME], - # QUESTION: [THEME] - # } - question_themes = {} - themes_used = [] - self.questions.joins(:themes).pluck('questions.id', 'themes.title').each do |qt| - themes_used << qt[1] unless themes_used.include?(qt[1]) - question_themes[qt[0]] = [] unless question_themes[qt[0]].present? - question_themes[qt[0]] << qt[1] unless question_themes[qt[0]].include?(qt[1]) - end - - # Gather all of the Guidance available for the themes used in the plan as a hash - # { - # THEME: { - # GUIDANCE_GROUP: [GUIDANCE, GUIDANCE], - # GUIDANCE_GROUP: [GUIDANCE] - # } - # } - theme_guidance = {} - GuidanceGroup.includes(guidances: :themes).joins(:guidances). - where('guidance_groups.published = ? AND guidances.published = ? AND themes.title IN (?) AND guidance_groups.id IN (?)', true, true, themes_used, guidance_groups.collect(&:id)). - pluck('guidance_groups.name', 'themes.title', 'guidances.text').each do |tg| - - theme_guidance[tg[1]] = {} unless theme_guidance[tg[1]].present? - theme_guidance[tg[1]][tg[0]] = [] unless theme_guidance[tg[1]][tg[0]].present? - theme_guidance[tg[1]][tg[0]] << tg[2] unless theme_guidance[tg[1]][tg[0]].include?(tg[2]) - end - - # Generate a hash for the view that contains all of a question guidance - # { - # QUESTION: { - # GUIDANCE_GROUP: { - # THEME: [GUIDANCE, GUIDANCE], - # THEME: [GUIDANCE] - # } - # } - # } - question_guidance = {} - question_themes.keys.each do |question| - ggs = {} - # Gather all of the guidance groups applicable to the themes assigned to the question - groups = [] - question_themes[question].each do |theme| - groups << theme_guidance[theme].keys if theme_guidance[theme].present? - end - - # Loop through all of the applicable guidance groups and collect their themed guidance - groups.flatten.uniq.each do |guidance_group| - guidances_by_theme = {} - - # Collect all of the guidances for each theme used by the question - question_themes[question].each do |theme| - if theme_guidance[theme].present? && theme_guidance[theme][guidance_group].present? - guidances_by_theme[theme] = [] unless guidances_by_theme[theme].present? - guidances_by_theme[theme] = theme_guidance[theme][guidance_group] - end - end - - ggs[guidance_group] = guidances_by_theme unless ggs[guidance_group] - end - - question_guidance[question] = ggs - end - - question_guidance - end - ## # determines if the plan is editable by the specified user # - # @param user_id [Integer] the id for a user - # @return [Boolean] true if user can edit the plan + # user_id - The id for a user + # + # Returns Boolean def editable_by?(user_id) user_id = user_id.id if user_id.is_a?(User) - has_role(user_id, :editor) + role?(user_id, :editor) end ## # determines if the plan is readable by the specified user # - # @param user_id [Integer] the id for a user - # @return [Boolean] true if the user can read the plan + # user_id - The Integer id for a user + # + # Returns Boolean def readable_by?(user_id) user = user_id.is_a?(User) ? user_id : User.find(user_id) owner_orgs = owner_and_coowners.collect(&:org) @@ -352,577 +353,217 @@ :org_admins_read_all) # Super Admins can view plans read-only - if user.can_super_admin? - return true + return true if user.can_super_admin? + # Org Admins can view their Org's plans if system permission allows - elsif user.can_org_admin? && owner_orgs.include?(user.org) && sys_permission - return true + return true if user.can_org_admin? && owner_orgs.include?(user.org) && + sys_permission + # ...otherwise the user must have the commenter role. - elsif has_role(user.id, :commenter) - return true - else - return false - end + return true if role?(user.id, :commenter) + + # Else + false end - ## - # determines if the plan is readable by the specified user + # determines if the plan is readable by the specified user. # - # @param user_id [Integer] the id for a user - # @return [Boolean] true if the user can read the plan + # user_id - The Integer id for a user + # + # Returns Boolean def commentable_by?(user_id) user_id = user_id.id if user_id.is_a?(User) - has_role(user_id, :commenter) + role?(user_id, :commenter) end - ## # determines if the plan is administerable by the specified user # - # @param user_id [Integer] the id for the user - # @return [Boolean] true if the user can administer the plan + # user_id - The Integer id for the user + # + # Returns Boolean def administerable_by?(user_id) user_id = user_id.id if user_id.is_a?(User) - has_role(user_id, :administrator) + role?(user_id, :administrator) end - ## - # determines if the plan is owned by the specified user - # - # @param user_id [Integer] the id for the user - # @return [Boolean] true if the user can administer the plan - def owned_by?(user_id) - user_id = user_id.id if user_id.is_a?(User) - has_role(user_id, :creator) - end - - ## # determines if the plan is reviewable by the specified user # - # @param user_id [Integer] the id for the user - # @return [Boolean] true if the user can administer the plan + # user_id - The Integer id for the user + # + # Returns Boolean def reviewable_by?(user_id) user_id = user_id.id if user_id.is_a?(User) - has_role(user_id, :reviewer) + role?(user_id, :reviewer) end - ## - # determines whether or not the specified user has any rol on the plan + # Assigns the passed user_id to the creater_role for the project gives the + # user rights to read, edit, administrate, and defines them as creator # - # @param user_id [Integer] the id for the user - # @return [Boolean] true if the user has any rol - def any_role?(user) - user_id = user.id if user.is_a?(User) - !self.roles.index{ |rol| rol.user_id == user_id }.nil? - end - - ## - # defines and returns the status of the plan - # status consists of a hash of the num_questions, num_answers, sections, questions, and spaced used. - # For each section, it contains the id's of each of the questions - # for each question, it contains the answer_id, answer_created_by, answer_text, answer_options_id, aand answered_by + # user_id - The Integer user to be given priveleges' id # - # @return [Status] - - def status - status = { - "num_questions" => 0, - "num_answers" => 0, - "sections" => {}, - "questions" => {}, - "space_used" => 0 # percentage of available space in pdf used - } - - space_used = height_of_text(self.title, 2, 2) - - section_ids = sections.map {|s| s.id} - - # we retrieve this is 2 joins: - # 1. sections and questions - # 2. questions and answers - # why? because Rails 4 doesn't have any sensible left outer join. - # when we change to RAILS 5 it is meant to have so this can be fixed then - - records = Section.joins(questions: :question_format) - .select('sections.id as sectionid, - sections.title as stitle, - questions.id as questionid, - questions.text as questiontext, - question_formats.title as qformat') - .where("sections.id in (?) ", section_ids) - .to_a - - # extract question ids to get answers - question_ids = records.map {|r| r.questionid}.uniq - status["num_questions"] = question_ids.count - - arecords = Question.joins(answers: :user) - .select('questions.id as questionid, - answers.id as answerid, - answers.plan_id as plan_id, - answers.text as answertext, - answers.updated_at as updated, - users.email as username') - .where("questions.id in (?) and answers.plan_id = ?",question_ids, self.id) - .to_a - - # we want answerids to extract options later - answer_ids = arecords.map {|r| r.answerid}.uniq - status["num_answers"] = answer_ids.count - - # create map from questionid to answer structure - qa_map = {} - arecords.each do |rec| - qa_map[rec.questionid] = { - plan: rec.plan_id, - id: rec.answerid, - text: rec.answertext, - updated: rec.updated, - user: rec.username - } - end - - - # build main status structure - records.each do |rec| - sid = rec.sectionid - stitle = rec.stitle - qid = rec.questionid - qtext = rec.questiontext - format = rec.qformat - - answer = nil - if qa_map.has_key?(qid) - answer = qa_map[qid] - end - - aid = answer.nil? ? nil : answer[:id] - atext = answer.nil? ? nil : answer[:text] - updated = answer.nil? ? nil : answer[:updated] - uname = answer.nil? ? nil : answer[:user] - - space_used += height_of_text(stitle, 1, 1) - - shash = status["sections"] - if !shash.has_key?(sid) - shash[sid] = {} - shash[sid]["num_questions"] = 0 - shash[sid]["num_answers"] = 0 - shash[sid]["questions"] = Array.new - end - - shash[sid]["questions"] << qid - shash[sid]["num_questions"] += 1 - - space_used += height_of_text(qtext) unless qtext == stitle - if atext.present? - space_used += height_of_text(atext) - else - space_used += height_of_text(_('Question not answered.')) - end - - if answer.present? then - shash[sid]["num_answers"] += 1 - end - - status["questions"][qid] = { - "format" => format, - "answer_id" => aid, - "answer_updated_at" => updated.to_i, - "answer_text" => atext, - "answered_by" => uname - } - - end - - records = Answer.joins(:question_options).select('answers.id as answerid, question_options.id as optid').where(id: answer_ids).to_a - opt_hash = {} - records.each do |rec| - aid = rec.answerid - optid = rec.optid - if !opt_hash.has_key?(aid) - opt_hash[aid] = Array.new - end - opt_hash[aid] << optid - end - - status["questions"].each_key do |questionid| - answerid = status["questions"][questionid]["answer_id"] - status["questions"][questionid]["answer_option_ids"] = opt_hash[answerid] - end - - status['space_used'] = estimate_space_used(space_used) - - return status - end - - - ## - # assigns the passed user_id to the creater_role for the project - # gives the user rights to read, edit, administrate, and defines them as creator - # - # @param user_id [Integer] the user to be given priveleges' id def assign_creator(user_id) user_id = user_id.id if user_id.is_a?(User) add_user(user_id, true, true, true) end - ## - # returns the funder id for the plan - # - # @return [Integer, nil] the id for the funder - def funder_id - if self.template.nil? then - return nil - end - return self.template.org - end - - ## - # returns the funder organisation for the project or nil if none is specified - # - # @return [Organisation, nil] the funder for project, or nil if none exists - def funder - template = self.template - if template.nil? then - return nil - end - - if template.customization_of - return template.customization_of.org - else - return template.org - end - end - - ## - # assigns the passed user_id as an editor for the project - # gives the user rights to read and edit - # - # @param user_id [Integer] the user to be given priveleges' id - def assign_editor(user_id) - add_user(user_id, true) - end - - ## - # assigns the passed user_id as a reader for the project - # gives the user rights to read - # - # @param user_id [Integer] the user to be given priveleges' id - def assign_reader(user_id) - add_user(user_id) - end - - ## - # assigns the passed user_id as an administrator for the project - # gives the user rights to read, adit, and administrate the project - # - # @param user_id [Integer] the user to be given priveleges' id - def assign_administrator(user_id) - add_user(user_id, true, true) - end - - ## # the datetime for the latest update of this plan # - # @return [DateTime] the time of latest update + # Returns DateTime def latest_update - latest_update = updated_at - phases.each do |phase| - if phase.updated_at > latest_update then - latest_update = phase.updated_at - end - end - return latest_update + (phases.pluck(:updated_at) + [updated_at]).max end - # Getters to match 'My plans' columns - - ## - # the title of the project - # - # @return [String] the title of the project - def name - self.title - end - - ## # the owner of the project # - # @return [User] the creater of the project + # Returns User + # Returns nil def owner vals = Role.access_values_for(:creator) - User.joins(:roles).where('roles.plan_id = ? AND roles.access IN (?)', self.id, vals).first + User.joins(:roles) + .where('roles.plan_id = ? AND roles.access IN (?)', id, vals).first end ## - # returns the shared roles of a plan, excluding the creator - def shared - role_values = Role.where(plan: self).where(Role.not_creator_condition).any? + # TODO: Rewrite this description + # + # Returns Boolean + def shared? + roles.where(Role.not_creator_condition).any? end - ## + alias shared shared? + + deprecate :shared, deprecator: Cleanup::Deprecators::PredicateDeprecator.new + # the owner and co-owners of the project # - # @return [Users] + # Returns ActiveRecord::Relation def owner_and_coowners - vals = Role.access_values_for(:creator).concat(Role.access_values_for(:administrator)) - User.joins(:roles).where("roles.plan_id = ? AND roles.access IN (?)", self.id, vals) + vals = Role.access_values_for(:creator) + .concat(Role.access_values_for(:administrator)) + User.joins(:roles) + .where(roles: { plan_id: id, access: vals }) end - ## - # the time the project was last updated, formatted as a date + # The number of answered questions from the entire plan # - # @return [Date] last update as a date - def last_edited - self.latest_update.to_date - end - - # Returns the number of answered questions from the entire plan + # Returns Integer def num_answered_questions - return Answer.where(id: answers.map(&:id)).includes({ question: :question_format }, :question_options).reduce(0) do |m, a| - if a.is_valid? - m+=1 - end - m - end + Answer.where(id: answers.map(&:id)) + .includes(:question_options, { question: :question_format }) + .to_a + .sum { |answer| answer.is_valid? ? 1 : 0 } end - # Returns a section given its id or nil if does not exist for the current plan - def get_section(section_id) - self.sections.find { |s| s.id == section_id } - end - - # Returns the number of questions for a plan. + # The number of questions for a plan. + # + # Returns Integer def num_questions - return sections.includes(:questions).joins(:questions).reduce(0){ |m, s| m + s.questions.length } - end - # the following two methods are for eager loading. One gets used for the plan/show - # page and the oter for the plan/edit. The difference is just that one pulls in more than - # the other. - # TODO: revisit this and work out for sure that maintaining the difference is worthwhile. - # it may not be. Also make sure nether is doing more thanit needs to. - # - def self.eager_load(id) - Plan.includes( - [{template: [ - {phases: {sections: {questions: :answers}}} - ]}, - {plans_guidance_groups: {guidance_group: :guidances}} - ]).find(id) - end - - # Pre-fetched a plan phase together with its sections and questions associated. It also pre-fetches the answers and notes associated to the plan - def self.load_for_phase(id, phase_id) - plan = Plan - .joins(template: { phases: { sections: :questions }}) - .preload(template: { phases: { sections: :questions }}) # Preserves the default order defined in the model relationships - .where("plans.id = :id AND phases.id = :phase_id", { id: id, phase_id: phase_id }) - .merge(Plan.includes(answers: :notes))[0] - phase = plan.template.phases.find {|p| p.id==phase_id.to_i } - - return plan, phase - end - - # deep copy the given plan and all of it's associations - # - # @params [Plan] plan to be deep copied - # @return [Plan] saved copied plan - def self.deep_copy(plan) - plan_copy = plan.dup - plan_copy.title = "Copy of " + plan.title - plan_copy.save! - plan.answers.each do |answer| - answer_copy = Answer.deep_copy(answer) - answer_copy.plan_id = plan_copy.id - answer_copy.save! - end - plan.guidance_groups.each do |guidance_group| - if guidance_group.present? - plan_copy.guidance_groups << GuidanceGroup.where(id: guidance_group.id).first - end - end - return plan_copy - end - - # Returns visibility message given a Symbol type visibility passed, otherwise nil - def self.visibility_message(type) - message = { - :organisationally_visible => _('organisational'), - :publicly_visible => _('public'), - :is_test => _('test'), - :privately_visible => _('private') - } - message[type] + questions.count end # Determines whether or not visibility changes are permitted according to the - # percentage of the plan answered in respect to a threshold defined at application.config + # percentage of the plan answered in respect to a threshold defined at + # application.config + # + # Returns Boolean def visibility_allowed? - value=(self.num_answered_questions().to_f/self.num_questions()*100).round(2) - !self.is_test? && value >= Rails.application.config.default_plan_percentage_answered + value=(num_answered_questions.to_f/num_questions*100).round(2) + !is_test? && value >= Rails.application + .config + .default_plan_percentage_answered end # Determines whether or not a question (given its id) exists for the self plan + # + # Returns Boolean def question_exists?(question_id) - Plan.joins(:questions).exists?(id: self.id, "questions.id": question_id) + Plan.joins(:questions).exists?(id: id, "questions.id": question_id) end - # Checks whether or not the number of questions matches the number of valid answers + # Checks whether or not the number of questions matches the number of valid + # answers + # + # Returns Boolean def no_questions_matches_no_answers? num_questions = question_ids.length - pre_fetched_answers = Answer - .includes({ question: :question_format }, :question_options) - .where(id: answer_ids) + pre_fetched_answers = Answer.includes(:question_options, + question: :question_format) + .where(id: answer_ids) num_answers = pre_fetched_answers.reduce(0) do |m, a| - if a.is_valid? - m+=1 - end + m+=1 if a.is_valid? m end - return num_questions == num_answers + num_questions == num_answers end private # Returns whether or not the user has the specified role for the plan - def has_role(user_id, role_as_sym) + def role?(user_id, role_as_sym) if user_id.is_a?(Integer) && role_as_sym.is_a?(Symbol) vals = Role.access_values_for(role_as_sym) - self.roles.where(user_id: user_id, access: vals, active: true).first.present? + roles.where(user_id: user_id, access: vals, active: true).any? else false end end - ## - # adds a user to the project - # if no flags are specified, the user is given read privleges - # - # @param user_id [Integer] the user to be given privleges - # @param is_editor [Boolean] whether or not the user can edit the project - # @param is_administrator [Boolean] whether or not the user can administrate the project - # @param is_creator [Boolean] wheter or not the user created the project - # @return [Array<ProjectGroup>] - # - # TODO: change this to specifying uniqueness of user/plan association and handle - # that way - # - def add_user(user_id, is_editor = false, is_administrator = false, is_creator = false) - Role.where(plan_id: self.id, user_id: user_id).each do |r| - r.destroy - end + alias has_role role? - role = Role.new + deprecate :has_role, deprecator: Cleanup::Deprecators::PredicateDeprecator.new + + # Adds a user to the project if no flags are specified, the user is given read privleges + # TODO: change this to specifying uniqueness of user/plan association and + # handle that way. + # + # + # user_id - The Integer user ID to be given privleges + # is_editor - Whether or not the user can edit the project (defaults: false) + # is_administrator - Whether or not the user can administrate the project + # (defaults: false) + # is_creator - Wheter or not the user created the project (defaults: false) + # + # Returns Boolean + # + def add_user(user_id, is_editor = false, + is_administrator = false, + is_creator = false) + + Role.where(plan_id: id, user_id: user_id).find_each(&:destroy) + + role = Role.new role.user_id = user_id role.plan_id = id # if you get assigned a role you can comment - role.commenter= true + role.commenter = true # the rest of the roles are inclusing so creator => administrator => editor if is_creator - role.creator = true + role.creator = true role.administrator = true - role.editor = true + role.editor = true end if is_administrator role.administrator = true - role.editor = true + role.editor = true end - if is_editor - role.editor = true - end + role.editor = true if is_editor role.save - - # This is necessary because we're creating the associated record but not assigning it - # to roles. Auto-saving like this may be confusing when coding upstream in a controller, - # view or api. Should probably change this to: - # self.roles << role - # and then let the save be called manually via: - # plan.save! - #self.reload - end - - ## - # creates a plan for each phase in the template associated with this project - # unless the phase is unpublished, it creates a new plan, and a new version of the plan and adds them to the project's plans - # - # @return [Array<Plan>] - def create_plans - self.template.phases.each do |phase| - latest_published_version = phase.latest_published_version - unless latest_published_version.nil? - new_plan = Plan.new - new_plan.version = latest_published_version - plans << new_plan - end - end - end - - - - ## - # Based on the height of the text gathered so far and the available vertical - # space of the pdf, estimate a percentage of how much space has been used. - # This is highly dependent on the layout in the pdf. A more accurate approach - # would be to render the pdf and check how much space had been used, but that - # could be very slow. - # NOTE: This is only an estimate, rounded up to the nearest 5%; it is intended - # for guidance when editing plan data, not to be 100% accurate. - # - # @param used_height [Integer] an estimate of the height used so far - # @return [Integer] the estimate of space used of an A4 portrain - def estimate_space_used(used_height) - @formatting ||= self.settings(:export).formatting - - return 0 unless @formatting[:font_size] > 0 - - margin_height = @formatting[:margin][:top].to_i + @formatting[:margin][:bottom].to_i - page_height = A4_PAGE_HEIGHT - margin_height # 297mm for A4 portrait - available_height = page_height * self.template.settings(:export).max_pages - - percentage = (used_height / available_height) * 100 - (percentage / ROUNDING).ceil * ROUNDING # round up to nearest five - end - - ## - # Take a guess at the vertical height (in mm) of the given text based on the - # font-size and left/right margins stored in the plan's settings. - # This assumes a fixed-width for each glyph, which is obviously - # incorrect for the font-face choices available; the idea is that - # they'll hopefully average out to that in the long-run. - # Allows for hinting different font sizes (offset from base via font_size_inc) - # and vertical margins (i.e. for heading text) - # - # @param text [String] the text to estimate size of - # @param font_size_inc [Integer] the size of the font of the text, defaults to 0 - # @param vertical_margin [Integer] the top margin above the text, defaults to 0 - def height_of_text(text, font_size_inc = 0, vertical_margin = 0) - @formatting ||= self.settings(:export).formatting - @margin_width ||= @formatting[:margin][:left].to_i + @formatting[:margin][:right].to_i - @base_font_size ||= @formatting[:font_size] - - return 0 unless @base_font_size > 0 - - font_height = FONT_HEIGHT_CONVERSION_FACTOR * (@base_font_size + font_size_inc) - font_width = font_height * FONT_WIDTH_HEIGHT_RATIO # Assume glyph width averages at 2/5s the height - leading = font_height / 2 - - chars_in_line = (A4_PAGE_WIDTH - @margin_width) / font_width # 210mm for A4 portrait - num_lines = (text.length / chars_in_line).ceil - - (num_lines * font_height) + vertical_margin + leading end # Initialize the title for new templates - # -------------------------------------------------------- + # + # Returns nil + # Returns String def set_creation_defaults - # Only run this before_validation because rails fires this before save/create - if self.id.nil? - self.title = "My plan (#{self.template.title})" if self.title.nil? && !self.template.nil? - end + # Only run this before_validation because rails fires this before + # save/create + return if id? + self.title = "My plan (#{template.title})" if title.nil? && !template.nil? end end diff --git a/app/models/pref.rb b/app/models/pref.rb index 02e1a59..c3c4707 100644 --- a/app/models/pref.rb +++ b/app/models/pref.rb @@ -1,19 +1,38 @@ +# == Schema Information +# +# Table name: prefs +# +# id :integer not null, primary key +# settings :text +# user_id :integer +# + class Pref < ActiveRecord::Base + include ValidationMessages + ## # Serialize prefs to JSON # The settings object only stores deviations from the default serialize :settings, JSON - ## - # Associations + # ================ + # = Associations = + # ================ belongs_to :user - ## - # Returns the hash generated from default preferences + # =============== + # = Validations = + # =============== + + validates :user, presence: { message: PRESENCE_MESSAGE } + + validates :settings, presence: { message: PRESENCE_MESSAGE } + + # The default preferences # - # @return [JSON] preferences hash + # Returns Hash def self.default_settings - return Rails.configuration.branding[:preferences] + Branding.fetch(:preferences) end -end \ No newline at end of file +end diff --git a/app/models/question.rb b/app/models/question.rb index 8a5a833..8b36f9a 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -1,51 +1,104 @@ -class Question < ActiveRecord::Base +# == Schema Information +# +# Table name: questions +# +# id :integer not null, primary key +# default_value :text +# modifiable :boolean +# number :integer +# option_comment_display :boolean default(TRUE) +# text :text +# created_at :datetime +# updated_at :datetime +# question_format_id :integer +# section_id :integer +# +# Indexes +# +# index_questions_on_section_id (section_id) +# +# Foreign Keys +# +# fk_rails_... (question_format_id => question_formats.id) +# fk_rails_... (section_id => sections.id) +# +class Question < ActiveRecord::Base + include ValidationMessages + include ActsAsSortable + + # ============== + # = Attributes = + # ============== + + alias_attribute :to_s, :text + + # include ## # Sort order: Number ASC default_scope { order(number: :asc) } - ## - # Associations - has_many :answers, :dependent => :destroy - has_many :question_options, :dependent => :destroy, :inverse_of => :question # inverse_of needed for nester forms - has_many :annotations, :dependent => :destroy + # ================ + # = Associations = + # ================ + + has_many :answers, dependent: :destroy + + # inverse_of needed for nested forms + has_many :question_options, dependent: :destroy, inverse_of: :question + + has_many :annotations, dependent: :destroy, inverse_of: :question + has_and_belongs_to_many :themes, join_table: "questions_themes" + belongs_to :section + belongs_to :question_format + has_one :phase, through: :section + has_one :template, through: :section - ## - # Nested Attributes + + # =============== + # = Validations = + # =============== + + validates :text, presence: { message: PRESENCE_MESSAGE } + + validates :section, presence: { message: PRESENCE_MESSAGE } + + validates :question_format, presence: { message: PRESENCE_MESSAGE } + + validates :number, presence: { message: PRESENCE_MESSAGE }, + uniqueness: { scope: :section_id, + message: UNIQUENESS_MESSAGE } + + + # ===================== + # = Nested Attributes = + # ===================== + # TODO: evaluate if we need this - accepts_nested_attributes_for :answers, :reject_if => lambda {|a| a[:text].blank? }, :allow_destroy => true - accepts_nested_attributes_for :question_options, :reject_if => lambda {|a| a[:text].blank? }, :allow_destroy => true - accepts_nested_attributes_for :annotations, :allow_destroy => true + accepts_nested_attributes_for :answers, reject_if: -> (a) { a[:text].blank? }, + allow_destroy: true - ## - # Possibly needed for active_admin - # -relies on protected_attributes gem as syntax depricated in rails 4.2 - attr_accessible :default_value, :dependency_id, :dependency_text, :guidance,:number, - :annotation, :text, :section_id, :question_format_id, - :question_options_attributes, :annotations_attributes, - :option_comment_display, :theme_ids, :section, :question_format, - :question_options, :annotations, :answers, :themes, - :modifiable, :option_comment_display, :as => [:default, :admin] + accepts_nested_attributes_for :question_options, allow_destroy: true, + reject_if: -> (a) { a[:text].blank? } - validates :text, :section, :number, presence: {message: _("can't be blank")} - - ## - # returns the text from the question - # - # @return [String] question's text - def to_s - "#{text}" - end + accepts_nested_attributes_for :annotations, allow_destroy: true - def option_based? - format = self.question_format - return format.option_based - end + + # ===================== + # = Delegated methods = + # ===================== + + delegate :option_based?, to: :question_format + + + # =========================== + # = Public instance methods = + # =========================== def deep_copy(**options) copy = self.dup @@ -58,21 +111,21 @@ self.themes.each{ |theme| copy.themes << theme } return copy end - -# TODO: consider moving this to a view helper instead and use the built in scopes for guidance. May need to add -# a new one for 'thematic_guidance'. This method doesn't even make reference to this class and its returning -# a hash that is specific to a view - ## - # guidance for org + + # TODO: consider moving this to a view helper instead and use the built in + # scopes for guidance. May need to add a new one for 'thematic_guidance'. + # This method doesn't even make reference to this class and its returning + # a hash that is specific to a view guidance for org # - # @param org [Org] the org to find guidance for - # @return [Hash{String => String}] + # org - The Org to find guidance for + # + # Returns Hash def guidance_for_org(org) # pulls together guidance from various sources for question guidances = {} - theme_ids = themes.collect{|t| t.id} - if theme_ids.present? - GuidanceGroup.includes(guidances: :themes).where(org_id: org.id).each do |group| + if theme_ids.any? + GuidanceGroup.includes(guidances: :themes) + .where(org_id: org.id).each do |group| group.guidances.each do |g| g.themes.each do |theme| if theme_ids.include? theme.id @@ -86,32 +139,42 @@ return guidances end - ## # get example answer belonging to the currents user for this question # - # @param org_ids [Array<Integer>] the ids for the organisations - # @return [Array<Annotation>] the example answers for this question for the specified orgs - def get_example_answers(org_ids) - org_ids = [org_ids] unless org_ids.is_a?(Array) - self.annotations.where(org_id: org_ids, type: Annotation.types[:example_answer]).order(:created_at) + # org_ids - The ids for the organisations + # + # Returns ActiveRecord::Relation + def example_answers(org_ids) + annotations.where(org_id: Array(org_ids), + type: Annotation.types[:example_answer]) + .order(:created_at) end - ## + alias get_example_answers example_answers + + deprecate :get_example_answers, + deprecator: Cleanup::Deprecators::GetDeprecator.new + # get guidance belonging to the current user's org for this question(need org # to distinguish customizations) # - # @param org_id [Integer] the id for the organisation - # @return [String] the annotation guidance for this question for the specified org - def get_guidance_annotation(org_id) - guidance = self.annotations.where(org_id: org_id).where(type: Annotation.types[:guidance]) - return guidance.first + # org_id - The id for the organisation + # + # Returns Annotation + def guidance_annotation(org_id) + annotations.where(org_id: org_id, type: Annotation.types[:guidance]).first end + alias get_guidance_annotation guidance_annotation + + deprecate :get_guidance_annotation, + deprecator: Cleanup::Deprecators::GetDeprecator.new + def annotations_per_org(org_id) example_answer = annotations.find_by(org_id: org_id, type: Annotation.types[:example_answer]) - guidance = annotations.find_by(org_id: org_id, type: Annotation.types[:guidance]) + guidance = annotations.find_by(org_id: org_id, type: Annotation.types[:guidance]) example_answer = annotations.build({ type: :example_answer, text: '', org_id: org_id }) unless example_answer.present? - guidance = annotations.build({ type: :guidance, text: '', org_id: org_id }) unless guidance.present? + guidance = annotations.build({ type: :guidance, text: '', org_id: org_id }) unless guidance.present? return [example_answer, guidance] end end diff --git a/app/models/question_format.rb b/app/models/question_format.rb index b6d1e36..45056dc 100644 --- a/app/models/question_format.rb +++ b/app/models/question_format.rb @@ -1,37 +1,61 @@ +# == Schema Information +# +# Table name: question_formats +# +# id :integer not null, primary key +# description :text +# formattype :integer default(0) +# option_based :boolean default(FALSE) +# title :string +# created_at :datetime not null +# updated_at :datetime not null +# + class QuestionFormat < ActiveRecord::Base + include ValidationMessages + include ValidationValues ## - # Associations + # + FORMAT_TYPES = %i[textarea textfield radiobuttons checkbox dropdown + multiselectbox date rda_metadata] + + + # ============== + # = Attributes = + # ============== + + enum formattype: FORMAT_TYPES + + alias_attribute :to_s, :title + + alias_attribute :option_based?, :option_based + + # ================ + # = Associations = + # ================ + has_many :questions - enum formattype: [ :textarea, :textfield, :radiobuttons, :checkbox, :dropdown, :multiselectbox, :date, :rda_metadata ] - attr_accessible :formattype - validates :title, presence: {message: _("can't be blank")}, uniqueness: {message: _("must be unique")} + # =============== + # = Validations = + # =============== - ## - # Possibly needed for active_admin - # -relies on protected_attributes gem as syntax depricated in rails 4.2 - attr_accessible :title, :description, :option_based, :questions, :as => [:default, :admin] + validates :title, presence: { message: PRESENCE_MESSAGE }, + uniqueness: { message: UNIQUENESS_MESSAGE } + + validates :description, presence: { message: PRESENCE_MESSAGE } + + validates :option_based, inclusion: { in: BOOLEAN_VALUES } + + + # ================= + # = Class methods = + # ================= # Retrieves the id for a given formattype passed - scope :id_for, -> (formattype) { where(formattype: formattype).pluck(:id).first } - - ## - # Define Bit Field Values so we can test a format without doing string comps - # Column type - - # EVALUATE CLASS AND INSTANCE METHODS BELOW - # - # What do they do? do they do it efficiently, and do we need them? - - - ## - # gives the title of the question_format - # - # @return [String] title of the question_format - def to_s - "#{title}" + def self.id_for(formattype) + where(formattype: formattype).pluck(:id).first end - end diff --git a/app/models/question_option.rb b/app/models/question_option.rb index 3eeb7e5..31e1187 100644 --- a/app/models/question_option.rb +++ b/app/models/question_option.rb @@ -1,28 +1,64 @@ +# == Schema Information +# +# Table name: question_options +# +# id :integer not null, primary key +# is_default :boolean +# number :integer +# text :string +# created_at :datetime +# updated_at :datetime +# question_id :integer +# +# Indexes +# +# index_question_options_on_question_id (question_id) +# +# Foreign Keys +# +# fk_rails_... (question_id => questions.id) +# + class QuestionOption < ActiveRecord::Base - ## - # Associations + include ValidationMessages + include ValidationValues + + # ================ + # = Associations = + # ================ + belongs_to :question + has_and_belongs_to_many :answers, join_table: :answers_question_options - ## - # Possibly needed for active_admin - # -relies on protected_attributes gem as syntax depricated in rails 4.2 - attr_accessible :text, :question_id, :is_default, :number, :question, - :as => [:default, :admin] - validates :text, :question, :number, presence: {message: _("can't be blank")} + # =============== + # = Validations = + # =============== + + validates :text, presence: { message: PRESENCE_MESSAGE } + + validates :question, presence: { message: PRESENCE_MESSAGE } + + validates :number, presence: { message: PRESENCE_MESSAGE } + + validates :is_default, inclusion: { in: BOOLEAN_VALUES, + message: INCLUSION_MESSAGE } + + # ========== + # = Scopes = + # ========== scope :by_number, -> { order(:number) } - ## - # deep copy the given question_option and all it's associations - # - # @params [QuestionOption] question_option to be deep copied - # @return [QuestionOption] the saved, copied question_option - def self.deep_copy(question_option) - question_option_copy = question_option.dup - question_option_copy.save! - return question_option_copy - end + + + # =========================== + # = Public instance methods = + # =========================== + + # =========================== + # = Public instance methods = + # =========================== def deep_copy(**options) copy = self.dup diff --git a/app/models/region.rb b/app/models/region.rb index a7e366a..6dd483f 100644 --- a/app/models/region.rb +++ b/app/models/region.rb @@ -1,8 +1,35 @@ -class Region < ActiveRecord::Base - has_many :sub_regions, class_name: 'Region', foreign_key: 'super_region_id' - - belongs_to :super_region, class_name: 'Region' - - validates :name, presence: {message: _("can't be blank")}, uniqueness: {message: _("must be unique")} - validates :abbreviation, uniqueness: {message: _("must be unique")}, allow_nil: true -end \ No newline at end of file +# == Schema Information +# +# Table name: regions +# +# id :integer not null, primary key +# abbreviation :string +# description :string +# name :string +# super_region_id :integer +# + +class Region < ActiveRecord::Base + include ValidationMessages + + # ================ + # = Associations = + # ================ + + has_many :sub_regions, class_name: 'Region', foreign_key: 'super_region_id' + + belongs_to :super_region, class_name: 'Region' + + # =============== + # = Validations = + # =============== + + validates :name, presence: { message: PRESENCE_MESSAGE }, + uniqueness: { message: UNIQUENESS_MESSAGE } + + validates :description, presence: true + + validates :abbreviation, presence: { message: PRESENCE_MESSAGE }, + uniqueness: { message: UNIQUENESS_MESSAGE } + +end diff --git a/app/models/role.rb b/app/models/role.rb index e48bc58..321aff3 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -1,11 +1,70 @@ -class Role < ActiveRecord::Base - after_initialize :set_defaults - include FlagShihTzu +# == Schema Information +# +# Table name: roles +# +# id :integer not null, primary key +# access :integer default(0), not null +# active :boolean default(TRUE) +# created_at :datetime +# updated_at :datetime +# plan_id :integer +# user_id :integer +# +# Indexes +# +# index_roles_on_plan_id (plan_id) +# index_roles_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (plan_id => plans.id) +# fk_rails_... (user_id => users.id) +# - ## - # Associationsrequire "role" +class Role < ActiveRecord::Base + + include FlagShihTzu + include ValidationMessages + include ValidationValues + + # ============= + # = Constants = + # ============= + + # Returns a hash of hashes where each key represents an access level + # (e.g. see access_level method to understand the integers) + # + # This method becomes useful for generating template messages (e.g. + # permissions change notification mailer) + ACCESS_LEVEL_MESSAGES = { + 5 => { + type: _('reviewer'), + placeholder1: _('read the plan and provide feedback.'), + placeholder2: nil + }, + 3 => { + type: _('co-owner'), + placeholder1: _('write and edit the plan in a collaborative manner.'), + placeholder2: _('You can also grant rights to other collaborators.') + }, + 2 => { + type: _('editor'), + placeholder1: _('write and edit the plan in a collaborative manner.'), + placeholder2: nil, + }, + 1 => { + type: _('read-only'), + placeholder1: _('read the plan and leave comments.'), + placeholder2: nil, + } + } + + # ================ + # = Associations = + # ================ belongs_to :user + belongs_to :plan ## @@ -18,15 +77,34 @@ 5 => :reviewer, # 16 column: 'access' - validates :user, :plan, :access, presence: {message: _("can't be blank")} - validates :access, numericality: {greater_than: 0, message: _("can't be less than zero")} + # =============== + # = Validations = + # =============== + + validates :user, presence: { message: PRESENCE_MESSAGE } + + validates :plan, presence: { message: PRESENCE_MESSAGE } + + validates :active, inclusion: { in: BOOLEAN_VALUES, + message: INCLUSION_MESSAGE } + + validates :access, presence: { message: PRESENCE_MESSAGE }, + numericality: { greater_than: 0, only_integer: true, + message: _("can't be less than zero") } + + # ============= + # = Callbacks = + # ============= + + # TODO: Push this down to the DB constraints + after_initialize :set_defaults ## # Roles with given FlagShihTzu access flags # - # @param flags [Array] One or more symbols that represent access flags + # flags - One or more symbols that represent access flags # - # @return [ActiveRecord::Relation] + # Return ActiveRecord::Relation scope :with_access_flags, -> (*flags) { bad_flag = flags.detect { |flag| !flag.in?(flag_mapping['access'].keys) } raise ArgumentError, "Unkown access flag '#{bad_flag}'" if bad_flag @@ -36,15 +114,19 @@ where(access: access_values) } - ## - # return the access level for the current project group - # 5 if the user is a reviewer - # 3 if the user is an administrator - # 2 if the user is an editor - # 1 if the user can only read + # =========================== + # = Public instance methods = + # =========================== + + + # The access level for the current project group: + # - 5 if the user is a reviewer + # - 3 if the user is an administrator + # - 2 if the user is an editor + # - 1 if the user can only read # used to facilliatte formtastic # - # @return [Integer] + # Returns Integer def access_level if self.reviewer? return 5 @@ -57,52 +139,17 @@ end end - # Sets access_level according to bit fields defined in the column access - # TODO refactor according to the hash defined above (e.g. 1 key is :creator, 2 key is :administrator, etc) - def set_access_level(access_level) - if access_level >= 1 - self.commenter = true - else - self.commenter = false - end - if access_level >= 2 - self.editor = true - else - self.editor = false - end - if access_level >= 3 - self.administrator = true - else - self.administrator = false - end + def access_level=(value) + self.commenter = value.to_i >= 1 + self.editor = value.to_i >= 2 + self.administrator = value.to_i >= 3 end - # Returns a hash of hashes where each key represents an access level (e.g. see access_level method to understand the integers) - # This method becomes useful for generating template messages (e.g. permissions change notification mailer) - def self.access_level_messages - { - 5 => { - :type => _('reviewer'), - :placeholder1 => _('read the plan and provide feedback.'), - :placeholder2 => nil - }, - 3 => { - :type => _('co-owner'), - :placeholder1 => _('write and edit the plan in a collaborative manner.'), - :placeholder2 => _('You can also grant rights to other collaborators.') - }, - 2 => { - :type => _('editor'), - :placeholder1 => _('write and edit the plan in a collaborative manner.'), - :placeholder2 => nil, - }, - 1 => { - :type => _('read-only'), - :placeholder1 => _('read the plan and leave comments.'), - :placeholder2 => nil, - } - } - end + alias :set_access_level :access_level= + + deprecate :set_access_level, + deprecator: Cleanup::Deprecators::SetDeprecator.new + def set_defaults self.active = true if self.new_record? @@ -143,4 +190,4 @@ # 28 - editor + commenter + reviewer # 29 - creator + editor + commenter + reviewer # 30 - administrator + editor + commenter + reviewer -# 31 - creator + administrator + editor + commenter + reviewer \ No newline at end of file +# 31 - creator + administrator + editor + commenter + reviewer diff --git a/app/models/scopes/template_scope.rb b/app/models/scopes/template_scope.rb deleted file mode 100644 index 1f5556f..0000000 --- a/app/models/scopes/template_scope.rb +++ /dev/null @@ -1,90 +0,0 @@ -require 'active_support/concern' - -module TemplateScope - extend ActiveSupport::Concern - - included do - scope :archived, -> { where(archived: true) } - scope :unarchived, -> { where(archived: false) } -# scope :default, -> { published(where(is_default: true).last) } - scope :default, -> { where(is_default: true, published: true).last } - scope :published, -> (family_id = nil) { - if family_id.present? - unarchived.where(published: true, family_id: family_id) - else - unarchived.where(published: true) - end - } - - # Retrieves the latest templates, i.e. those with maximum version associated. It can be filtered down - # if family_id is passed. NOTE, the template objects instantiated only contain version and family attributes - # populated. See Template::latest_version scope method for an adequate instantiation of template instances - scope :latest_version_per_family, -> (family_id = nil) { - chained_scope = unarchived.select("MAX(version) AS version", :family_id) - if family_id.present? - chained_scope = chained_scope.where(family_id: family_id) - end - chained_scope.group(:family_id) - } - scope :latest_customized_version_per_customised_of, -> (customization_of=nil, org_id = nil) { - chained_scope = select("MAX(version) AS version", :customization_of) - chained_scope = chained_scope.where(customization_of: customization_of) - if org_id.present? - chained_scope = chained_scope.where(org_id: org_id) - end - chained_scope.group(:customization_of) - } - # Retrieves the latest templates, i.e. those with maximum version associated. It can be filtered down - # if family_id is passed - scope :latest_version, -> (family_id = nil) { - unarchived.from(latest_version_per_family(family_id), :current) - .joins("INNER JOIN templates ON current.version = templates.version " + - "AND current.family_id = templates.family_id INNER JOIN orgs ON orgs.id = templates.org_id") - } - # Retrieves the latest customized versions, i.e. those with maximum version associated for a set - # of family_id and an org - scope :latest_customized_version, -> (family_id = nil, org_id = nil) { - unarchived.from(latest_customized_version_per_customised_of(family_id, org_id), :current) - .joins("INNER JOIN templates ON current.version = templates.version"\ - " AND current.customization_of = templates.customization_of INNER JOIN orgs ON orgs.id = templates.org_id") - .where(templates: { org_id: org_id }) - } - # Retrieves the latest templates, i.e. those with maximum version associated for a set of org_id passed - scope :latest_version_per_org, -> (org_id = nil) { - if org_id.respond_to?(:each) - family_ids = families(org_id).pluck(:family_id) - else - family_ids = families([org_id]).pluck(:family_id) - end - latest_version(family_ids) - } - # Retrieve all of the latest customizations for the specified org - scope :latest_customized_version_per_org, -> (org_id=nil) { - family_ids = families(org_id).pluck(:family_id) - latest_customized_version(family_ids, org_id) - } - # Retrieves templates with distinct family_id. It can be filtered down if org_id is passed - scope :families, -> (org_id=nil) { - if org_id.respond_to?(:each) - unarchived.where(org_id: org_id, customization_of: nil).distinct - else - unarchived.where(customization_of: nil).distinct - end - } - # Retrieves the latest version of each customizable funder template (and the default template) - scope :latest_customizable, -> { - family_ids = families(Org.funder.collect(&:id)).distinct.pluck(:family_id) << default.family_id - published(family_ids.flatten).where('visibility = ? OR is_default = ?', visibilities[:publicly_visible], true) - } - # Retrieves unarchived templates with public visibility - scope :publicly_visible, -> { unarchived.where(:visibility => visibilities[:publicly_visible]) } - # Retrieves unarchived templates with organisational visibility - scope :organisationally_visible, -> { unarchived.where(:visibility => visibilities[:organisationally_visible]) } - # Retrieves unarchived templates whose title or org.name includes the term passed - scope :search, -> (term) { - search_pattern = "%#{term}%" - unarchived.where("templates.title LIKE ? OR orgs.name LIKE ?", search_pattern, search_pattern) - } - end -end - diff --git a/app/models/section.rb b/app/models/section.rb index cf9282a..42cb330 100644 --- a/app/models/section.rb +++ b/app/models/section.rb @@ -1,26 +1,94 @@ +# == Schema Information +# +# Table name: sections +# +# id :integer not null, primary key +# description :text +# modifiable :boolean +# number :integer +# title :string +# created_at :datetime +# updated_at :datetime +# phase_id :integer +# +# Indexes +# +# index_sections_on_phase_id (phase_id) +# +# Foreign Keys +# +# fk_rails_... (phase_id => phases.id) +# + class Section < ActiveRecord::Base - ## - # Associations + include ValidationMessages + include ValidationValues + include ActsAsSortable + + # ================ + # = Associations = + # ================ + belongs_to :phase belongs_to :organisation - has_many :questions, :dependent => :destroy + has_many :questions, dependent: :destroy has_one :template, through: :phase - #Link the data - accepts_nested_attributes_for :questions, :reject_if => lambda {|a| a[:text].blank? }, :allow_destroy => true + # =============== + # = Validations = + # =============== - attr_accessible :phase_id, :description, :number, :title, :published, - :questions_attributes, :organisation, :phase, :modifiable, - :as => [:default, :admin] + validates :phase, presence: { message: PRESENCE_MESSAGE } - validates :phase, :title, :number, presence: {message: _("can't be blank")} + validates :title, presence: { message: PRESENCE_MESSAGE } - before_validation :set_defaults + # validates :description, presence: { message: PRESENCE_MESSAGE } - ## - # return the title of the section + validates :number, presence: { message: PRESENCE_MESSAGE }, + uniqueness: { scope: :phase_id, + message: UNIQUENESS_MESSAGE } + + validates :modifiable, inclusion: { in: BOOLEAN_VALUES, + message: INCLUSION_MESSAGE } + + # ============= + # = Callbacks = + # ============= + + # TODO: Move this down to DB constraints + before_validation :set_modifiable + + before_validation :set_number + + # ===================== + # = Nested Attributes = + # ===================== + + accepts_nested_attributes_for :questions, + reject_if: -> (a) { a[:text].blank? }, + allow_destroy: true + + # ========== + # = Scopes = + # ========== + + # The sections for this Phase that have been added by the admin # - # @return [String] the title of the section + # Returns ActiveRecord::Relation + scope :modifiable, -> { where(modifiable: true) } + + # The sections for this Phase that were part of the original Template + # + # Returns ActiveRecord::Relation + scope :not_modifiable, -> { where(modifiable: false) } + + # =========================== + # = Public instance methods = + # =========================== + + # The title of the Section + # + # Returns String def to_s "#{title}" end @@ -28,13 +96,10 @@ # Returns the number of answered questions for a given plan def num_answered_questions(plan) return 0 if plan.nil? - questions_hash = questions.reduce({}){ |m, q| m[q.id] = q; m } - return plan.answers.includes({ question: :question_format }, :question_options).reduce(0) do |m, a| - if questions_hash[a.question_id].present? && a.is_valid? - m+= 1 - end - m - end + plan.answers.includes({ question: :question_format }, :question_options) + .where(question_id: question_ids) + .to_a + .count(&:is_valid?) end def deep_copy(**options) @@ -42,13 +107,28 @@ copy.modifiable = options.fetch(:modifiable, self.modifiable) copy.phase_id = options.fetch(:phase_id, nil) copy.save!(validate: false) if options.fetch(:save, false) - options[:section_id] = id + options[:section_id] = copy.id self.questions.map{ |question| copy.questions << question.deep_copy(options) } return copy end + # Can't be modified as it was duplicatd over from another Phase. + def template? + !modifiable? + end + private - def set_defaults - self.modifiable = true if modifiable.nil? - end + + # ============================ + # = Private instance methods = + # ============================ + + def set_modifiable + self.modifiable = true if modifiable.nil? + end + + def set_number + return if phase.nil? + self.number ||= phase.sections.maximum(:number) + 1 + end end diff --git a/app/models/settings/template.rb b/app/models/settings/template.rb index d27957f..79a8941 100644 --- a/app/models/settings/template.rb +++ b/app/models/settings/template.rb @@ -1,8 +1,19 @@ +# == Schema Information +# +# Table name: settings +# +# id :integer not null, primary key +# target_type :string not null +# value :text +# var :string not null +# created_at :datetime not null +# updated_at :datetime not null +# target_id :integer not null +# + module Settings class Template < RailsSettings::SettingObject - #attr_accessible :var, :target, :target_id, :target_type - VALID_FONT_FACES = [ '"Times New Roman", Times, Serif', 'Arial, Helvetica, Sans-Serif' @@ -13,7 +24,9 @@ VALID_ADMIN_FIELDS = ['project_name', 'project_identifier', 'grant_title', 'principal_investigator', 'project_data_contact', 'project_description', 'funder', 'institution', 'orcid'] - + + VALID_FORMATS = ['csv', 'html', 'pdf', 'text', 'docx'] + DEFAULT_SETTINGS = { formatting: { margin: { diff --git a/app/models/splash_log.rb b/app/models/splash_log.rb index 43dcc4d..ea57796 100644 --- a/app/models/splash_log.rb +++ b/app/models/splash_log.rb @@ -1,3 +1,12 @@ -class SplashLog < ActiveRecord::Base - #attr_accessible :destination -end +# == Schema Information +# +# Table name: splash_logs +# +# id :integer not null, primary key +# destination :string +# created_at :datetime not null +# updated_at :datetime not null +# + +class SplashLog < ActiveRecord::Base +end diff --git a/app/models/template.rb b/app/models/template.rb index d973c03..b0ce36a 100644 --- a/app/models/template.rb +++ b/app/models/template.rb @@ -1,126 +1,338 @@ +# == Schema Information +# +# Table name: templates +# +# id :integer not null, primary key +# archived :boolean +# customization_of :integer +# description :text +# is_default :boolean +# links :text +# locale :string +# published :boolean +# title :string +# version :integer +# visibility :integer +# created_at :datetime +# updated_at :datetime +# family_id :integer +# org_id :integer +# +# Indexes +# +# index_templates_on_customization_of_and_version_and_org_id (customization_of,version,org_id) UNIQUE +# index_templates_on_family_id (family_id) +# index_templates_on_family_id_and_version (family_id,version) UNIQUE +# index_templates_on_org_id (org_id) +# template_organisation_dmptemplate_index (org_id,family_id) +# +# Foreign Keys +# +# fk_rails_... (org_id => orgs.id) +# + class Template < ActiveRecord::Base include GlobalHelpers - include ActiveModel::Validations - include TemplateScope + include ValidationMessages + include ValidationValues + validates_with TemplateLinksValidator - before_validation :set_defaults - after_update :reconcile_published, if: Proc.new { |template| template.published? } - # Stores links as an JSON object: { funder: [{"link":"www.example.com","text":"foo"}, ...], sample_plan: [{"link":"www.example.com","text":"foo"}, ...]} - # The links is validated against custom validator allocated at validators/template_links_validator.rb + # Stores links as an JSON object: + # {funder: [{"link":"www.example.com","text":"foo"}, ...], + # sample_plan: [{"link":"www.example.com","text":"foo"}, ...]} + # + # The links is validated against custom validator allocated at + # validators/template_links_validator.rb serialize :links, JSON - - ## - # Associations + + # ================ + # = Associations = + # ================ + belongs_to :org + has_many :plans + has_many :phases, dependent: :destroy + has_many :sections, through: :phases + has_many :questions, through: :sections - ## - # Possibly needed for active_admin - # -relies on protected_attributes gem as syntax depricated in rails 4.2 - attr_accessible :id, :org_id, :description, :published, :title, :locale, :customization_of, - :is_default, :guidance_group_ids, :org, :plans, :phases, :family_id, - :archived, :version, :visibility, :published, :links, :as => [:default, :admin] + has_many :annotations, through: :questions - # A standard template should be organisationally visible. Funder templates that are - # meant for external use will be publicly visible. This allows a funder to create 'funder' as - # well as organisational templates. The default template should also always be publicly_visible - enum visibility: [:organisationally_visible, :publicly_visible] + # =============== + # = Validations = + # =============== + + validates :title, presence: { message: PRESENCE_MESSAGE } + + validates :org, presence: { message: PRESENCE_MESSAGE } + + validates :locale, presence: { message: PRESENCE_MESSAGE } + + validates :version, presence: { message: PRESENCE_MESSAGE }, + uniqueness: { message: UNIQUENESS_MESSAGE, + scope: :family_id } + + validates :visibility, presence: { message: PRESENCE_MESSAGE } + + validates :family_id, presence: { message: PRESENCE_MESSAGE } + + + # ============= + # = Callbacks = + # ============= + + before_validation :set_defaults + + after_update :reconcile_published, if: -> (template) { template.published? } + + # ========== + # = Scopes = + # ========== + + scope :archived, -> { where(archived: true) } + + scope :unarchived, -> { where(archived: false) } + + scope :published, ->(family_id = nil) { + if family_id.present? + unarchived.where(published: true, family_id: family_id) + else + unarchived.where(published: true) + end + } + + # Retrieves the latest templates, i.e. those with maximum version associated. + # It can be filtered down if family_id is passed + scope :latest_version, ->(family_id = nil) { + unarchived.from(latest_version_per_family(family_id), :current) + .joins(<<~SQL) + INNER JOIN templates ON current.version = templates.version + AND current.family_id = templates.family_id + INNER JOIN orgs ON orgs.id = templates.org_id + SQL + } + + # Retrieves the latest customized versions, i.e. those with maximum version + # associated for a set of family_id and an org + scope :latest_customized_version, ->(family_id = nil, org_id = nil) { + unarchived + .from(latest_customized_version_per_customised_of(family_id, org_id), + :current) + .joins(<<~SQL) + INNER JOIN templates ON current.version = templates.version + AND current.customization_of = templates.customization_of + INNER JOIN orgs ON orgs.id = templates.org_id + SQL + .where(templates: { org_id: org_id }) + } + + # Retrieves the latest templates, i.e. those with maximum version associated + # for a set of org_id passed + scope :latest_version_per_org, ->(org_id = nil) { + family_ids = if org_id.respond_to?(:each) + families(org_id).pluck(:family_id) + else + families([org_id]).pluck(:family_id) + end + latest_version(family_ids) + } + + # Retrieve all of the latest customizations for the specified org + scope :latest_customized_version_per_org, lambda { |org_id = nil| + family_ids = families(org_id).pluck(:family_id) + latest_customized_version(family_ids, org_id) + } + + # Retrieves templates with distinct family_id. It can be filtered down if + # org_id is passed + scope :families, lambda { |org_id = nil| + if org_id.respond_to?(:each) + unarchived.where(org_id: org_id, customization_of: nil).distinct + else + unarchived.where(customization_of: nil).distinct + end + } + + # Retrieves the latest version of each customizable funder template (and the + # default template) + scope :latest_customizable, lambda { + funder_ids = Org.funder.pluck(:id) + family_ids = families(funder_ids).distinct + .pluck(:family_id) + [default.family_id] + + published(family_ids.flatten) + .where('visibility = :visibility OR is_default = :is_default', + visibility: visibilities[:publicly_visible], is_default: true) + } + + # Retrieves unarchived templates with public visibility + scope :publicly_visible, lambda { + unarchived.where(visibility: visibilities[:publicly_visible]) + } + + # Retrieves unarchived templates with organisational visibility + scope :organisationally_visible, lambda { + unarchived.where(visibility: visibilities[:organisationally_visible]) + } + + # Retrieves unarchived templates whose title or org.name includes the term + # passed + scope :search, lambda { |term| + unarchived.where("templates.title LIKE :term OR orgs.name LIKE :term", + term: "%#{term}%") + } + + # A standard template should be organisationally visible. Funder templates + # that are meant for external use will be publicly visible. This allows a + # funder to create 'funder' as well as organisational templates. The default + # template should also always be publicly_visible. + enum visibility: %i[organisationally_visible publicly_visible] # defines the export setting for a template object has_settings :export, class_name: 'Settings::Template' do |s| s.key :export, defaults: Settings::Template::DEFAULT_SETTINGS end - validates :org, :title, presence: {message: _("can't be blank")} + validates :org, :title, presence: { message: _("can't be blank") } - # Class methods gets defined within this - class << self - def current(family_id) - unarchived.where(family_id: family_id).order(version: :desc).first + # ================= + # = Class Methods = + # ================= + + def self.default + where(is_default: true, published: true).last + end + + def self.current(family_id) + unarchived.where(family_id: family_id).order(version: :desc).first + end + + def self.live(family_id) + if family_id.respond_to?(:each) + unarchived.where(family_id: family_id, published: true) + else + unarchived.where(family_id: family_id, published: true).first end - def live(family_id) - if family_id.respond_to?(:each) - unarchived.where(family_id: family_id, published: true) - else - unarchived.where(family_id: family_id, published: true).first - end - end - def find_or_generate_version!(template) - if template.latest? - if template.generate_version? - return template.generate_version! - end - return template - end + end + + def self.find_or_generate_version!(template) + if template.latest? && template.generate_version? + template.generate_version! + elsif template.latest? && !template.generate_version? + template + else raise _('A historical template cannot be retrieved for being modified') end end + # Retrieves the latest templates, i.e. those with maximum version associated. + # It can be filtered down if family_id is passed. NOTE, the template objects + # instantiated only contain version and family attributes populated. See + # Template::latest_version scope method for an adequate instantiation of + # template instances. + def self.latest_version_per_family(family_id = nil) + chained_scope = unarchived.select("MAX(version) AS version", :family_id) + if family_id.present? + chained_scope = chained_scope.where(family_id: family_id) + end + chained_scope.group(:family_id) + end + + private_class_method :latest_version_per_family + + def self.latest_customized_version_per_customised_of(customization_of = nil, + org_id = nil) + chained_scope = select("MAX(version) AS version", :customization_of) + chained_scope = chained_scope.where(customization_of: customization_of) + chained_scope = chained_scope.where(org_id: org_id) if org_id.present? + chained_scope.group(:customization_of) + end + + private_class_method :latest_customized_version_per_customised_of + + + # =========================== + # = Public instance methods = + # =========================== + # Creates a copy of the current template - # raises ActiveRecord::RecordInvalid when save option is true and validations fails + # raises ActiveRecord::RecordInvalid when save option is true and validations + # fails. def deep_copy(attributes: {}, **options) - copy = self.dup + copy = dup if attributes.respond_to?(:each_pair) - attributes.each_pair{ |attribute, value| copy.send("#{attribute}=".to_sym, value) if copy.respond_to?("#{attribute}=".to_sym) } + attributes.each_pair do |attribute, value| + if copy.respond_to?("#{attribute}=".to_sym) + copy.send("#{attribute}=".to_sym, value) + end + end end copy.save! if options.fetch(:save, false) options[:template_id] = copy.id - self.phases.each{ |phase| copy.phases << phase.deep_copy(options) } - return copy + phases.each { |phase| copy.phases << phase.deep_copy(options) } + copy end # Retrieves the template's org or the org of the template this one is derived # from of it is a customization def base_org - if self.customization_of.present? - return Template.where(family_id: self.customization_of).first.org + if customization_of.present? + Template.where(family_id: customization_of).first.org else - return self.org + org end end - # Returns whether or not this is the latest version of the current template's family + # Returns whether or not this is the latest version of the current template's + # family def latest? - return (self.id == Template.latest_version(self.family_id).pluck(:id).first) + id == Template.latest_version(family_id).pluck(:id).first end + # Determines whether or not a new version should be generated def generate_version? - return self.published - end - # Determines whether or not a customization for the customizing_org passed should be generated - def customize?(customizing_org) - if customizing_org.is_a?(Org) && (self.org.funder_only? || self.is_default) - return !Template.unarchived.where(customization_of: self.family_id, org: customizing_org).exists? - end - return false - end - # Determines whether or not a customized template should be upgraded - def upgrade_customization? - if self.customization_of.present? - funder_template = Template.published(self.customization_of).select(:created_at).first - if funder_template.present? - return funder_template.created_at > self.created_at - end - end - return false + published end - # Checks to see if the template family has a published version and if its not the current template + # Determines whether or not a customization for the customizing_org passed + # should be generated + def customize?(customizing_org) + if customizing_org.is_a?(Org) && (org.funder_only? || is_default) + return !Template.unarchived.where(customization_of: family_id, + org: customizing_org).exists? + end + false + end + + # Determines whether or not a customized template should be upgraded + def upgrade_customization? + if customization_of.present? + funder_template = Template.published(customization_of) + .select(:created_at).first + + return funder_template.created_at > created_at if funder_template.present? + end + false + end + + # Checks to see if the template family has a published version and if its not + # the current template def draft? - return !self.published && Template.published(self.family_id).length > 0 + !published && !Template.published(family_id).empty? end def removable? - versions = Template.includes(:plans).where(family_id: self.family_id) - return versions.select{|version| version.plans.length > 0 }.empty? + versions = Template.includes(:plans).where(family_id: family_id) + versions.reject { |version| version.plans.empty? }.empty? end - # Returns a new unpublished copy of self with a new family_id, version = zero for the specified org + # Returns a new unpublished copy of self with a new family_id, version = zero + # for the specified org def generate_copy!(org) raise _('generate_copy! requires an organisation target') unless org.is_a?(Org) # Assume customizing_org is persisted template = deep_copy( @@ -130,9 +342,10 @@ family_id: new_family_id, org: org, is_default: false, - title: _('Copy of %{template}') % { template: self.title } - }, modifiable: true, save: true) - return template + title: format(_('Copy of %{template}'), template: title) + }, modifiable: true, save: true + ) + template end # Generates a new copy of self with an incremented version number @@ -140,37 +353,57 @@ raise _('generate_version! requires a published template') unless published template = deep_copy( attributes: { - version: self.version+1, + version: version + 1, published: false, - org: self.org - }, save: true) - return template + org: org + }, save: true + ) + template end # Generates a new copy of self for the specified customizing_org def customize!(customizing_org) - raise _('customize! requires an organisation target') unless customizing_org.is_a?(Org) # Assume customizing_org is persisted - raise _('customize! requires a template from a funder') if !self.org.funder_only? && !self.is_default # Assume self has org associated + # Assume customizing_org is persisted + unless customizing_org.is_a?(Org) + raise _('customize! requires an organisation target') + end + + # Assume self has org associated + if !org.funder_only? && !is_default + raise _('customize! requires a template from a funder') + end + customization = deep_copy( attributes: { version: 0, published: false, family_id: new_family_id, - customization_of: self.family_id, + customization_of: family_id, org: customizing_org, visibility: Template.visibilities[:organisationally_visible], is_default: false - }, modifiable: false, save: true) - return customization + }, modifiable: false, save: true + ) + customization end - - # Generates a new copy of self including latest changes from the funder this template is customized_of + + # Generates a new copy of self including latest changes from the funder this + # template is customized_of def upgrade_customization! - raise _('upgrade_customization! requires a customised template') unless customization_of.present? - funder_template = Template.published(self.customization_of).first - raise _('upgrade_customization! cannot be carried out since there is no published template of its current funder') unless funder_template.present? - source = deep_copy(attributes: { version: self.version+1, published: false }) # preserves modifiable flags from the self template copied - # Creates a new customisation for the published template whose family_id is self.customization_of + if customization_of.blank? + raise _('upgrade_customization! requires a customised template') + end + funder_template = Template.published(customization_of).first + + if funder_template.blank? + raise _("upgrade_customization! cannot be carried out since there is no published template of its current funder") + end + + # preserves modifiable flags from the self template copied + source = deep_copy(attributes: { version: version + 1, published: false }) + + # Creates a new customisation for the published template whose family_id is + # self.customization_of customization = funder_template.deep_copy( attributes: { version: source.version, @@ -180,73 +413,100 @@ org: source.org, visibility: Template.visibilities[:organisationally_visible], is_default: false - }, modifiable: false, save: true) + }, modifiable: false, save: true + ) + # Sorts the phases from the source template, i.e. self - sorted_phases = source.phases.sort{ |phase1,phase2| phase1.number <=> phase2.number } - # Merges modifiable sections or questions from source into customization template object + sorted_phases = source.phases.sort_by(&:number) + + # Merges modifiable sections or questions from source into customization + # template object customization.phases.each do |customization_phase| - # Search for the phase in the source template whose number matches the customization_phase - candidate_phase = sorted_phases.bsearch{ |phase| customization_phase.number <=> phase.number } - if candidate_phase.present? # The funder could have added this new phase after the customisation took place - # Selects modifiable sections from the candidate_phase - modifiable_sections = candidate_phase.sections.select{ |section| section.modifiable } - # Attaches modifiable sections into the customization_phase - modifiable_sections.each{ |modifiable_section| customization_phase.sections << modifiable_section } - # Sorts the sections for the customization_phase - sorted_sections = customization_phase.sections.sort{ |section1, section2| section1.number <=> section2.number } - # Selects unmodifiable sections from the candidate_phase - unmodifiable_sections = candidate_phase.sections.select{ |section| !section.modifiable } - unmodifiable_sections.each do |unmodifiable_section| - # Search for modifiable questions within the unmodifiable_section from candidate_phase - modifiable_questions = unmodifiable_section.questions.select{ |question| question.modifiable } - customization_section = sorted_sections.bsearch{ |section| unmodifiable_section.number <=> section.number } - if customization_section.present? # The funder could have deleted the section - modifiable_questions.each{ |modifiable_question| customization_section.questions << modifiable_question; } - end - # Search for unmodifiable questions within the unmodifiable_section in case source template added annotations - unmodifiable_questions = unmodifiable_section.questions.select{ |question| !question.modifiable } - sorted_questions = customization_section.questions.sort{ |question1, question2| question1.number <=> question2.number } - unmodifiable_questions.each do |unmodifiable_question| - customization_question = sorted_questions.bsearch{ |question| unmodifiable_question.number <=> question.number } - if customization_question.present? # The funder could have deleted the question - annotations_added_by_customiser = unmodifiable_question.annotations.select{ |annotation| annotation.org_id == source.org_id } - annotations_added_by_customiser.each{ |annotation| customization_question.annotations << annotation } - end + # Search for the phase in the source template whose number matches the + # customization_phase + + candidate_phase = sorted_phases.bsearch do |phase| + customization_phase.number <=> phase.number + end + + # The funder could have added this new phase after the customisation took + # place + next if candidate_phase.blank? + # Selects modifiable sections from the candidate_phase + modifiable_sections = candidate_phase.sections.select(&:modifiable) + + # Attaches modifiable sections into the customization_phase + modifiable_sections.each { |modifiable_section| customization_phase.sections << modifiable_section } + + # Sorts the sections for the customization_phase + sorted_sections = customization_phase.sections.sort_by(&:number) + + # Selects unmodifiable sections from the candidate_phase + unmodifiable_sections = candidate_phase.sections.reject(&:modifiable) + + unmodifiable_sections.each do |unmodifiable_section| + # Search for modifiable questions within the unmodifiable_section + # from candidate_phase + modifiable_questions = unmodifiable_section.questions.select(&:modifiable) + customization_section = sorted_sections.bsearch { |section| unmodifiable_section.number <=> section.number } + # The funder could have deleted the section + if customization_section.present? + modifiable_questions.each { |modifiable_question| customization_section.questions << modifiable_question; } + end + # Search for unmodifiable questions within the unmodifiable_section in case source template added annotations + unmodifiable_questions = unmodifiable_section.questions.reject(&:modifiable) + sorted_questions = customization_section.questions.sort_by(&:number) + unmodifiable_questions.each do |unmodifiable_question| + customization_question = sorted_questions.bsearch { |question| unmodifiable_question.number <=> question.number } + if customization_question.present? # The funder could have deleted the question + annotations_added_by_customiser = unmodifiable_question.annotations.select { |annotation| annotation.org_id == source.org_id } + annotations_added_by_customiser.each { |annotation| customization_question.annotations << annotation } end end end end # Appends the modifiable phases from source - source.phases.select{ |phase| phase.modifiable }.each{ |modifiable_phase| customization.phases << modifiable_phase } - return customization + source.phases.select(&:modifiable).each do |modifiable_phase| + customization.phases << modifiable_phase + end + customization end private - # Generate a new random family identifier - def new_family_id - family_id = loop do - random = rand 2147483647 - break random unless Template.exists?(family_id: random) - end - family_id + + # ============================ + # = Private instance methods = + # ============================ + + # Generate a new random family identifier + def new_family_id + family_id = loop do + random = rand 2_147_483_647 + break random unless Template.exists?(family_id: random) end - - # Default values to set before running any validation - def set_defaults - self.published ||= false - self.archived ||= false - self.is_default ||= false - self.version ||= 0 - self.visibility = ((self.org.present? && self.org.funder_only?) || self.is_default?) ? Template.visibilities[:publicly_visible] : Template.visibilities[:organisationally_visible] unless self.id.present? - self.customization_of ||= nil - self.family_id ||= new_family_id - self.archived ||= false - self.links ||= { funder: [], sample_plan: [] } - end - - # Only one version of a template should be published at a time, so if this one was published make sure other versions are not - def reconcile_published - # Unpublish all other versions of this template family - Template.where('family_id = ? AND published = ? AND id != ?', self.family_id, true, self.id).update_all(published: false) - end + family_id + end + + # Default values to set before running any validation + def set_defaults + self.published ||= false + self.archived ||= false + self.is_default ||= false + self.version ||= 0 + self.visibility = (org.present? && org.funder_only?) || is_default? ? Template.visibilities[:publicly_visible] : Template.visibilities[:organisationally_visible] if id.blank? + self.customization_of ||= nil + self.family_id ||= new_family_id + self.archived ||= false + self.links ||= { funder: [], sample_plan: [] } + end + + # Only one version of a template should be published at a time, so if this + # one was published make sure other versions are not + def reconcile_published + # Unpublish all other versions of this template family + Template.published + .where(family_id: family_id) + .where.not(id: id) + .update_all(published: false) + end end diff --git a/app/models/theme.rb b/app/models/theme.rb index 4f0738e..4e66430 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -1,30 +1,48 @@ -class Theme < ActiveRecord::Base +# == Schema Information +# +# Table name: themes +# +# id :integer not null, primary key +# description :text +# locale :string +# title :string +# created_at :datetime not null +# updated_at :datetime not null +# - ## - # Associations +class Theme < ActiveRecord::Base + include ValidationMessages + + # ================ + # = Associations = + # ================ + has_and_belongs_to_many :questions, join_table: "questions_themes" has_and_belongs_to_many :guidances, join_table: "themes_in_guidance" - ## - # Possibly needed for active_admin - # -relies on protected_attributes gem as syntax depricated in rails 4.2 - attr_accessible :guidance_ids , :as => [:default, :admin] - attr_accessible :question_ids, :as => [:default, :admin] - attr_accessible :description, :title, :locale , :as => [:default, :admin] + # =============== + # = Validations = + # =============== + validates :title, presence: { message: PRESENCE_MESSAGE } - validates :title, presence: {message: _("can't be blank")} + # ========== + # = Scopes = + # ========== scope :search, -> (term) { search_pattern = "%#{term}%" where("title LIKE ? OR description LIKE ?", search_pattern, search_pattern) } - ## - # returns the title of the theme + + # =========================== + # = Public instance methods = + # =========================== + + # The title of the Theme # - # @return [String] title of the theme + # Returns String def to_s title end - end diff --git a/app/models/token_permission_type.rb b/app/models/token_permission_type.rb index 5770e6c..8dd979b 100644 --- a/app/models/token_permission_type.rb +++ b/app/models/token_permission_type.rb @@ -1,31 +1,56 @@ +# == Schema Information +# +# Table name: token_permission_types +# +# id :integer not null, primary key +# text_description :text +# token_type :string +# created_at :datetime +# updated_at :datetime +# + class TokenPermissionType < ActiveRecord::Base - ## - # Associations - #has_and_belongs_to_many :org_token_permissions, join_table: "org_token_permissions" -# has_and_belongs_to_many :organisations, join_table: 'org_token_permissions', unique: true - has_and_belongs_to_many :orgs, join_table: 'org_token_permissions', unique: true + include ValidationMessages + + # ============= + # = Constants = + # ============= ## - # Possibly needed for active_admin - # - relies on proetected_attributes gem as syntax depricated in rails 4.2 - attr_accessible :token_type, :text_description, :as => [:default, :admin] - - ## - # Validators - validates :token_type, presence: {message: _("can't be blank")}, uniqueness: {message: _("must be unique")} - - ## - # Constant Token Permission Types + # GUIDANCES = TokenPermissionType.where(token_type: 'guidances').first.freeze + + ## + # PLANS = TokenPermissionType.where(token_type: 'plans').first.freeze + + ## + # TEMPLATES = TokenPermissionType.where(token_type: 'templates').first.freeze + + ## + # STATISTICS = TokenPermissionType.where(token_type: 'statistics').first.freeze - ## - # returns the token_type of the token_permission_type + # ================ + # = Associations = + # ================ + + has_and_belongs_to_many :orgs, join_table: 'org_token_permissions', unique: true + + + # ============== + # = Validators = + # ============== + + validates :token_type, presence: { message: PRESENCE_MESSAGE }, + uniqueness: { message: UNIQUENESS_MESSAGE } + + + # The token_type of the token_permission_type # - # @return [String] token_type of the token_permission_type + # Returns String def to_s self.token_type end diff --git a/app/models/user.rb b/app/models/user.rb index 780e610..76882a7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,5 +1,55 @@ +# == Schema Information +# +# Table name: users +# +# id :integer not null, primary key +# accept_terms :boolean +# active :boolean default(TRUE) +# api_token :string +# confirmation_sent_at :datetime +# confirmation_token :string +# confirmed_at :datetime +# current_sign_in_at :datetime +# current_sign_in_ip :string +# email :string(80) default(""), not null +# encrypted_password :string default("") +# firstname :string +# invitation_accepted_at :datetime +# invitation_created_at :datetime +# invitation_sent_at :datetime +# invitation_token :string +# invited_by_type :string +# last_sign_in_at :datetime +# last_sign_in_ip :string +# other_organisation :string +# recovery_email :string +# remember_created_at :datetime +# reset_password_sent_at :datetime +# reset_password_token :string +# sign_in_count :integer default(0) +# surname :string +# created_at :datetime not null +# updated_at :datetime not null +# invited_by_id :integer +# language_id :integer +# org_id :integer +# +# Indexes +# +# index_users_on_email (email) UNIQUE +# index_users_on_org_id (org_id) +# +# Foreign Keys +# +# fk_rails_... (language_id => languages.id) +# fk_rails_... (org_id => orgs.id) +# + class User < ActiveRecord::Base include ConditionalUserMailer + include ValidationMessages + include ValidationValues + ## # Devise # Include default devise modules. Others available are: @@ -14,39 +64,50 @@ # User Notification Preferences serialize :prefs, Hash - ## - # Associations + # ================ + # = Associations = + # ================ + + has_and_belongs_to_many :perms, join_table: :users_perms + belongs_to :language + belongs_to :org + has_one :pref + has_many :answers + has_many :notes + has_many :exported_plans + has_many :roles, dependent: :destroy - has_many :plans, through: :roles do - def filter(query) - return self unless query.present? - t = self.arel_table - q = "%#{query}%" - conditions = t[:title].matches(q) - columns = %i( - grant_number identifier description principal_investigator data_contact - ) - columns = ['grant_number', 'identifier', 'description', 'principal_investigator', 'data_contact'] - columns.each {|col| conditions = conditions.or(t[col].matches(q)) } - self.where(conditions) - end - end + + has_many :plans, through: :roles + has_many :user_identifiers + has_many :identifier_schemes, through: :user_identifiers - has_and_belongs_to_many :notifications, dependent: :destroy, join_table: 'notification_acknowledgements' - validates :email, email: true, allow_nil: true, uniqueness: {message: _("must be unique")} + has_and_belongs_to_many :notifications, dependent: :destroy, + join_table: 'notification_acknowledgements' - ## - # Scopes + + # =============== + # = Validations = + # =============== + + validates :active, inclusion: { in: BOOLEAN_VALUES, + message: INCLUSION_MESSAGE } + + + # ========== + # = Scopes = + # ========== + default_scope { includes(:org, :perms) } # Retrieves all of the org_admins for the specified org @@ -57,8 +118,9 @@ scope :search, -> (term) { search_pattern = "%#{term}%" - # MySQL does not support standard string concatenation and since concat_ws or concat functions do - # not exist for sqlite, we have to come up with this conditional + # MySQL does not support standard string concatenation and since concat_ws + # or concat functions do not exist for sqlite, we have to come up with this + # conditional if ActiveRecord::Base.connection.adapter_name == "Mysql2" where("concat_ws(' ', firstname, surname) LIKE ? OR email LIKE ?", search_pattern, search_pattern) else @@ -66,12 +128,33 @@ end } + # ============= + # = Callbacks = + # ============= + after_update :when_org_changes + # ================= + # = Class methods = + # ================= + ## + # Load the user based on the scheme and id provided by the Omniauth call + def self.from_omniauth(auth) + joins(user_identifiers: :identifier_scheme) + .where(user_identifiers: { identifier: auth.uid }, + identifier_schemes: { name: auth.provider.downcase }).first + end + + # =========================== + # = Public instance methods = + # =========================== + # This method uses Devise's built-in handling for inactive users + # + # Returns Boolean def active_for_authentication? - super && self.active? + super && active? end # EVALUATE CLASS AND INSTANCE METHODS BELOW @@ -79,7 +162,9 @@ # What do they do? do they do it efficiently, and do we need them? # Determines the locale set for the user or the organisation he/she belongs - # @return String or nil + # + # Returns String + # Returns nil def get_locale if !self.language.nil? return self.language.abbreviation @@ -90,12 +175,11 @@ end end - - ## - # gives either the name of the user, or the email if name unspecified + # Gives either the name of the user, or the email if name unspecified # - # @param user_email [Boolean] defaults to true, allows the use of email if there is no firstname or surname - # @return [String] the email or the firstname and surname of the user + # user_email - Use the email if there is no firstname or surname (defaults: true) + # + # Returns String def name(use_email = true) if (firstname.blank? && surname.blank?) || use_email then return email @@ -105,130 +189,103 @@ end end - ## - # returns all active plans for a user + # The user's identifier for the specified scheme name # - # @return [Plans] - def active_plans - self.plans.includes(:template).where("roles.active": true).where(Role.not_reviewer_condition) - end - - - ## - # Returns the user's identifier for the specified scheme name + # scheme - The identifier scheme name (e.g. ORCID) # - # @param the identifier scheme name (e.g. ORCID) - # @return [UserIdentifier] the user's identifier for that scheme + # Returns UserIdentifier def identifier_for(scheme) user_identifiers.where(identifier_scheme: scheme).first end - ## - # sets a new organisation for the user + # Checks if the user is a super admin. If the user has any privelege which requires + # them to see the super admin page then they are a super admin. # - # @param new_organisation [Organisation] the new organisation for the user - def organisation=(new_org) - org_id = new_org.id unless new_org.nil? - end - - ## - # checks if the user is a super admin - # if the user has any privelege which requires them to see the super admin page - # then they are a super admin - # - # @return [Boolean] true if the user is an admin + # Returns Boolean def can_super_admin? return self.can_add_orgs? || self.can_grant_api_to_orgs? || self.can_change_org? end - ## - # checks if the user is an organisation admin - # if the user has any privlege which requires them to see the org-admin pages - # then they are an org admin + # Checks if the user is an organisation admin if the user has any privlege which + # requires them to see the org-admin pages then they are an org admin. # - # @return [Boolean] true if the user is an organisation admin + # Returns Boolean def can_org_admin? return self.can_grant_permissions? || self.can_modify_guidance? || self.can_modify_templates? || self.can_modify_org_details? end - ## - # checks if the user can add new organisations + # Can the User add new organisations? # - # @return [Boolean] true if the user can add new organisations + # Returns Boolean def can_add_orgs? perms.include? Perm.add_orgs end - ## - # checks if the user can change their organisation affiliations + # Can the User change their organisation affiliations? # - # @return [Boolean] true if the user can change their organisation affiliations + # Returns Boolean def can_change_org? perms.include? Perm.change_affiliation end - ## - # checks if the user can grant their permissions to others + # Can the User can grant their permissions to others? # - # @return [Boolean] true if the user can grant their permissions to others + # Returns Boolean def can_grant_permissions? perms.include? Perm.grant_permissions end - ## - # checks if the user can modify organisation templates + # Can the User modify organisation templates? # - # @return [Boolean] true if the user can modify organisation templates + # Returns Boolean def can_modify_templates? self.perms.include? Perm.modify_templates end - ## - # checks if the user can modify organisation guidance + # Can the User modify organisation guidance? # - # @return [Boolean] true if the user can modify organistion guidance + # Returns Boolean def can_modify_guidance? perms.include? Perm.modify_guidance end - ## - # checks if the user can use the api + # Can the User use the API? # - # @return [Boolean] true if the user can use the api + # Returns Boolean def can_use_api? perms.include? Perm.use_api end - ## - # checks if the user can modify their org's details + # Can the User modify their org's details? # - # @return [Boolean] true if the user can modify the org's details + # Returns Boolean def can_modify_org_details? perms.include? Perm.change_org_details end - ## - # checks if the user can grant the api to organisations + # Can the User grant the api to organisations? # - # @return [Boolean] true if the user can grant api permissions to organisations + # Returns Boolean def can_grant_api_to_orgs? perms.include? Perm.grant_api end - ## - # removes the api_token from the user - # modifies the user model + # Removes the api_token from the user + # + # Returns nil + # Returns Boolean def remove_token! unless api_token.blank? update_column(:api_token, "") unless new_record? end end - ## - # generates a new token for the user unless the user already has a token. - # modifies the user's model. + # Generates a new token for the user unless the user already has a token. + # + # Returns nil + # Returns Boolean def keep_or_generate_token! if api_token.nil? || api_token.empty? self.api_token = loop do @@ -239,29 +296,14 @@ end end - ## - # Load the user based on the scheme and id provided by the Omniauth call - # -------------------------------------------------------------- - def self.from_omniauth(auth) - scheme = IdentifierScheme.find_by(name: auth.provider.downcase) - - if scheme.nil? - throw Exception.new('Unknown OAuth provider: ' + auth.provider) - else - joins(:user_identifiers).where('user_identifiers.identifier': auth.uid, - 'user_identifiers.identifier_scheme_id': scheme.id).first - end - end - - ## - # Return the user's preferences for a given base key + # The User's preferences for a given base key # - # @return [JSON] with symbols as keys + # Returns Hash def get_preferences(key) defaults = Pref.default_settings[key.to_sym] || Pref.default_settings[key.to_s] - if self.pref.present? - existing = self.pref.settings[key.to_s].deep_symbolize_keys + if pref.present? + existing = pref.settings[key.to_s].deep_symbolize_keys # Check for new preferences defaults.keys.each do |grp| @@ -278,28 +320,42 @@ end end - ## # Override devise_invitable email title - # -------------------------------------------------------------- def deliver_invitation(options = {}) super(options.merge(subject: _('A Data Management Plan in %{application_name} has been shared with you') % {application_name: Rails.configuration.branding[:application][:name]})) end - ## + # Case insensitive search over User model - # @param field [string] The name of the field being queried - # @param val [string] The string to search for, case insensitive. val is duck typed to check whether or not downcase method exist - # @return [ActiveRecord::Relation] The result of the search + # + # field - The name of the field being queried + # val - The String to search for, case insensitive. val is duck typed to check + # whether or not downcase method exist. + # + # Returns ActiveRecord::Relation + # Raises ArgumentError def self.where_case_insensitive(field, val) - User.where(field.to_sym => val.to_s.downcase) + unless columns.map(&:name).include?(field.to_s) + raise ArgumentError, "Field #{field} is not present on users table" + end + User.where("LOWER(#{field}) = :value", value: val.to_s.downcase) end - # Acknoledge a Notification - # @param notification Notification to acknowledge + # Acknowledge a Notification + # + # notification - Notification to acknowledge + # + # Returns ActiveRecord::Associations::CollectionProxy + # Returns nil def acknowledge(notification) notifications << notification if notification.dismissable? end private + + # ============================ + # = Private instance methods = + # ============================ + def when_org_changes if org_id != org_id_was unless can_change_org? diff --git a/app/models/user_identifier.rb b/app/models/user_identifier.rb index 840e394..5a1f5ad 100644 --- a/app/models/user_identifier.rb +++ b/app/models/user_identifier.rb @@ -1,9 +1,42 @@ +# == Schema Information +# +# Table name: user_identifiers +# +# id :integer not null, primary key +# identifier :string +# created_at :datetime +# updated_at :datetime +# identifier_scheme_id :integer +# user_id :integer +# +# Indexes +# +# index_user_identifiers_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (identifier_scheme_id => identifier_schemes.id) +# fk_rails_... (user_id => users.id) +# + class UserIdentifier < ActiveRecord::Base + include ValidationMessages + + # ================ + # = Associations = + # ================ + belongs_to :user belongs_to :identifier_scheme - - # Should only be able to have one identifier per scheme! - validates_uniqueness_of :identifier_scheme, scope: :user - - validates :identifier, :user, :identifier_scheme, presence: {message: _("can't be blank")} -end \ No newline at end of file + + # =============== + # = Validations = + # =============== + + validates :user, presence: true + + validates :identifier_scheme, presence: { message: PRESENCE_MESSAGE } + + validates :identifier, presence: { message: PRESENCE_MESSAGE } + +end diff --git a/app/policies/phase_policy.rb b/app/policies/phase_policy.rb index 6972fd6..834d68e 100644 --- a/app/policies/phase_policy.rb +++ b/app/policies/phase_policy.rb @@ -37,4 +37,7 @@ user.can_modify_templates? && (phase.template.org_id == user.org_id) end -end \ No newline at end of file + def sort? + user.can_modify_templates? && (phase.template.org_id == user.org_id) + end +end diff --git a/app/services/guidance_service.rb b/app/services/guidance_service.rb index 0e773d7..947c4ed 100644 --- a/app/services/guidance_service.rb +++ b/app/services/guidance_service.rb @@ -6,6 +6,7 @@ @plan = plan @guidance_groups = plan.guidance_groups.where(published: true) end + # Returns an Array of orgs according to the guidance related to plan # Note the Array is sorted in the following order: # First funder org (if the template from the plan is a customization of another) @@ -80,6 +81,36 @@ return hashified_annotations[org].select{ |annotation| (annotation.question_id == question.id) && (annotation.type == "guidance")} end + + # filters through the orgs with annotations and guidance groups to create a + # set of tabs with display names and any guidance/annotations to show + # + # question - The question to which guidance pretains + # + # Returns an array of tab hashes. These + def tablist(question) + # start with orgs + # filter into hash with annotation_presence, main_group presence, and + display_tabs = [] + orgs.each do |org| + annotations = guidance_annotations(org: org,question: question) + groups = guidance_groups_by_theme(org: org,question: question) + main_groups = groups.select{|group| group.optional_subset == false} + subsets = groups.reject{|group| group.optional_subset == false} + if annotations.present? || main_groups.present? # annotations and main group + # Tab with org.abbreviation + display_tabs << {name: org.abbreviation, groups: main_groups, + annotations: annotations} + end + if subsets.present? + subsets.each_pair do |group,theme| + display_tabs << {name: group.name.truncate(15), groups:{group => theme}} + end + end + end + return display_tabs + end + private def orgs_from_guidance_groups if !defined?(@orgs_from_guidance_groups) diff --git a/app/validators/after_validator.rb b/app/validators/after_validator.rb new file mode 100644 index 0000000..57fe579 --- /dev/null +++ b/app/validators/after_validator.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +class AfterValidator < ActiveModel::EachValidator + + DEFAULT_MESSAGE = _("must be after %{date}") + + def validate_each(record, attribute, value) + return if value.nil? + return if record.persisted? && options[:on].to_s == 'create' + return if record.new_record? && options[:on].to_s == 'update' + date = options[:date] + msg = options.fetch(:message, DEFAULT_MESSAGE % { date: options[:date] }) + record.errors.add(attribute, msg) if value.to_date < options[:date] + end +end diff --git a/app/views/answers/_new_edit.html.erb b/app/views/answers/_new_edit.html.erb index 063ce59..ca2f436 100644 --- a/app/views/answers/_new_edit.html.erb +++ b/app/views/answers/_new_edit.html.erb @@ -65,7 +65,7 @@ </fieldset> <!--Example Answer area --> <% if template.present? && template.org.present? %> - <% question.get_example_answers([base_template_org.id, template.org.id]).each do |annotation| %> + <% question.example_answers([base_template_org.id, template.org.id]).each do |annotation| %> <% if annotation.present? && annotation.org.present? && annotation.text.present? %> <div class="panel panel-default"> <span class="label label-default"> diff --git a/app/views/guidances/_guidance_display.html.erb b/app/views/guidances/_guidance_display.html.erb index b3bce3b..2e0885b 100644 --- a/app/views/guidances/_guidance_display.html.erb +++ b/app/views/guidances/_guidance_display.html.erb @@ -2,7 +2,7 @@ <div class="question-guidance"> <div class="accordion" id="<%= question.id %>-guidance"> - <% if !question.get_guidance_annotation(current_user.org).nil? && question.get_guidance_annotation(current_user.org) != "" %> + <% if !question.guidance_annotation(current_user.org).nil? && question.guidance_annotation(current_user.org) != "" %> <div class="accordion-group"> <div class="accordion-heading"> <a class="accordion-guidance-link" data-toggle="collapse" data-parent="#<%= question.id %>-guidance" href="#collapse-guidance-<%= question.id%>"> @@ -12,7 +12,7 @@ <span class="plus-laranja"> </span></a> </div> <div id="collapse-guidance-<%= question.id%>" class="guidance-accordion-body collapse"> - <div class="accordion-inner"><%= raw question.get_guidance_annotation(current_user.org).text %></div> + <div class="accordion-inner"><%= raw question.guidance_annotation(current_user.org).text %></div> </div> </div> <% end %> diff --git a/app/views/guidances/new_edit.html.erb b/app/views/guidances/new_edit.html.erb index 66e3e2c..28e3cf8 100644 --- a/app/views/guidances/new_edit.html.erb +++ b/app/views/guidances/new_edit.html.erb @@ -14,8 +14,8 @@ <%= text_area_tag("guidance-text", guidance.text, class: "form-control", 'aria-required': true, rows: 10) %> </div> <%= render partial: 'org_admin/shared/theme_selector', - locals: { f: f, all_themes: themes, as_radio: true, required: true, - popover_message: _('Select one theme that is relevant to this guidance. This will display your generic organisation-level guidance, or any Schools/Departments for which you create guidance groups, across all templates that have questions with the corresponding theme tags.') } %> + locals: { f: f, all_themes: themes, as_radio: false, required: true, + popover_message: _('Select one or more themes that are relevant to this guidance. This will display your generic organisation-level guidance, or any Schools/Departments for which you create guidance groups, across all templates that have questions with the corresponding theme tags.') } %> <div class="form-group"> <%= f.label _('Guidance group'), for: :guidance_group_id, class: 'control-label' %> <%= f.collection_select(:guidance_group_id, guidance_groups, diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 2e50441..5eff0ea 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,7 +1,7 @@ <!DOCTYPE html> <html lang="en"> <head> - <title><%= content_for?(:title) ? yield(:title) : _('%{application_name}') % { :application_name => Rails.configuration.branding[:application][:name] } %> + <title><%= content_for?(:title) ? yield(:title) : _('%{application_name}') % { :application_name => Rails.configuration.branding[:application][:name] } %> <%= favicon_link_tag "favicon.ico" %>
@@ -57,7 +57,7 @@
<%= (has_alert or has_notice) ? 'show' : 'hide' %>" role="<%= (has_notice ? 'status' : (has_alert ? 'alert' : '')) %>"> @@ -88,6 +88,7 @@ VALIDATION_MESSAGE_PASSWORDS_MATCH: _('The passwords must match.'), VALIDATION_MESSAGE_RADIO: _('Please choose one of the options.'), VALIDATION_MESSAGE_CHECKBOX: _('Please check the box to continue.'), + VALIDATION_MESSAGE_MULTI_CHECKBOX: _('Please select at least one of the options to continue.'), VALIDATION_MESSAGE_SELECT: _('Please select a value from the list.'), VALIDATION_MESSAGE_TEXT: _('This field is required.'), diff --git a/app/views/org_admin/phases/_overview.html.erb b/app/views/org_admin/phases/_overview.html.erb index bbdf21d..fde9db7 100644 --- a/app/views/org_admin/phases/_overview.html.erb +++ b/app/views/org_admin/phases/_overview.html.erb @@ -1,72 +1,6 @@

<%= _('Template Overview') %>

-<% phases.each do |phase| %> -
-
-
-
-

- <%= phase.title %> -
- <% if template.customization_of.present? && template.latest? %> - <%= link_to _('Customize phase'), org_admin_template_phase_path(template.id, phase.id), { class: "btn btn-default", role: 'button' } %> - <% elsif modifiable %> - <%= link_to _('Edit phase'), edit_org_admin_template_phase_path(template.id, phase.id), { class: "btn btn-default", role: 'button' } %> - <% else %> - <%= link_to _('Show phase'), org_admin_template_phase_path(template.id, phase.id), { class: "btn btn-default", role: 'button' } %> - <% end %> - <% if template.latest? && phase.modifiable %> - <%= link_to _('Delete phase'), org_admin_template_phase_path(template.id, phase.id), - data: { confirm: _("You are about to delete the '%{phase_title}' phase. This will remove all of the sections and questions listed below. Are you sure?") % { phase_title: phase.title }, - length: 20, omission: _('... (continued)') }, method: :delete, class: 'btn btn-default', role: 'button' %> - <% end %> -
-

-

- <%= raw phase.description %> -

-
-
-
-
-
- <% if phase.sections.length > 0 %> -
- - - - - - - - - <% phase.sections.each do |section| %> - - - - - <% end %> - -
<%= _('Sections')%><%= _('Questions')%>

<%= section.title %>

- <% if section.questions.present? %> -
    - <% section.questions.each do |question| %> -
  • - <%= raw question.text %> - <% if question.question_options.length > 0 %> -
      - <% question.question_options.each do |option| %> -
    • <%= option.text %>
    • - <% end %> -
    - <% end %> -
  • - <% end %> -
- <% end %> -
-
- <% end %> -
-
-<% end %> \ No newline at end of file +<%= render partial: "org_admin/phases/phase", + collection: phases, + locals: { template: template, + modifiable: modifiable } %> diff --git a/app/views/org_admin/phases/_phase.html.erb b/app/views/org_admin/phases/_phase.html.erb new file mode 100644 index 0000000..7571d4a --- /dev/null +++ b/app/views/org_admin/phases/_phase.html.erb @@ -0,0 +1,67 @@ +
+
+
+
+

+ <%= phase.title %> +
+ <% if template.customization_of.present? && template.latest? %> + <%= link_to _('Customize phase'), org_admin_template_phase_path(template.id, phase.id), { class: "btn btn-default", role: 'button' } %> + <% elsif modifiable %> + <%= link_to _('Edit phase'), edit_org_admin_template_phase_path(template.id, phase.id), { class: "btn btn-default", role: 'button' } %> + <% else %> + <%= link_to _('Show phase'), org_admin_template_phase_path(template.id, phase.id), { class: "btn btn-default", role: 'button' } %> + <% end %> + <% if template.latest? && phase.modifiable %> + <%= link_to _('Delete phase'), org_admin_template_phase_path(template.id, phase.id), + data: { confirm: _("You are about to delete the '%{phase_title}' phase. This will remove all of the sections and questions listed below. Are you sure?") % { phase_title: phase.title }, + length: 20, omission: _('... (continued)') }, method: :delete, class: 'btn btn-default', role: 'button' %> + <% end %> +
+

+

+ <%= raw phase.description %> +

+
+
+ +
+ <% if phase.sections.length > 0 %> +
+ + + + + + + + + <% phase.sections.each do |section| %> + + + + + <% end %> + +
<%= _('Sections')%><%= _('Questions')%>

<%= section.title %>

+ <% if section.questions.present? %> +
    + <% section.questions.each do |question| %> +
  • + <%= raw question.text %> + <% if question.question_options.length > 0 %> +
      + <% question.question_options.each do |option| %> +
    • <%= option.text %>
    • + <% end %> +
    + <% end %> +
  • + <% end %> +
+ <% end %> +
+
+ <% end %> +
+
diff --git a/app/views/org_admin/phases/container.html.erb b/app/views/org_admin/phases/container.html.erb index fed9838..767c96a 100644 --- a/app/views/org_admin/phases/container.html.erb +++ b/app/views/org_admin/phases/container.html.erb @@ -31,18 +31,40 @@

<%= _('Sections') %>

- <% if phase.sections.length > 1 %> -
- + +
+ +
+ <% if phase.sections.many? %> + + <% end %>
- <% end %> + +
+
+ + <%= _("Drag arrows to rearrange customized sections. You may place + them before or after the main template sections.") %> + +
+
+
+
- <%= render partial: 'org_admin/sections/index', locals: local_assigns.merge({ modifiable: modifiable }) %> + <%= render partial: 'org_admin/sections/index', + locals: local_assigns.merge(modifiable: modifiable) %>
diff --git a/app/views/org_admin/phases/preview.html.erb b/app/views/org_admin/phases/preview.html.erb index 19acbfd..312c7e9 100644 --- a/app/views/org_admin/phases/preview.html.erb +++ b/app/views/org_admin/phases/preview.html.erb @@ -36,7 +36,6 @@ plan: nil, phase: phase, readonly: true, - question_guidance: {}, edit: false, guidance_groups: [], base_template_org: template.base_org, diff --git a/app/views/org_admin/question_options/_option_fields.html.erb b/app/views/org_admin/question_options/_option_fields.html.erb index 63cd834..1bb60f2 100644 --- a/app/views/org_admin/question_options/_option_fields.html.erb +++ b/app/views/org_admin/question_options/_option_fields.html.erb @@ -21,7 +21,7 @@ <% options_q.number = i %>
- <%= op.number_field :number, in: 1..20, class: 'form-control' %> + <%= op.number_field :number, min: 1, class: 'form-control' %>
<%= op.text_field :text, as: :string, class: 'form-control' %> diff --git a/app/views/org_admin/questions/_show.html.erb b/app/views/org_admin/questions/_show.html.erb index 05adce1..6026969 100644 --- a/app/views/org_admin/questions/_show.html.erb +++ b/app/views/org_admin/questions/_show.html.erb @@ -38,14 +38,14 @@ <% if !question.section.phase.template.org.funder? %> - <% example_answer = question.get_example_answers(template.base_org.id).first %> + <% example_answer = question.example_answers(template.base_org.id).first %> <% if example_answer.present? && example_answer.text.present? %>
<%= _('example answer')%>
<%= raw example_answer.text %>
<% end %> <% end %> - <% guidance = question.get_guidance_annotation(template.base_org.id) %> + <% guidance = question.guidance_annotation(template.base_org.id) %> <% if guidance.present? %>
<%= _('Guidance')%>
<%= raw guidance.text %>
diff --git a/app/views/org_admin/sections/_form.html.erb b/app/views/org_admin/sections/_form.html.erb index 9c207d1..951b2ee 100644 --- a/app/views/org_admin/sections/_form.html.erb +++ b/app/views/org_admin/sections/_form.html.erb @@ -4,15 +4,6 @@ <%= f.text_field(:title, { class: "form-control", placeholder: _('Enter a title for the section'), 'data-toggle': 'tooltip', title: _('Enter a title for the section'), 'aria-required': true} ) %>
-
-
- <%= f.label(:number, _('Order of display'), class: "control-label") %> -
-
- <%= f.number_field(:number, in: 1..15, class: "form-control", 'aria-required': true, 'data-toggle': 'tooltip', title: _('This allows you to order sections.')) %> -
-
-
"> <%= f.label(:description, _('Description'), class: "control-label") %> <%= f.text_area(:description, class: "section") %> diff --git a/app/views/org_admin/sections/_index.html.erb b/app/views/org_admin/sections/_index.html.erb index 041013a..ea28203 100644 --- a/app/views/org_admin/sections/_index.html.erb +++ b/app/views/org_admin/sections/_index.html.erb @@ -1,60 +1,61 @@
-
- <% sections.each do |section| %> -
- <% href = (section.modifiable? ? edit_org_admin_template_phase_section_path(template_id: template.id, phase_id: phase.id, id: section.id) : org_admin_template_phase_section_path(template_id: template.id, phase_id: phase.id, id: section.id)) %> - -
" - class="panel-collapse collapse<%= " in" if current_section.present? && section.id == current_section.id %>" - role="tabpanel" - aria-labelledby="<%= "headingSection#{section.id}" %>"> -
- <%# This is AJAX loaded on demand unless section_id is specified in the URL %> - <% if current_section.present? && section.id == current_section.id %> - <% partial = "org_admin/sections/#{section.modifiable? ? 'edit' : 'show'}" %> - <%= render partial: "#{partial}", - locals: { - template: template, - phase: phase, - section: current_section } - %> - <% end %> -
-
-
+
+ + <%= render partial: "org_admin/sections/section_group", + locals: { sections: Array(prefix_section), + phase: phase, + template: template, + current_section: current_section, + panel_id: "section-#{prefix_section.id}", + modifiable: true } if prefix_section %> + + <%# If we are working with a modifiable phase then allow the core sections to be reordered %> + <% if phase.modifiable? %> + <% sections.each do |s| %> + <%= render partial: "org_admin/sections/section_group", + locals: { sections: Array(s), + phase: phase, + template: template, + current_section: current_section, + panel_id: "section-#{s.id}", + modifiable: s.modifiable } %> + <% end %> + <% else %> + <%= render partial: "org_admin/sections/section_group", + locals: { sections: sections, + phase: phase, + template: template, + current_section: current_section, + panel_id: "baseline-sections", + modifiable: modifiable } %> <% end %> + + <% suffix_sections.each do |s| %> + <%= render partial: "org_admin/sections/section_group", + locals: { sections: Array(s), + phase: phase, + template: template, + panel_id: "section-#{s.id}", + current_section: current_section, + modifiable: true } %> + <% end %> +
+ <% if template.latest? && (modifiable || template.customization_of.present?) %>
+
" + class="panel-collapse collapse<%= " in" if current_section.present? && section.id == current_section.id %>" + role="tabpanel" + aria-labelledby="<%= "headingSection#{section.id}" %>"> +
+ <%# This is AJAX loaded on demand unless section_id is specified in the URL %> + <% if current_section.present? && section.id == current_section.id %> + <% partial = "org_admin/sections/#{section.modifiable? ? 'edit' : 'show'}" %> + <%= render partial: "#{partial}", + locals: { + template: template, + phase: phase, + section: current_section } + %> + <% end %> +
+
+
diff --git a/app/views/org_admin/sections/_section_group.html.erb b/app/views/org_admin/sections/_section_group.html.erb new file mode 100644 index 0000000..7873f3f --- /dev/null +++ b/app/views/org_admin/sections/_section_group.html.erb @@ -0,0 +1,15 @@ + +
+ <% sections.each do |section| %> + <%= render partial: "org_admin/sections/section", + object: section, + locals: { phase: phase, + template: template, + current_section: current_section, + data_parent: panel_id, + draggable: section == sections.first } %> + <% end%> +
diff --git a/app/views/org_admin/shared/_theme_selector.html.erb b/app/views/org_admin/shared/_theme_selector.html.erb index 11fe8e7..13dd640 100644 --- a/app/views/org_admin/shared/_theme_selector.html.erb +++ b/app/views/org_admin/shared/_theme_selector.html.erb @@ -3,43 +3,41 @@ <% required ||= false %>
- <% if required %> - * - <% end %> - <%= f.label _('Themes'), for: :theme_ids, class: 'control-label' %> - <%= render partial: 'shared/popover', - locals: { message: popover_message, placement: 'right' }%> -
- <% if all_themes.length > 0 %> <% cntr = 0 nbr_of_cols = (all_themes.length.to_f / MAX_NUMBER_THEMES_PER_COLUMN.to_f).ceil col_size = (12 / (nbr_of_cols > 4 ? 3 : nbr_of_cols)).round %> -
- <% all_themes.each do |theme| %> - <% if cntr >= MAX_NUMBER_THEMES_PER_COLUMN %> +
+ + <%= _('Themes') %> + <%= render partial: 'shared/popover', + locals: { message: popover_message, placement: 'right' }%> + + +
+ <% all_themes.each do |theme| %> + <% if cntr >= MAX_NUMBER_THEMES_PER_COLUMN %> +
+
+ <% cntr = 0 %> + <% end %> +
+ <% namespace = f.object.class.name.downcase %> + <% id = f.object.id.present? ? f.object.id : 'new' %> + + value="<%= theme.id %>"<%= f.object.themes.include?(theme) ? ' checked="checked"' : '' %>> + <%= theme.title %>
-
- <% cntr = 0 %> + <% cntr += 1 %> <% end %> -
-
- <% namespace = f.object.class.name.downcase %> - <% id = f.object.id.present? ? f.object.id : 'new' %> - - value="<%= theme.id %>"<%= f.object.themes.include?(theme) ? ' checked="checked"' : '' %>> - <%= theme.title %> -
-
- <% cntr += 1 %> - <% end %> -
+
+
<% else %>

<%= _('No themes have been defined. Please contact your administrator for assistance.') %>

<% end %> diff --git a/app/views/org_admin/templates/_form.html.erb b/app/views/org_admin/templates/_form.html.erb index 81af474..a6f92e0 100644 --- a/app/views/org_admin/templates/_form.html.erb +++ b/app/views/org_admin/templates/_form.html.erb @@ -56,7 +56,7 @@ locals: { context: 'funder', title: _('Funder Links'), - links: template.links['funder'], + links: Hash(template.links).fetch('funder', []), max_number_links: MAX_NUMBER_LINKS_FUNDER, tooltip: _('Add links to funder websites that provide additional information about the requirements for this template') }) %>
@@ -65,7 +65,7 @@ locals: { context: 'sample_plan', title: _('Sample Plan Links'), - links: template.links['sample_plan'], + links: Hash(template.links).fetch('sample_plan', []), max_number_links: MAX_NUMBER_LINKS_SAMPLE_PLAN, tooltip: _('Add links to sample plans if provided by the funder.') }) %>
diff --git a/app/views/org_admin/templates/_row.html.erb b/app/views/org_admin/templates/_row.html.erb new file mode 100644 index 0000000..6605e9c --- /dev/null +++ b/app/views/org_admin/templates/_row.html.erb @@ -0,0 +1,23 @@ + + + <%= "#{template.is_default? ? '* ' : ''}#{template.title}" %> + + + <%= template.org.name %> + + + <% if template.published? %> + <%= _('Published') %> + <% elsif template.draft? %> + <% tooltip = _('This template is published changes but has unpublished changes!') %> + <%= _('Published') %> <%= tooltip %> +    + <% else %> + <%= _('Unpublished') %> + <% end %> + + + <% last_temp_updated = template.updated_at %> + <%= l last_temp_updated.to_date, formats: :short %> + + diff --git a/app/views/orgs/_feedback_form.html.erb b/app/views/orgs/_feedback_form.html.erb index 30df386..c16752e 100644 --- a/app/views/orgs/_feedback_form.html.erb +++ b/app/views/orgs/_feedback_form.html.erb @@ -8,19 +8,14 @@
-

<%= _('Request Expert Feedback - Automated Email:') %>

+

+ <%= _('Request Expert Feedback - Message Displayed on Share Plan Tab:') %> +

-
- <%= f.label :feedback_email_subject, _('Subject'), class: "control-label" %> - - <%= f.text_field :feedback_email_subject, class: "form-control", placeholder: feedback_confirmation_default_subject.gsub('%{application_name}', Rails.configuration.branding[:application][:name]) %> -
-
-
- <% tip = feedback_confirmation_default_message.gsub('%{organisation_email}', org.contact_email.present? ? org.contact_email : '%{organisation_email}') %> -
+
<%= f.label :feedback_email_msg, _('Message'), class: "control-label" %> - <%= f.text_area :feedback_email_msg, class: "form-control" %> + <%= f.text_area :feedback_email_msg, class: "form-control", + "aria-required" => true %>
diff --git a/app/views/paginable/plans/_privately_visible.html.erb b/app/views/paginable/plans/_privately_visible.html.erb index 4ba2026..3068da3 100644 --- a/app/views/paginable/plans/_privately_visible.html.erb +++ b/app/views/paginable/plans/_privately_visible.html.erb @@ -38,7 +38,7 @@ <%= plan.visibility === 'is_test' ? _('N/A') : raw(display_visibility(plan.visibility)) %> - <% if plan.shared %> + <% if plan.shared? %> <%= _("Yes") %> <% else %> <%= _('No') %> @@ -55,7 +55,7 @@
<% if guidances_active %> -
- -
- <% guidance_service.orgs.each_with_index do |org, i| %> - <% if guidance_service.any?(org: org, question: question) %> -
" role="tabpanel" class="tab-pane <%= active_nav == org.id ? 'active' : '' %>"> -
-
- <% guidance_service.guidance_annotations(org: org, question: question).each do |annotation| %> - <%= - render partial: 'org_admin/annotations/show', locals: { - template: template, - example_answer: nil, - guidance: annotation, - for_plan: true } - %> - <% end %> - <% if guidance_service.guidance_annotations?(org: org, question: question) && - guidance_service.guidance_groups_by_theme?(org: org, question: question) %> -
- <% end %> - <% if guidance_service.guidance_groups_by_theme?(org: org, question: question) %> - <%= render partial: 'guidance_groups/index_by_theme', - locals: { guidance_groups_by_theme: guidance_service.guidance_groups_by_theme(org: org, question: question) } %> - <% end %> + <% tablist = guidance_service.tablist(question) %> +
+ +
+ <% tablist.each_with_index do |tab, i| %> +
" role="tabpanel" class="tab-pane <%= active_nav == tab[:name] ? 'active' : '' %>"> +
+
+ <% if tab[:annotations].present? %> + <% tab[:annotations].each do |annotation| %> + <%= + render partial: 'org_admin/annotations/show', locals: { + template: template, + example_answer: nil, + guidance: annotation, + for_plan: true } + %> + <% end %> + <% if tab[:groups].present? %> +
+ <% end %> + <% end %> + <% if tab[:groups].present? %> + <%= render partial: 'guidance_groups/index_by_theme', + locals: { guidance_groups_by_theme: tab[:groups] } %> + <% end %> +
-
<% end %> - <% end %> +
-
<% end %> <% if plan.present? %> diff --git a/app/views/phases/_overview.html.erb b/app/views/phases/_overview.html.erb index f216c85..f49ea31 100644 --- a/app/views/phases/_overview.html.erb +++ b/app/views/phases/_overview.html.erb @@ -1,5 +1,5 @@ <%# locals: { phase } %> -
+

<%= _('Instructions') %> @@ -9,8 +9,7 @@ <%= raw(phase.description) %>

-
-
+
    <% phase.sections.each do |s| %> @@ -25,4 +24,4 @@ <% end %>
-
\ No newline at end of file +
diff --git a/app/views/phases/edit.html.erb b/app/views/phases/edit.html.erb index 046de4a..f6decf0 100644 --- a/app/views/phases/edit.html.erb +++ b/app/views/phases/edit.html.erb @@ -1,8 +1,7 @@ -<%# locals: { plan, phase, readonly, question_guidance } %> <% title "#{plan.title} - Write plan" %>
-

<%= plan.title %>

+

<%= plan.title %>

diff --git a/app/views/plans/_download_form.html.erb b/app/views/plans/_download_form.html.erb index 296e2e0..2e19d6e 100644 --- a/app/views/plans/_download_form.html.erb +++ b/app/views/plans/_download_form.html.erb @@ -30,7 +30,7 @@

<%= _('Format') %>

- <%= select_tag :format, options_for_select(ExportedPlan::VALID_FORMATS, :pdf), + <%= select_tag :format, options_for_select(Settings::Template::VALID_FORMATS, :pdf), class: 'form-control', "aria-labelledby": "format" %>
@@ -48,54 +48,54 @@
<%= label_tag "export[formatting][font_face]", _('Face'), class: 'control-label' %> - <%= select_tag "export[formatting][font_face]", - options_for_select(Settings::Template::VALID_FONT_FACES, - @export_settings.formatting[:font_face]), - class: 'form-control', + <%= select_tag "export[formatting][font_face]", + options_for_select(Settings::Template::VALID_FONT_FACES, + @export_settings.formatting[:font_face]), + class: 'form-control', "data-default": @plan.template.settings(:export).formatting[:font_face] %>
<%= label_tag "export[formatting][font_size]", _('Size') + " (pt)", class: 'control-label' %> - <%= select_tag "export[formatting][font_size]", + <%= select_tag "export[formatting][font_size]", options_for_select(Settings::Template::VALID_FONT_SIZE_RANGE.to_a, @export_settings.formatting[:font_size]), - class: 'form-control', + class: 'form-control', "data-default": @plan.template.settings(:export).formatting[:font_size] %>
- <%= label_tag "export[formatting][margin][top]", _('Top'), + <%= label_tag "export[formatting][margin][top]", _('Top'), class: 'control-label' %> - <%= select_tag "export[formatting][margin][top]", - options_for_select(Settings::Template::VALID_MARGIN_RANGE.to_a, - @export_settings.formatting[:margin][:top]), - class: 'form-control', + <%= select_tag "export[formatting][margin][top]", + options_for_select(Settings::Template::VALID_MARGIN_RANGE.to_a, + @export_settings.formatting[:margin][:top]), + class: 'form-control', "data-default": @plan.template.settings(:export).formatting[:margin][:top] %>
- <%= label_tag "export[formatting][margin][bottom]", _('Bottom'), + <%= label_tag "export[formatting][margin][bottom]", _('Bottom'), class: 'control-label' %> - <%= select_tag "export[formatting][margin][bottom]", - options_for_select(Settings::Template::VALID_MARGIN_RANGE.to_a, - @export_settings.formatting[:margin][:bottom]), - class: 'form-control', + <%= select_tag "export[formatting][margin][bottom]", + options_for_select(Settings::Template::VALID_MARGIN_RANGE.to_a, + @export_settings.formatting[:margin][:bottom]), + class: 'form-control', "data-default": @plan.template.settings(:export).formatting[:margin][:bottom] %>
- <%= label_tag "export[formatting][margin][left]", _('Left'), + <%= label_tag "export[formatting][margin][left]", _('Left'), class: 'control-label' %> - <%= select_tag "export[formatting][margin][left]", - options_for_select(Settings::Template::VALID_MARGIN_RANGE.to_a, - @export_settings.formatting[:margin][:left]), - class: 'form-control', + <%= select_tag "export[formatting][margin][left]", + options_for_select(Settings::Template::VALID_MARGIN_RANGE.to_a, + @export_settings.formatting[:margin][:left]), + class: 'form-control', "data-default": @plan.template.settings(:export).formatting[:margin][:left] %>
- <%= label_tag "export[formatting][margin][right]", _('Right'), + <%= label_tag "export[formatting][margin][right]", _('Right'), class: 'control-label' %> - <%= select_tag "export[formatting][margin][right]", - options_for_select(Settings::Template::VALID_MARGIN_RANGE.to_a, - @export_settings.formatting[:margin][:right]), - class: 'form-control', + <%= select_tag "export[formatting][margin][right]", + options_for_select(Settings::Template::VALID_MARGIN_RANGE.to_a, + @export_settings.formatting[:margin][:right]), + class: 'form-control', "data-default": @plan.template.settings(:export).formatting[:margin][:rigth] %>
diff --git a/app/views/plans/_share_form.html.erb b/app/views/plans/_share_form.html.erb index 9caf694..f216bb5 100644 --- a/app/views/plans/_share_form.html.erb +++ b/app/views/plans/_share_form.html.erb @@ -41,7 +41,7 @@ <%= role.user.name %> <% if role.creator? %> - <%= _('Owner') %> + <%= _('Owner') %> <% else %> <% if administerable && role.user != current_user %> <%= form_for role, url: { controller: :roles, action: :update, id: role.id }, remote: true, html: { method: :put } do |f| %> @@ -78,7 +78,7 @@ <%= user.email_field :email, for: :user, name: "user", class: "form-control", "aria-required": true %> <% end %>
- +
<%= _('Permissions') %>
@@ -102,10 +102,13 @@ <% if plan.owner_and_coowners.include?(current_user) && current_user.org.present? && current_user.org.feedback_enabled? %>

<%= _('Request expert feedback') %>

<%= _('Click below to give data management staff at your organisation access to read and comment on your plan.') %>

+
+ <%= current_user.org.feedback_email_msg.to_s.html_safe %> +

<%= _('You can continue to edit and download the plan in the interim.') %>

<%= link_to _('Request feedback'), request_feedback_plan_path, class: "btn btn-default#{' disabled' if @plan.feedback_requested?}" %> <%= _("Feedback has been requested.") if @plan.feedback_requested? %>
-<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/shared/_create_account_form.html.erb b/app/views/shared/_create_account_form.html.erb index e21ab0d..1c10cb6 100644 --- a/app/views/shared/_create_account_form.html.erb +++ b/app/views/shared/_create_account_form.html.erb @@ -15,7 +15,7 @@
<%= render partial: "shared/my_org", locals: {f: f, default_org: @default_org, - orgs: Org.where("parent_id IS NULL AND is_other IS NULL").order("sort_name ASC, name ASC"), + orgs: Org.where("is_other IS NULL").order("sort_name ASC, name ASC"), allow_other_orgs: true, required: false} %>
@@ -23,15 +23,18 @@ <%= f.label(:password, _('Password'), class: "control-label") %> <%= f.password_field(:password, class: "form-control", "aria-required": true) %>
-
- +
+
+ +
-
- <%= f.label(:accept_terms, - raw("#{ f.check_box(:accept_terms, "aria-required": true, "data-validation-error": _('You must agree to the term and conditions.')) } #{_('I accept the')} #{_('terms and conditions')}")) %> +
+
+ <%= f.label(:accept_terms, + raw("#{ f.check_box(:accept_terms, "aria-required": true, "data-validation-error": _('You must agree to the term and conditions.')) } #{_('I accept the')} #{_('terms and conditions')}")) %> +
- <%= f.button(_('Create account'), class: "btn btn-default", type: "submit") %> <% end %> diff --git a/app/views/shared/export/_plan.erb b/app/views/shared/export/_plan.erb index d6db153..b830166 100644 --- a/app/views/shared/export/_plan.erb +++ b/app/views/shared/export/_plan.erb @@ -32,55 +32,57 @@ <% end %> <% @hash[:phases].each do |phase| %> -
- -

<%= (@hash[:phases].length > 1 ? "#{@plan.title} - #{phase[:title]}" : @plan.title) %>

-
- <% phase[:sections].each do |section| %> - <% if @show_sections_questions %> -

<%= section[:title] %>

- <% end %> + <%# Only render selected phase %> + <% if phase[:title] == @selected_phase.title %> +
+

<%= download_plan_page_title(@plan, phase, @hash) %>

+
+ <% phase[:sections].each do |section| %> + <% if display_questions_and_section_headings(section, @show_sections_questions, @show_custom_sections) %> +

<%= section[:title] %>

- <% section[:questions].each do |question| %> -
- <% if @show_sections_questions && !@public_plan %> -

<%= raw question[:text].gsub(/(\s||<\/td>| )*(<\/tr>|)/,"") if question[:text].present?%>

-
- <% end %> - <% answer = @plan.answer(question[:id], false) %> - <% blank = answer.present? ? answer.is_blank? : true %> - <% options = answer.present? ? answer.question_options : [] %> - <%# case where question has not been answered sufficiently to display%> - <% if @show_unanswered && (answer.blank? || (options.blank? && blank))%> -

<%= _('Question not answered.') -%>

- <% else %> - <%# case where Question has options %> - <% if options.present?%> -
    - <% options.each do |opt| %> -
  • <%= opt.text %>
  • - <% end %> -
- <% end %> - <%# case for RDA answer display %> - <% if question[:format].rda_metadata? && !blank %> - <% ah = answer.answer_hash %> - <% if ah['standards'].present? %> -
    - <% ah['standards'].each do |id, title| %> -
  • <%= title %>
  • - <% end %> -
+ <% section[:questions].each do |question| %> +
+ <% if !@public_plan %> +

<%= raw question[:text].gsub(/(\s||<\/td>| )*(<\/tr>|)/,"") if question[:text].present?%>

+
<% end %> -

<%= raw ah['text'] %>

- <%# case for displaying comments OR text %> - <% elsif !blank %> -

<%= raw answer.text %>

- <% end %> + <% answer = @plan.answer(question[:id], false) %> + <% blank = answer.present? ? answer.is_blank? : true %> + <% options = answer.present? ? answer.question_options : [] %> + <%# case where question has not been answered sufficiently to display%> + <% if @show_unanswered && (answer.blank? || (options.blank? && blank))%> +

<%= _('Question not answered.') -%>

+ <% else %> + <%# case where Question has options %> + <% if options.any? %> +
    + <% options.each do |opt| %> +
  • <%= opt.text %>
  • + <% end %> +
+ <% end %> + <%# case for RDA answer display %> + <% if question[:format].rda_metadata? && !blank %> + <% ah = answer.answer_hash %> + <% if ah['standards'].present? %> +
    + <% ah['standards'].each do |id, title| %> +
  • <%= title %>
  • + <% end %> +
+ <% end %> +

<%= raw ah['text'] %>

+ <%# case for displaying comments OR text %> + <% elsif !blank %> +

<%= raw answer.text %>

+ <% end %> + <% end %> +
<% end %> -
- <% end %> - <% end %> + <% end %> + <% end %> + <% end %> <% end %> diff --git a/app/views/shared/export/_plan_txt.erb b/app/views/shared/export/_plan_txt.erb index 6b06132..d2be1c2 100644 --- a/app/views/shared/export/_plan_txt.erb +++ b/app/views/shared/export/_plan_txt.erb @@ -22,13 +22,14 @@ <% end %> <% @hash[:phases].each do |phase| %> -<%= (@hash[:phases].length > 1 ? "#{phase[:title]}" : "") %> +<%# Only render selected phase %> +<% if phase[:title] == @selected_phase.title %> + <%= (@hash[:phases].length > 1 ? "#{phase[:title]}" : "") %> <% phase[:sections].each do |section| %> - <% if @show_sections_questions %> + <% if display_questions_and_section_headings(section, @show_sections_questions, @show_custom_sections) %> <%= "#{section[:title]}\n" %> - <% end %> - <% section[:questions].each do |question| %> - <% if @show_sections_questions %> + + <% section[:questions].each do |question| %> <%# text in this case is an array to accomodate for option_based %> <% if question[:text].respond_to?(:each) %> <% question[:text].each do |txt| %> @@ -37,22 +38,22 @@ <% else %> <%= "#{strip_tags(question[:text][0].gsub(/(\s||<\/td>| )*(<\/tr>|)/,""))}\n" if question[:text].present? && question[:text][0].present? %> <% end %> - <% end %> - <% answer = @plan.answer(question[:id], false) %> - <% blank = (answer.present? && answer.is_valid?) ? answer.text.gsub(/<\/?p>/, '').gsub(//, '\n').chomp.blank? : true %> - <% if blank && @show_unanswered %> + <% answer = @plan.answer(question[:id], false) %> + <% blank = (answer.present? && answer.is_valid?) ? answer.text.gsub(/<\/?p>/, '').gsub(//, '\n').chomp.blank? : true %> + <% if blank && @show_unanswered %> <%= " #{_("Question not answered.")}\n\n" %> - <% elsif !blank %> - <% if answer.question_options.length > 0 %> - <% answer.question_options.each do |opt| %> + <% elsif !blank %> + <% if answer.question_options.length > 0 %> + <% answer.question_options.each do |opt| %> <%= " #{opt.text}\n" %> + <% end %> + <% end %> +<%= " #{strip_tags(answer.text.gsub(/<\/?p>/, '').gsub(//, '\n').chomp)}\n\n" if answer.text.present? %> <% end %> <% end %> -<%= " #{strip_tags(answer.text.gsub(/<\/?p>/, '').gsub(//, '\n').chomp)}\n\n" if answer.text.present? %> <% end %> <% end %> <% end %> <% end %> - <%= "----------------------------------------------------------" %> <%= _("A Data Management Plan created using %{application_name}") % { application_name: Rails.configuration.branding[:application][:name] } %> \ No newline at end of file diff --git a/app/views/user_mailer/permissions_change_notification.html.erb b/app/views/user_mailer/permissions_change_notification.html.erb index d9553fe..db036b4 100644 --- a/app/views/user_mailer/permissions_change_notification.html.erb +++ b/app/views/user_mailer/permissions_change_notification.html.erb @@ -4,14 +4,18 @@ plan_title = @role.plan.title access = nil access_level = @role.access_level() - access_level_messages = Role.access_level_messages %> <% FastGettext.with_locale FastGettext.default_locale do %>

<%= _('Hello %{username}') %{ :username => username } %>

- <%= _('Your permissions relating to %{plan_title} have changed. You now have %{type} access. This means you can %{placeholder1} %{placeholder2}') %{ :plan_title => plan_title, :type => access_level_messages[access_level][:type], :placeholder1 => access_level_messages[access_level][:placeholder1], :placeholder2 => access_level_messages[access_level][:placeholder2] } %> + <%= _('Your permissions relating to %{plan_title} have changed. You now have %{type} access. This means you can %{placeholder1} %{placeholder2}') % { + plan_title: plan_title, + type: Role::ACCESS_LEVEL_MESSAGES[access_level][:type], + placeholder1: Role::ACCESS_LEVEL_MESSAGES[access_level][:placeholder1], + placeholder2: Role::ACCESS_LEVEL_MESSAGES[access_level][:placeholder2] + } %>

<%= render partial: 'email_signature' %> <% end %> diff --git a/app/views/user_mailer/plan_visibility.html.erb b/app/views/user_mailer/plan_visibility.html.erb index 7c63eb1..8d0ac01 100644 --- a/app/views/user_mailer/plan_visibility.html.erb +++ b/app/views/user_mailer/plan_visibility.html.erb @@ -2,7 +2,7 @@ tool_name = Rails.configuration.branding[:application][:name] username = @user.name plan_title = @plan.title - plan_visibility = Plan.visibility_message(@plan.visibility.to_sym) + plan_visibility = Plan::VISIBILITY_MESSAGE[@plan.visibility.to_sym] %> <% FastGettext.with_locale FastGettext.default_locale do %>

diff --git a/bin/_guard-core b/bin/_guard-core new file mode 100755 index 0000000..cd565c3 --- /dev/null +++ b/bin/_guard-core @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application '_guard-core' 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("guard", "_guard-core") diff --git a/bin/bundle b/bin/bundle index 66e9889..524dfd3 100755 --- a/bin/bundle +++ b/bin/bundle @@ -1,3 +1,105 @@ #!/usr/bin/env ruby -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) -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 || ">= 0.a" + 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 ||= begin + env_var_version || cli_arg_version || + lockfile_version || "#{Gem::Requirement.default}.a" + end + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + # must dup string for RG < 1.8 compatibility + activate_bundler(bundler_version.dup) + end + + def activate_bundler(bundler_version) + if Gem::Version.correct?(bundler_version) && Gem::Version.new(bundler_version).release < Gem::Version.new("2.0") + bundler_version = "< 2" + end + gem_error = activation_error_handling do + gem "bundler", bundler_version + 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_version).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_version}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_version}'`" + 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/guard b/bin/guard new file mode 100755 index 0000000..bcb966f --- /dev/null +++ b/bin/guard @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'guard' 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("guard", "guard") diff --git a/bin/rspec b/bin/rspec new file mode 100755 index 0000000..6e67092 --- /dev/null +++ b/bin/rspec @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +begin + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') +end +require 'bundler/setup' +load Gem.bin_path('rspec-core', 'rspec') diff --git a/bin/rubocop_changed b/bin/rubocop_changed new file mode 100755 index 0000000..08c8e49 --- /dev/null +++ b/bin/rubocop_changed @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +# Which branch should we compare HEAD with for changed files? (defaults: 'development') +BRANCH='development' +DIRS='app lib' + +while getopts 'b:d:' optname +do + case "$optname" in + "b") + BRANCH=$OPTARG + ;; + "d") + DIRS=$OPTARG + ;; + esac +done + +# Get a list of all files that have changed on this branch in app and lib directories. +CHANGED_FILES=$(git diff --name-only $BRANCH -- $DIRS) + +# A string with the name of each changed file +EXISTING_FILES="" + +# Iterate over each changed file +for FILEPATH in $CHANGED_FILES +do + # Append this filename if the file still exists (in case the file has been deleted) + if [ -f $FILEPATH ]; then + EXISTING_FILES+=" $FILEPATH" + fi +done + +# If there are no files that have been changed... +if [ -z "$EXISTING_FILES" ] +then + # Print a message and exit + echo "Rubocop changed: No matching files have changed." + exit 0 +else + # Run Rubocop against the files that have chagned + rubocop -p $EXISTING_FILES +fi diff --git a/bin/setup b/bin/setup index acdb2c1..88bd345 100755 --- a/bin/setup +++ b/bin/setup @@ -1,5 +1,8 @@ #!/usr/bin/env ruby +# frozen_string_literal: true + require 'pathname' +require 'fileutils' # path to your application root. APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) @@ -7,23 +10,32 @@ Dir.chdir APP_ROOT do # This script is a starting point to setup your application. # Add necessary setup steps to this file: - puts "== Installing dependencies ==" system "gem install bundler --conservative" system "bundle check || bundle install" - # puts "\n== Copying sample files ==" - # unless File.exist?("config/database.yml") - # system "cp config/database.yml.sample config/database.yml" - # end + puts "== Copying config files ==" + system("cp -n config/database_example.yml config/database.yml") + system("cp -n config/secrets_example.yml config/secrets.yml") + system("cp -n config/branding_example.yml config/branding.yml") + system("cp -n config/initializers/devise.rb.example "\ + "config/initializers/devise.rb") + system("cp -n config/initializers/recaptcha.rb.example "\ + "config/initializers/recaptcha.rb") + system("cp -n config/initializers/wicked_pdf.rb.example "\ + "config/initializers/wicked_pdf.rb") + puts "\n== Preparing database ==" - system "bin/rake db:setup" + system "bundle exec rake db:create" + system "bundle exec rake db:schema:load" puts "\n== Removing old logs and tempfiles ==" - system "rm -f log/*" - system "rm -rf tmp/cache" + FileUtils.rm_f("./log/*") + FileUtils.rm_rf("./tmp/cache") puts "\n== Restarting application server ==" system "touch tmp/restart.txt" end + + diff --git a/bin/spring b/bin/spring new file mode 100755 index 0000000..fb2ec2e --- /dev/null +++ b/bin/spring @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby + +# This file loads spring without using Bundler, in order to be fast. +# It gets overwritten when you run the `spring binstub` command. + +unless defined?(Spring) + require 'rubygems' + require 'bundler' + + lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) + spring = lockfile.specs.detect { |spec| spec.name == "spring" } + if spring + Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path + gem 'spring', spring.version + require 'spring/binstub' + end +end diff --git a/config/application.rb b/config/application.rb index e09c0ef..2f19684 100644 --- a/config/application.rb +++ b/config/application.rb @@ -19,6 +19,19 @@ module DMPRoadmap class Application < Rails::Application + + config.generators do |g| + g.orm :active_record + g.template_engine :erb + g.test_framework :rspec + g.javascripts false + g.stylesheets false + g.skip_routes true + g.view_specs false + g.helper_specs false + g.controller_specs false + end + # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. @@ -30,7 +43,7 @@ # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] # config.i18n.default_locale = :de - + # Configure the default encoding used in templates for Ruby 1.9. config.encoding = "utf-8" @@ -39,8 +52,7 @@ # Enable escaping HTML in JSON. config.active_support.escape_html_entities_in_json = true - - config.eager_load_paths << "app/models/scopes" + config.eager_load_paths << "app/services" # Use SQL instead of Active Record's schema dumper when creating the database. @@ -52,7 +64,7 @@ # This will create an empty whitelist of attributes available for mass-assignment for all models # in your app. As such, your models will need to explicitly whitelist or blacklist accessible # parameters by using an attr_accessible or attr_protected declaration. - #config.active_record.whitelist_attributes = true + #config.active_record.whitelist_attributes = true config.autoload_paths += %W(#{config.root}/lib) config.action_controller.include_all_helpers = true @@ -83,7 +95,7 @@ # Load Branded terminology (e.g. organization name, application name, etc.) config.branding = config_for(:branding).deep_symbolize_keys - + # The default visibility setting for new plans # organisationally_visible - Any member of the user's org can view, export and duplicate the plan # publicly_visibile - (NOT advisable because plans will show up in Public DMPs page by default) diff --git a/config/brakeman.ignore b/config/brakeman.ignore new file mode 100644 index 0000000..ae8ddb0 --- /dev/null +++ b/config/brakeman.ignore @@ -0,0 +1,296 @@ +{ + "ignored_warnings": [ + { + "warning_type": "Cross-Site Scripting", + "warning_code": 2, + "fingerprint": "10f48f3b8b0b9b24f2d1258d017123dc31ac1c28d3842a589d62ea15c5dffb06", + "check_name": "CrossSiteScripting", + "message": "Unescaped model attribute", + "file": "app/views/shared/export/_plan.erb", + "line": 75, + "link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting", + "code": "Plan.includes(:answers).find(params[:id]).answer(question[:id], false).answer_hash[\"text\"]", + "render_path": [{"type":"controller","class":"PlansController","method":"export","line":277,"file":"app/controllers/plans_controller.rb"},{"type":"template","name":"plans/export","line":2,"file":"app/views/plans/export.erb"}], + "location": { + "type": "template", + "template": "shared/export/_plan" + }, + "user_input": null, + "confidence": "High", + "note": "" + }, + { + "warning_type": "Cross-Site Scripting", + "warning_code": 2, + "fingerprint": "162c981ef989558c8e8b7a5cbdbc105837680ba4d2be60ae58242ad18b171ce2", + "check_name": "CrossSiteScripting", + "message": "Unescaped model attribute", + "file": "app/views/plans/_share_form.html.erb", + "line": 105, + "link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting", + "code": "current_user.org.feedback_email_msg.to_s", + "render_path": null, + "location": { + "type": "template", + "template": "plans/_share_form" + }, + "user_input": "current_user.org.feedback_email_msg", + "confidence": "Weak", + "note": "" + }, + { + "warning_type": "Cross-Site Scripting", + "warning_code": 2, + "fingerprint": "181f32bb1f44117835a889acb8f01d807e2ee5485d0503352f7dee356c39a224", + "check_name": "CrossSiteScripting", + "message": "Unescaped model attribute", + "file": "app/views/orgs/shibboleth_ds.html.erb", + "line": 17, + "link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting", + "code": "Org.joins(:identifier_schemes).where(\"identifier_schemes.name = ?\", \"shibboleth\").sort do\n (x.name <=> y.name)\n end.collect do\n \"\"\n end.join(\"\")", + "render_path": [{"type":"controller","class":"OrgsController","method":"shibboleth_ds","line":72,"file":"app/controllers/orgs_controller.rb"}], + "location": { + "type": "template", + "template": "orgs/shibboleth_ds" + }, + "user_input": "Org.joins(:identifier_schemes).where(\"identifier_schemes.name = ?\", \"shibboleth\")", + "confidence": "Weak", + "note": "" + }, + { + "warning_type": "Cross-Site Scripting", + "warning_code": 2, + "fingerprint": "33dc7682a340f04357c5f41cdd038533b3fa36cee21396faab6c5078e7b8325b", + "check_name": "CrossSiteScripting", + "message": "Unescaped model attribute", + "file": "app/views/org_admin/questions/_show.html.erb", + "line": 51, + "link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting", + "code": "Question.includes(:annotations, :question_options, :section => ({ :phase => :template })).find(params[:id]).guidance_annotation(Question.includes(:annotations, :question_options, :section => ({ :phase => :template })).find(params[:id]).section.phase.template.base_org.id).text", + "render_path": [{"type":"controller","class":"OrgAdmin::QuestionsController","method":"show","line":12,"file":"app/controllers/org_admin/questions_controller.rb"}], + "location": { + "type": "template", + "template": "org_admin/questions/_show" + }, + "user_input": null, + "confidence": "High", + "note": "" + }, + { + "warning_type": "Cross-Site Scripting", + "warning_code": 2, + "fingerprint": "34a8618e72614b866bb7b2d92406bb09ffb46f021d5ff6622c475730af607cb9", + "check_name": "CrossSiteScripting", + "message": "Unescaped model attribute", + "file": "app/views/org_admin/questions/_show.html.erb", + "line": 44, + "link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting", + "code": "Question.includes(:annotations, :question_options, :section => ({ :phase => :template })).find(params[:id]).example_answers(Question.includes(:annotations, :question_options, :section => ({ :phase => :template })).find(params[:id]).section.phase.template.base_org.id).first.text", + "render_path": [{"type":"controller","class":"OrgAdmin::QuestionsController","method":"show","line":12,"file":"app/controllers/org_admin/questions_controller.rb"}], + "location": { + "type": "template", + "template": "org_admin/questions/_show" + }, + "user_input": null, + "confidence": "High", + "note": "" + }, + { + "warning_type": "Redirect", + "warning_code": 18, + "fingerprint": "3ea917c822b3e5b1dad1e672ba4a40c0e8e37abf8cea9cf5793772942aa07f99", + "check_name": "Redirect", + "message": "Possible unprotected redirect", + "file": "app/controllers/plans_controller.rb", + "line": 302, + "link": "https://brakemanscanner.org/docs/warning_types/redirect/", + "code": "redirect_to(Plan.deep_copy(Plan.find(params[:id])), :notice => success_message(_(\"plan\"), _(\"copied\")))", + "render_path": null, + "location": { + "type": "method", + "class": "PlansController", + "method": "duplicate" + }, + "user_input": "Plan.deep_copy(Plan.find(params[:id]))", + "confidence": "High", + "note": "" + }, + { + "warning_type": "Redirect", + "warning_code": 18, + "fingerprint": "715556db27ab9050c36a6e9db8f6a79a2ec53bd24bcfc15a967e9e745f357245", + "check_name": "Redirect", + "message": "Possible unprotected redirect", + "file": "app/controllers/orgs_controller.rb", + "line": 92, + "link": "https://brakemanscanner.org/docs/warning_types/redirect/", + "code": "redirect_to(\"#{\"#{request.base_url.gsub(\"http:\", \"https:\")}#{Rails.application.config.shibboleth_login}\"}?target=#{\"#{user_shibboleth_omniauth_callback_url.gsub(\"http:\", \"https:\")}\"}&entityID=#{OrgIdentifier.where(:org_id => params[\"shib-ds\"][:org_id], :identifier_scheme => IdentifierScheme.find_by(:name => \"shibboleth\")).first.identifier}\")", + "render_path": null, + "location": { + "type": "method", + "class": "OrgsController", + "method": "shibboleth_ds_passthru" + }, + "user_input": "OrgIdentifier.where(:org_id => params[\"shib-ds\"][:org_id], :identifier_scheme => IdentifierScheme.find_by(:name => \"shibboleth\")).first.identifier", + "confidence": "High", + "note": "" + }, + { + "warning_type": "Cross-Site Scripting", + "warning_code": 2, + "fingerprint": "764c88db5352f612aea973695ee0a62134815f518a5453081dc6d5f6b28baa81", + "check_name": "CrossSiteScripting", + "message": "Unescaped model attribute", + "file": "app/views/org_admin/questions/_show.html.erb", + "line": 16, + "link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting", + "code": "Question.includes(:annotations, :question_options, :section => ({ :phase => :template })).find(params[:id]).text", + "render_path": [{"type":"controller","class":"OrgAdmin::QuestionsController","method":"show","line":12,"file":"app/controllers/org_admin/questions_controller.rb"}], + "location": { + "type": "template", + "template": "org_admin/questions/_show" + }, + "user_input": null, + "confidence": "High", + "note": "" + }, + { + "warning_type": "SQL Injection", + "warning_code": 0, + "fingerprint": "7bd7ecdde88008eac29303c8c264366d1d670eb468e316c17a6121d4684b8bca", + "check_name": "SQL", + "message": "Possible SQL injection", + "file": "app/models/user.rb", + "line": 348, + "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", + "code": "User.where(\"LOWER(#{field}) = :value\", :value => val.to_s.downcase)", + "render_path": null, + "location": { + "type": "method", + "class": "User", + "method": "User.where_case_insensitive" + }, + "user_input": "field", + "confidence": "Medium", + "note": "" + }, + { + "warning_type": "Cross-Site Scripting", + "warning_code": 2, + "fingerprint": "947cb537b07e43881f0e836cb4afee491a165679350690728e0400b3b523f444", + "check_name": "CrossSiteScripting", + "message": "Unescaped model attribute", + "file": "app/views/shared/export/_plan_coversheet.erb", + "line": 28, + "link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting", + "code": "Plan.includes(:answers).find(params[:id]).description", + "render_path": [{"type":"controller","class":"PlansController","method":"export","line":277,"file":"app/controllers/plans_controller.rb"},{"type":"template","name":"plans/export","line":2,"file":"app/views/plans/export.erb"},{"type":"template","name":"shared/export/_plan","line":31,"file":"app/views/shared/export/_plan.erb"}], + "location": { + "type": "template", + "template": "shared/export/_plan_coversheet" + }, + "user_input": null, + "confidence": "High", + "note": "" + }, + { + "warning_type": "Redirect", + "warning_code": 18, + "fingerprint": "9af8ff997f5587d8fa01550ea532d84fdf6b0095d892343d4431945ced6c09da", + "check_name": "Redirect", + "message": "Possible unprotected redirect", + "file": "app/controllers/splash_logs_controller.rb", + "line": 14, + "link": "https://brakemanscanner.org/docs/warning_types/redirect/", + "code": "redirect_to(params[:destination])", + "render_path": null, + "location": { + "type": "method", + "class": "SplashLogsController", + "method": "create" + }, + "user_input": "params[:destination]", + "confidence": "High", + "note": "" + }, + { + "warning_type": "Cross-Site Scripting", + "warning_code": 2, + "fingerprint": "a21e892094c534b9931877dd3b7c9ae2a87171b9469be761d2364c54aaa81541", + "check_name": "CrossSiteScripting", + "message": "Unescaped model attribute", + "file": "app/views/org_admin/sections/_show.html.erb", + "line": 4, + "link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting", + "code": "Section.includes(:questions => ([:annotations, :question_options])).find(params[:id]).description", + "render_path": [{"type":"controller","class":"OrgAdmin::SectionsController","method":"show","line":31,"file":"app/controllers/org_admin/sections_controller.rb"}], + "location": { + "type": "template", + "template": "org_admin/sections/_show" + }, + "user_input": null, + "confidence": "High", + "note": "" + }, + { + "warning_type": "SQL Injection", + "warning_code": 0, + "fingerprint": "a2f11c8d81b0932f4fe0de989fc8bb0e689cbbfdc26fddc2b1a13177ba62c1b5", + "check_name": "SQL", + "message": "Possible SQL injection", + "file": "app/controllers/concerns/paginable.rb", + "line": 106, + "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", + "code": "scope.search(@paginable_params[:search]).order(\"#{@paginable_params[:sort_field]} #{upcasing_sort_direction}\")", + "render_path": null, + "location": { + "type": "method", + "class": "Paginable", + "method": "refine_query" + }, + "user_input": "@paginable_params[:sort_field]", + "confidence": "Weak", + "note": "" + }, + { + "warning_type": "Cross-Site Scripting", + "warning_code": 2, + "fingerprint": "e4016073dbfce89f1712e35cc3d55da7b4e54393ab25f8f33b91f744999f9822", + "check_name": "CrossSiteScripting", + "message": "Unescaped model attribute", + "file": "app/views/org_admin/questions/_show.html.erb", + "line": 26, + "link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting", + "code": "Question.includes(:annotations, :question_options, :section => ({ :phase => :template })).find(params[:id]).default_value", + "render_path": [{"type":"controller","class":"OrgAdmin::QuestionsController","method":"show","line":12,"file":"app/controllers/org_admin/questions_controller.rb"}], + "location": { + "type": "template", + "template": "org_admin/questions/_show" + }, + "user_input": null, + "confidence": "High", + "note": "" + }, + { + "warning_type": "Cross-Site Scripting", + "warning_code": 2, + "fingerprint": "f68bebd6980826084889d58192706bba9696247729e304c1f3aabe678e4f32d9", + "check_name": "CrossSiteScripting", + "message": "Unescaped model attribute", + "file": "app/views/shared/export/_plan.erb", + "line": 78, + "link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting", + "code": "Plan.includes(:answers).find(params[:id]).answer(question[:id], false).text", + "render_path": [{"type":"controller","class":"PlansController","method":"export","line":277,"file":"app/controllers/plans_controller.rb"},{"type":"template","name":"plans/export","line":2,"file":"app/views/plans/export.erb"}], + "location": { + "type": "template", + "template": "shared/export/_plan" + }, + "user_input": null, + "confidence": "High", + "note": "" + } + ], + "updated": "2018-08-07 17:38:06 +0100", + "brakeman_version": "4.3.1" +} diff --git a/config/database_example.yml b/config/database_example.yml index 1f6931b..a538d9b 100644 --- a/config/database_example.yml +++ b/config/database_example.yml @@ -1,22 +1,12 @@ -development: - adapter: mysql2 - database: roadmap - username: root - password: - encoding: utf8mb4 - -# Warning: The database defined as "test" will be erased and -# re-generated from your development database when you run "rake". -# Do not set this db to the same as development or production. -test: - adapter: sqlite3 - database: db/test.sqlite3 +defaults: &defaults + adapter: <%= ENV['DB_ADAPTER'] || 'postgresql' %> + encoding: <%= ENV['DB_ADAPTER'] == "mysql2" ? "utf8mb4" : "" %> + username: <%= ENV["DB_ADAPTER"] == "postgresql" ? 'postgres' : '' %> + database: roadmap_<%= ENV['RAILS_ENV'] %> pool: 5 - timeout: 5000 -production: - adapter: mysql2 - database: roadmap - username: root - password: - encoding: utf8mb4 +development: + <<: *defaults + +test: + <<: *defaults diff --git a/config/environments/development.rb b/config/environments/development.rb index 4e6f302..b55ece1 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,4 +1,7 @@ Rails.application.configure do + + config.i18n.available_locales = %w[en en_GB] + # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development @@ -31,9 +34,6 @@ # Only use best-standards-support built into browsers config.action_dispatch.best_standards_support = :builtin - # Raise exception on mass assignment protection for Active Record models - config.active_record.mass_assignment_sanitizer = :strict - config.action_mailer.perform_deliveries = false BetterErrors::Midleware.allow_ip! "10.0.2.2" if defined?(BetterErrors) && Rails.env == :development diff --git a/config/environments/test.rb b/config/environments/test.rb index 11e8536..5183373 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,6 +1,10 @@ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. + # This is required for the Faker gem. See this issue here: + # https://github.com/stympy/faker/issues/266 + config.i18n.available_locales = %w[en en_GB] + # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped @@ -40,7 +44,7 @@ # Raises error for missing translations # config.action_view.raise_on_missing_translations = true - + # Assets pipeline config.assets.enabled = false config.assets.debug = false diff --git a/config/initializers/bullet.rb b/config/initializers/bullet.rb new file mode 100644 index 0000000..0da2ba9 --- /dev/null +++ b/config/initializers/bullet.rb @@ -0,0 +1,20 @@ +Bullet.tap do |config| + config.enable = true + # config.alert = true + config.bullet_logger = true + config.console = true + # config.growl = true + # config.xmpp = { :account => 'bullets_account@jabber.org', + # :password => 'bullets_password_for_jabber', + # :receiver => 'your_account@jabber.org', + # :show_online_status => true } + config.rails_logger = true + # config.honeybadger = true + # config.bugsnag = true + # config.airbrake = true + # config.rollbar = true + config.add_footer = true + # config.stacktrace_includes = [ 'your_gem', 'your_middleware' ] + # config.stacktrace_excludes = [ 'their_gem', 'their_middleware' ] + # config.slack = { webhook_url: 'http://some.slack.url', channel: '#default', username: 'notifier' } +end if defined?(Bullet) diff --git a/config/initializers/constants.rb b/config/initializers/constants.rb index 0b64627..eca0bc3 100644 --- a/config/initializers/constants.rb +++ b/config/initializers/constants.rb @@ -1,6 +1,12 @@ -LANGUAGES = (ActiveRecord::Base.connection.table_exists? 'languages') ? Language.sorted_by_abbreviation : [] -MANY_LANGUAGES = LANGUAGES.length > 1 TABLE_FILTER_MIN_ROWS = 10 MAX_NUMBER_LINKS_FUNDER = 5 MAX_NUMBER_LINKS_SAMPLE_PLAN = 5 MAX_NUMBER_THEMES_PER_COLUMN = 5 + +if Rails.env.test? + LANGUAGES = [] + MANY_LANGUAGES = false +else + LANGUAGES = (ActiveRecord::Base.connection.table_exists? 'languages') ? Language.sorted_by_abbreviation : [] + MANY_LANGUAGES = LANGUAGES.length > 1 +end diff --git a/config/routes.rb b/config/routes.rb index 9ebab51..f31b1ec 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -152,7 +152,6 @@ resources :plans do member do - get 'status' get 'locked' get 'answer' put 'update_guidance_choices' @@ -269,25 +268,26 @@ patch 'publish', action: :publish, constraints: {format: [:json]} patch 'unpublish', action: :unpublish, constraints: {format: [:json]} end - + # Used for the organisational and customizable views of index collection do get 'organisational' get 'customisable' end - - resources :phases, only: [:show, :edit, :new, :create, :edit, :update, :destroy] do + + resources :phases, except: [:index] do member do get 'preview' + post 'sort' end - + resources :sections, only: [:index, :show, :edit, :update, :create, :destroy] do resources :questions, only: [:show, :edit, :new, :update, :create, :destroy] do end end end end - + resources :annotations, only: [:create, :destroy, :update] do ; end get 'template_options' => 'templates#template_options', constraints: {format: [:json]} diff --git a/db/migrate/20130905073232_remove_slug_from_plans.rb b/db/migrate/20130905073232_remove_slug_from_plans.rb deleted file mode 100644 index a06bdee..0000000 --- a/db/migrate/20130905073232_remove_slug_from_plans.rb +++ /dev/null @@ -1,9 +0,0 @@ -class RemoveSlugFromPlans < ActiveRecord::Migration - def up - remove_column :plans, :slug - end - - def down - add_column :plans, :slug, :string - end -end diff --git a/db/migrate/20170303220255_remove_orcid_id_from_users.rb b/db/migrate/20170303220255_remove_orcid_id_from_users.rb deleted file mode 100644 index 7c71213..0000000 --- a/db/migrate/20170303220255_remove_orcid_id_from_users.rb +++ /dev/null @@ -1,5 +0,0 @@ -class RemoveOrcidIdFromUsers < ActiveRecord::Migration - def change - #remove_column :users, :orcid_id - end -end diff --git a/db/migrate/20180713145319_fix_invalid_mysql_indices.rb b/db/migrate/20180713145319_fix_invalid_mysql_indices.rb new file mode 100644 index 0000000..d2a3250 --- /dev/null +++ b/db/migrate/20180713145319_fix_invalid_mysql_indices.rb @@ -0,0 +1,20 @@ +class FixInvalidMysqlIndices < ActiveRecord::Migration + def up + if index_exists?("settings", ["target_type", "target_id", "var"]) + remove_index "settings", ["target_type", "target_id", "var"] + + add_index "settings", ["target_type", "target_id"], + name: "index_settings_on_target_type_and_target_id", + unique: true + end + end + + def down + if index_exists?("settings", ["target_type", "target_id"]) + remove_index "settings", ["target_type", "target_id"] + add_index "settings", ["target_type", "target_id", "var"], + name: "index_settings_on_target_type_and_target_id_and_var", + unique: true + end + end +end diff --git a/db/migrate/20180713145547_remove_defaults_from_links.rb b/db/migrate/20180713145547_remove_defaults_from_links.rb new file mode 100644 index 0000000..e9fea71 --- /dev/null +++ b/db/migrate/20180713145547_remove_defaults_from_links.rb @@ -0,0 +1,12 @@ +class RemoveDefaultsFromLinks < ActiveRecord::Migration + def up + change_column :templates, :links, :text, default: nil + change_column :orgs, :links, :text, default: nil + end + def down + change_column :templates, :links, :text, + default: "{\"funder\":[], \"sample_plan\":[]}" + change_column :orgs, :links, :text, + default: "{\"funder\":[], \"sample_plan\":[]}" + end +end diff --git a/db/migrate/20180713161007_remove_settings_indices.rb b/db/migrate/20180713161007_remove_settings_indices.rb new file mode 100644 index 0000000..62dfbba --- /dev/null +++ b/db/migrate/20180713161007_remove_settings_indices.rb @@ -0,0 +1,11 @@ +class RemoveSettingsIndices < ActiveRecord::Migration + def up + remove_index "settings", ["target_type", "target_id"] + end + def down + add_index "settings", ["target_type", "target_id"], + name: "index_settings_on_target_type_and_target_id", + unique: true, + using: :btree + end +end diff --git a/db/migrate/20180713164120_add_length_constraints_to_users_email.rb b/db/migrate/20180713164120_add_length_constraints_to_users_email.rb new file mode 100644 index 0000000..00a9afa --- /dev/null +++ b/db/migrate/20180713164120_add_length_constraints_to_users_email.rb @@ -0,0 +1,8 @@ +class AddLengthConstraintsToUsersEmail < ActiveRecord::Migration + def up + change_column :users, :email, :string, default: "", null: false, limit: 80 + end + def down + change_column :users, :email, :string, default: "", null: false, limit: nil + end +end diff --git a/db/migrate/20180803105147_remove_published_from_sections.rb b/db/migrate/20180803105147_remove_published_from_sections.rb new file mode 100644 index 0000000..03fa4b8 --- /dev/null +++ b/db/migrate/20180803105147_remove_published_from_sections.rb @@ -0,0 +1,8 @@ +class RemovePublishedFromSections < ActiveRecord::Migration + def up + remove_column :sections, :published, :boolean + end + def down + add_column :sections, :published, :boolean + end +end diff --git a/db/migrate/20180807114035_add_default_value_to_optional_subset_on_guidance_groups.rb b/db/migrate/20180807114035_add_default_value_to_optional_subset_on_guidance_groups.rb new file mode 100644 index 0000000..45f686e --- /dev/null +++ b/db/migrate/20180807114035_add_default_value_to_optional_subset_on_guidance_groups.rb @@ -0,0 +1,8 @@ +class AddDefaultValueToOptionalSubsetOnGuidanceGroups < ActiveRecord::Migration + def up + change_column :guidance_groups, :optional_subset, :boolean, default: false, null: false + end + def down + change_column :guidance_groups, :optional_subset, :boolean, default: nil, null: true + end +end diff --git a/db/migrate/20180807114052_add_default_value_to_published_on_guidance_groups.rb b/db/migrate/20180807114052_add_default_value_to_published_on_guidance_groups.rb new file mode 100644 index 0000000..25006ae --- /dev/null +++ b/db/migrate/20180807114052_add_default_value_to_published_on_guidance_groups.rb @@ -0,0 +1,8 @@ +class AddDefaultValueToPublishedOnGuidanceGroups < ActiveRecord::Migration + def up + change_column :guidance_groups, :published, :boolean, default: false, null: false + end + def down + change_column :guidance_groups, :published, :boolean, default: nil, null: true + end +end diff --git a/db/migrate/20180807120926_add_default_value_to_archived_on_notes.rb b/db/migrate/20180807120926_add_default_value_to_archived_on_notes.rb new file mode 100644 index 0000000..c03e3f5 --- /dev/null +++ b/db/migrate/20180807120926_add_default_value_to_archived_on_notes.rb @@ -0,0 +1,9 @@ +class AddDefaultValueToArchivedOnNotes < ActiveRecord::Migration + def up + change_column :notes, :archived, :boolean, default: false, null: false + end + + def down + change_column :notes, :archived, :boolean, default: nil, null: true + end +end diff --git a/db/migrate/20180807121126_add_default_value_to_is_other_on_orgs.rb b/db/migrate/20180807121126_add_default_value_to_is_other_on_orgs.rb new file mode 100644 index 0000000..3aade3b --- /dev/null +++ b/db/migrate/20180807121126_add_default_value_to_is_other_on_orgs.rb @@ -0,0 +1,8 @@ +class AddDefaultValueToIsOtherOnOrgs < ActiveRecord::Migration + def up + change_column :orgs, :is_other, :boolean, default: false, null: false + end + def down + change_column :orgs, :is_other, :boolean, default: nil, null: true + end +end diff --git a/db/migrate/20180813114157_remove_logo_file_name_from_orgs.rb b/db/migrate/20180813114157_remove_logo_file_name_from_orgs.rb new file mode 100644 index 0000000..334fdd7 --- /dev/null +++ b/db/migrate/20180813114157_remove_logo_file_name_from_orgs.rb @@ -0,0 +1,11 @@ +class RemoveLogoFileNameFromOrgs < ActiveRecord::Migration + def up + if column_exists?(:orgs, :logo_file_name) + remove_column :orgs, :logo_file_name + end + end + + def down + add_column :orgs, :logo_file_name, :string + end +end diff --git a/db/migrate/20180813114216_remove_parent_id_from_orgs.rb b/db/migrate/20180813114216_remove_parent_id_from_orgs.rb new file mode 100644 index 0000000..118125b --- /dev/null +++ b/db/migrate/20180813114216_remove_parent_id_from_orgs.rb @@ -0,0 +1,11 @@ +class RemoveParentIdFromOrgs < ActiveRecord::Migration + def up + if column_exists?(:orgs, :parent_id) + remove_column :orgs, :parent_id + end + end + + def down + add_column :orgs, :parent_id, :integer + end +end diff --git a/db/migrate/20180813114234_remove_wayfless_entity_from_orgs.rb b/db/migrate/20180813114234_remove_wayfless_entity_from_orgs.rb new file mode 100644 index 0000000..a19bb0c --- /dev/null +++ b/db/migrate/20180813114234_remove_wayfless_entity_from_orgs.rb @@ -0,0 +1,11 @@ +class RemoveWayflessEntityFromOrgs < ActiveRecord::Migration + def up + if column_exists?(:orgs, :wayfless_entity) + remove_column :orgs, :wayfless_entity + end + end + + def down + add_column :orgs, :wayfless_entity, :string + end +end diff --git a/db/migrate/20180813114348_remove_slug_from_phases.rb b/db/migrate/20180813114348_remove_slug_from_phases.rb new file mode 100644 index 0000000..3520ef6 --- /dev/null +++ b/db/migrate/20180813114348_remove_slug_from_phases.rb @@ -0,0 +1,11 @@ +class RemoveSlugFromPhases < ActiveRecord::Migration + def up + if column_exists?(:phases, :slug) + remove_column :phases, :slug + end + end + + def down + add_column :phases, :slug, :string + end +end diff --git a/db/migrate/20180813114525_remove_slug_from_plans.rb b/db/migrate/20180813114525_remove_slug_from_plans.rb new file mode 100644 index 0000000..11ebb04 --- /dev/null +++ b/db/migrate/20180813114525_remove_slug_from_plans.rb @@ -0,0 +1,11 @@ +class RemoveSlugFromPlans < ActiveRecord::Migration + def up + if column_exists?(:plans, :slug) + remove_column :plans, :slug + end + end + + def down + add_column :plans, :slug, :string + end +end diff --git a/db/migrate/20180813114550_drop_table_friendly_id_slugs.rb b/db/migrate/20180813114550_drop_table_friendly_id_slugs.rb new file mode 100644 index 0000000..8d0ab2e --- /dev/null +++ b/db/migrate/20180813114550_drop_table_friendly_id_slugs.rb @@ -0,0 +1,17 @@ +class DropTableFriendlyIdSlugs < ActiveRecord::Migration + def up + drop_table :friendly_id_slugs if table_exists?(:friendly_id_slugs) + end + + def down + create_table :friendly_id_slugs do |t| + t.string :slug, :null => false + t.integer :sluggable_id, :null => false + t.string :sluggable_type, :limit => 40 + t.datetime :created_at + end + add_index :friendly_id_slugs, :sluggable_id + add_index :friendly_id_slugs, [:slug, :sluggable_type], :unique => true + add_index :friendly_id_slugs, :sluggable_type + end +end diff --git a/db/migrate/20180813114614_drop_table_file_types.rb b/db/migrate/20180813114614_drop_table_file_types.rb new file mode 100644 index 0000000..d0be6f1 --- /dev/null +++ b/db/migrate/20180813114614_drop_table_file_types.rb @@ -0,0 +1,17 @@ +class DropTableFileTypes < ActiveRecord::Migration + + def up + drop_table(:file_types) if table_exists?(:file_types) + end + + def down + create_table "file_types", force: :cascade do |t| + t.string "name" + t.string "icon_name" + t.integer "icon_size" + t.string "icon_location" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + end +end diff --git a/db/migrate/20180813114622_drop_table_file_uploads.rb b/db/migrate/20180813114622_drop_table_file_uploads.rb new file mode 100644 index 0000000..efc1f5c --- /dev/null +++ b/db/migrate/20180813114622_drop_table_file_uploads.rb @@ -0,0 +1,19 @@ +class DropTableFileUploads < ActiveRecord::Migration + def up + drop_table(:file_uploads) if table_exists?(:file_uploads) + end + + def down + create_table "file_uploads", force: :cascade do |t| + t.string "name" + t.string "title" + t.text "description" + t.integer "size" + t.boolean "published" + t.string "location" + t.integer "file_type_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + end +end diff --git a/db/migrate/20180813114628_drop_table_splash_logs.rb b/db/migrate/20180813114628_drop_table_splash_logs.rb new file mode 100644 index 0000000..b09cc0b --- /dev/null +++ b/db/migrate/20180813114628_drop_table_splash_logs.rb @@ -0,0 +1,13 @@ +class DropTableSplashLogs < ActiveRecord::Migration + def up + drop_table(:splash_logs) if table_exists?(:splash_logs) + end + + def down + create_table "splash_logs", force: :cascade do |t| + t.string "destination" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + end +end diff --git a/db/migrate/20180813114649_remove_banner_text_from_orgs.rb b/db/migrate/20180813114649_remove_banner_text_from_orgs.rb new file mode 100644 index 0000000..315e4b4 --- /dev/null +++ b/db/migrate/20180813114649_remove_banner_text_from_orgs.rb @@ -0,0 +1,9 @@ +class RemoveBannerTextFromOrgs < ActiveRecord::Migration + def up + remove_column :orgs, :banner_text + end + + def down + add_column :orgs, :banner_text, :text + end +end diff --git a/db/migrate/20180813114719_remove_question_id_from_guidances.rb b/db/migrate/20180813114719_remove_question_id_from_guidances.rb new file mode 100644 index 0000000..4849d84 --- /dev/null +++ b/db/migrate/20180813114719_remove_question_id_from_guidances.rb @@ -0,0 +1,9 @@ +class RemoveQuestionIdFromGuidances < ActiveRecord::Migration + def up + remove_column :guidances, :question_id + end + + def down + add_column :guidances, :question_id, :integer + end +end diff --git a/db/migrate/20180813114801_remove_orcid_id_from_users.rb b/db/migrate/20180813114801_remove_orcid_id_from_users.rb new file mode 100644 index 0000000..aa89421 --- /dev/null +++ b/db/migrate/20180813114801_remove_orcid_id_from_users.rb @@ -0,0 +1,9 @@ +class RemoveOrcidIdFromUsers < ActiveRecord::Migration + def up + remove_column :users, :orcid_id + end + + def down + add_column :users, :orcid_id, :string + end +end diff --git a/db/migrate/20180813114813_remove_shibboleth_id_from_users.rb b/db/migrate/20180813114813_remove_shibboleth_id_from_users.rb new file mode 100644 index 0000000..51cd451 --- /dev/null +++ b/db/migrate/20180813114813_remove_shibboleth_id_from_users.rb @@ -0,0 +1,9 @@ +class RemoveShibbolethIdFromUsers < ActiveRecord::Migration + def up + remove_column :users, :shibboleth_id + end + + def down + add_column :users, :shibboleth_id, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 8864996..41ee2bf 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,10 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180508151824) do +ActiveRecord::Schema.define(version: 20180813114813) do + + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" create_table "annotations", force: :cascade do |t| t.integer "question_id" @@ -22,7 +25,7 @@ t.datetime "updated_at" end - add_index "annotations", ["question_id"], name: "index_annotations_on_question_id" + add_index "annotations", ["question_id"], name: "index_annotations_on_question_id", using: :btree create_table "answers", force: :cascade do |t| t.text "text" @@ -34,15 +37,15 @@ t.integer "lock_version", default: 0 end - add_index "answers", ["plan_id"], name: "index_answers_on_plan_id" - add_index "answers", ["question_id"], name: "index_answers_on_question_id" + add_index "answers", ["plan_id"], name: "index_answers_on_plan_id", using: :btree + add_index "answers", ["question_id"], name: "index_answers_on_question_id", using: :btree create_table "answers_question_options", id: false, force: :cascade do |t| t.integer "answer_id", null: false t.integer "question_option_id", null: false end - add_index "answers_question_options", ["answer_id"], name: "index_answers_question_options_on_answer_id" + add_index "answers_question_options", ["answer_id"], name: "index_answers_question_options_on_answer_id", using: :btree create_table "exported_plans", force: :cascade do |t| t.integer "plan_id" @@ -53,48 +56,26 @@ t.integer "phase_id" end - create_table "file_types", force: :cascade do |t| - t.string "name" - t.string "icon_name" - t.integer "icon_size" - t.string "icon_location" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - - create_table "file_uploads", force: :cascade do |t| - t.string "name" - t.string "title" - t.text "description" - t.integer "size" - t.boolean "published" - t.string "location" - t.integer "file_type_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - create_table "guidance_groups", force: :cascade do |t| t.string "name" t.integer "org_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "optional_subset" - t.boolean "published" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "optional_subset", default: false, null: false + t.boolean "published", default: false, null: false end - add_index "guidance_groups", ["org_id"], name: "index_guidance_groups_on_org_id" + add_index "guidance_groups", ["org_id"], name: "index_guidance_groups_on_org_id", using: :btree create_table "guidances", force: :cascade do |t| t.text "text" t.integer "guidance_group_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.integer "question_id" t.boolean "published" end - add_index "guidances", ["guidance_group_id"], name: "index_guidances_on_guidance_group_id" + add_index "guidances", ["guidance_group_id"], name: "index_guidances_on_guidance_group_id", using: :btree create_table "identifier_schemes", force: :cascade do |t| t.string "name" @@ -116,14 +97,14 @@ create_table "notes", force: :cascade do |t| t.integer "user_id" t.text "text" - t.boolean "archived" + t.boolean "archived", default: false, null: false t.integer "answer_id" t.integer "archived_by" t.datetime "created_at" t.datetime "updated_at" end - add_index "notes", ["answer_id"], name: "index_notes_on_answer_id" + add_index "notes", ["answer_id"], name: "index_notes_on_answer_id", using: :btree create_table "notification_acknowledgements", force: :cascade do |t| t.integer "user_id" @@ -132,8 +113,8 @@ t.datetime "updated_at" end - add_index "notification_acknowledgements", ["notification_id"], name: "index_notification_acknowledgements_on_notification_id" - add_index "notification_acknowledgements", ["user_id"], name: "index_notification_acknowledgements_on_user_id" + add_index "notification_acknowledgements", ["notification_id"], name: "index_notification_acknowledgements_on_notification_id", using: :btree + add_index "notification_acknowledgements", ["user_id"], name: "index_notification_acknowledgements_on_user_id", using: :btree create_table "notifications", force: :cascade do |t| t.integer "notification_type" @@ -163,26 +144,22 @@ t.datetime "updated_at" end - add_index "org_token_permissions", ["org_id"], name: "index_org_token_permissions_on_org_id" + add_index "org_token_permissions", ["org_id"], name: "index_org_token_permissions_on_org_id", using: :btree create_table "orgs", force: :cascade do |t| t.string "name" t.string "abbreviation" t.string "target_url" - t.string "wayfless_entity" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "parent_id" - t.boolean "is_other" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "is_other", default: false, null: false t.string "sort_name" - t.text "banner_text" - t.string "logo_file_name" t.integer "region_id" t.integer "language_id" t.string "logo_uid" t.string "logo_name" t.string "contact_email" - t.integer "org_type", default: 0, null: false + t.integer "org_type", default: 0, null: false t.text "links" t.string "contact_name" t.boolean "feedback_enabled", default: false @@ -203,18 +180,16 @@ t.integer "template_id" t.datetime "created_at" t.datetime "updated_at" - t.string "slug" t.boolean "modifiable" end - add_index "phases", ["template_id"], name: "index_phases_on_template_id" + add_index "phases", ["template_id"], name: "index_phases_on_template_id", using: :btree create_table "plans", force: :cascade do |t| t.string "title" t.integer "template_id" t.datetime "created_at" t.datetime "updated_at" - t.string "slug" t.string "grant_number" t.string "identifier" t.text "description" @@ -231,14 +206,14 @@ t.boolean "complete", default: false end - add_index "plans", ["template_id"], name: "index_plans_on_template_id" + add_index "plans", ["template_id"], name: "index_plans_on_template_id", using: :btree create_table "plans_guidance_groups", force: :cascade do |t| t.integer "guidance_group_id" t.integer "plan_id" end - add_index "plans_guidance_groups", ["guidance_group_id", "plan_id"], name: "index_plans_guidance_groups_on_guidance_group_id_and_plan_id" + add_index "plans_guidance_groups", ["guidance_group_id", "plan_id"], name: "index_plans_guidance_groups_on_guidance_group_id_and_plan_id", using: :btree create_table "prefs", force: :cascade do |t| t.text "settings" @@ -263,7 +238,7 @@ t.datetime "updated_at" end - add_index "question_options", ["question_id"], name: "index_question_options_on_question_id" + add_index "question_options", ["question_id"], name: "index_question_options_on_question_id", using: :btree create_table "questions", force: :cascade do |t| t.text "text" @@ -277,14 +252,14 @@ t.boolean "modifiable" end - add_index "questions", ["section_id"], name: "index_questions_on_section_id" + add_index "questions", ["section_id"], name: "index_questions_on_section_id", using: :btree create_table "questions_themes", id: false, force: :cascade do |t| t.integer "question_id", null: false t.integer "theme_id", null: false end - add_index "questions_themes", ["question_id"], name: "index_questions_themes_on_question_id" + add_index "questions_themes", ["question_id"], name: "index_questions_themes_on_question_id", using: :btree create_table "regions", force: :cascade do |t| t.string "abbreviation" @@ -302,8 +277,8 @@ t.boolean "active", default: true end - add_index "roles", ["plan_id"], name: "index_roles_on_plan_id" - add_index "roles", ["user_id"], name: "index_roles_on_user_id" + add_index "roles", ["plan_id"], name: "index_roles_on_plan_id", using: :btree + add_index "roles", ["user_id"], name: "index_roles_on_user_id", using: :btree create_table "sections", force: :cascade do |t| t.string "title" @@ -311,12 +286,11 @@ t.integer "number" t.datetime "created_at" t.datetime "updated_at" - t.boolean "published" t.integer "phase_id" t.boolean "modifiable" end - add_index "sections", ["phase_id"], name: "index_sections_on_phase_id" + add_index "sections", ["phase_id"], name: "index_sections_on_phase_id", using: :btree create_table "settings", force: :cascade do |t| t.string "var", null: false @@ -327,14 +301,6 @@ t.datetime "updated_at", null: false end - add_index "settings", ["target_type", "target_id", "var"], name: "index_settings_on_target_type_and_target_id_and_var", unique: true - - create_table "splash_logs", force: :cascade do |t| - t.string "destination" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - create_table "templates", force: :cascade do |t| t.string "title" t.text "description" @@ -352,11 +318,11 @@ t.text "links" end - add_index "templates", ["customization_of", "version", "org_id"], name: "index_templates_on_customization_of_and_version_and_org_id", unique: true - add_index "templates", ["family_id", "version"], name: "index_templates_on_family_id_and_version", unique: true - add_index "templates", ["family_id"], name: "index_templates_on_family_id" - add_index "templates", ["org_id", "family_id"], name: "template_organisation_dmptemplate_index" - add_index "templates", ["org_id"], name: "index_templates_on_org_id" + add_index "templates", ["customization_of", "version", "org_id"], name: "index_templates_on_customization_of_and_version_and_org_id", unique: true, using: :btree + add_index "templates", ["family_id", "version"], name: "index_templates_on_family_id_and_version", unique: true, using: :btree + add_index "templates", ["family_id"], name: "index_templates_on_family_id", using: :btree + add_index "templates", ["org_id", "family_id"], name: "template_organisation_dmptemplate_index", using: :btree + add_index "templates", ["org_id"], name: "index_templates_on_org_id", using: :btree create_table "themes", force: :cascade do |t| t.string "title" @@ -371,8 +337,8 @@ t.integer "guidance_id" end - add_index "themes_in_guidance", ["guidance_id"], name: "index_themes_in_guidance_on_guidance_id" - add_index "themes_in_guidance", ["theme_id"], name: "index_themes_in_guidance_on_theme_id" + add_index "themes_in_guidance", ["guidance_id"], name: "index_themes_in_guidance_on_guidance_id", using: :btree + add_index "themes_in_guidance", ["theme_id"], name: "index_themes_in_guidance_on_theme_id", using: :btree create_table "token_permission_types", force: :cascade do |t| t.string "token_type" @@ -389,21 +355,19 @@ t.integer "identifier_scheme_id" end - add_index "user_identifiers", ["user_id"], name: "index_user_identifiers_on_user_id" + add_index "user_identifiers", ["user_id"], name: "index_user_identifiers_on_user_id", using: :btree create_table "users", force: :cascade do |t| t.string "firstname" t.string "surname" - t.string "email", default: "", null: false - t.string "orcid_id" - t.string "shibboleth_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "encrypted_password", default: "" + t.string "email", limit: 80, default: "", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "encrypted_password", default: "" t.string "reset_password_token" t.datetime "reset_password_sent_at" t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0 + t.integer "sign_in_count", default: 0 t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" t.string "current_sign_in_ip" @@ -423,11 +387,11 @@ t.string "invited_by_type" t.integer "language_id" t.string "recovery_email" - t.boolean "active", default: true + t.boolean "active", default: true end - add_index "users", ["email"], name: "index_users_on_email", unique: true - add_index "users", ["org_id"], name: "index_users_on_org_id" + add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree + add_index "users", ["org_id"], name: "index_users_on_org_id", using: :btree create_table "users_perms", id: false, force: :cascade do |t| t.integer "user_id" @@ -441,6 +405,8 @@ add_foreign_key "answers", "plans" add_foreign_key "answers", "questions" add_foreign_key "answers", "users" + add_foreign_key "answers_question_options", "answers" + add_foreign_key "answers_question_options", "question_options" add_foreign_key "guidance_groups", "orgs" add_foreign_key "guidances", "guidance_groups" add_foreign_key "notes", "answers" @@ -460,6 +426,8 @@ add_foreign_key "question_options", "questions" add_foreign_key "questions", "question_formats" add_foreign_key "questions", "sections" + add_foreign_key "questions_themes", "questions" + add_foreign_key "questions_themes", "themes" add_foreign_key "roles", "plans" add_foreign_key "roles", "users" add_foreign_key "sections", "phases" @@ -470,4 +438,6 @@ add_foreign_key "user_identifiers", "users" add_foreign_key "users", "languages" add_foreign_key "users", "orgs" + add_foreign_key "users_perms", "perms" + add_foreign_key "users_perms", "users" end diff --git a/db/seeds.rb b/db/seeds.rb old mode 100644 new mode 100755 index f41156e..bbadc4d --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,30 +1,88 @@ -# -*- coding: utf-8 -*- -# This file should contain all the record creation needed to seed the database with its default values. -# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). +#!/usr/bin/env ruby +# encoding: utf-8 +# frozen_string_literal: true +# warn_indent: true + +# This file should contain all the record creation needed to seed the database +# with its default values. The data can then be loaded with the rake db:seed +# (or created alongside the db with db:setup). + +require 'factory_bot' +require 'faker' + +include FactoryBot::Syntax::Methods + + +LOCALE = :en + +I18n.locale = LOCALE +Faker::Config.locale = LOCALE +FastGettext.default_locale = LOCALE + + +require 'factory_bot' +include FactoryBot::Syntax::Methods # Identifier Schemes # ------------------------------------------------------- identifier_schemes = [ - {name: 'orcid', description: 'ORCID', active: true, - logo_url:'http://orcid.org/sites/default/files/images/orcid_16x16.png', - user_landing_url:'https://orcid.org' }, - {name: 'shibboleth', description: 'Your institutional credentials', active: true, + { + name: 'orcid', + description: 'ORCID', + active: true, + logo_url:'http://orcid.org/sites/default/files/images/orcid_16x16.png', + user_landing_url:'https://orcid.org' + }, + { + name: 'shibboleth', + description: 'Your institutional credentials', + active: true, + logo_url: 'http://newsite.shibboleth.net/wp-content/uploads/2017/01/Shibboleth-logo_2000x1200-1.png', + user_landing_url: "https://example.com" }, ] -identifier_schemes.map{ |is| IdentifierScheme.create!(is) if IdentifierScheme.find_by(name: is[:name]).nil? } +identifier_schemes.map { |is| create(:identifier_scheme, is) } # Question Formats # ------------------------------------------------------- question_formats = [ - {title: "Text area", option_based: false, formattype: 0}, - {title: "Text field", option_based: false, formattype: 1}, - {title: "Radio buttons", option_based: true, formattype: 2}, - {title: "Check box", option_based: true, formattype: 3}, - {title: "Dropdown", option_based: true, formattype: 4}, - {title: "Multi select box", option_based: true, formattype: 5}, - {title: "Date", option_based: true, formattype: 6} + { + title: "Text area", + option_based: false, + formattype: 0 + }, + { + title: "Text field", + option_based: false, + formattype: 1 + }, + { + title: "Radio buttons", + option_based: true, + formattype: 2 + }, + { + title: "Check box", + option_based: true, + formattype: 3 + }, + { + title: "Dropdown", + option_based: true, + formattype: 4 + }, + { + title: "Multi select box", + option_based: true, + formattype: 5 + }, + { + title: "Date", + option_based: true, + formattype: 6 + } ] -question_formats.map{ |qf| QuestionFormat.create!(qf) if QuestionFormat.find_by(title: qf[:title]).nil? } +question_formats.map{ |qf| create(:question_format, qf) } # Languages (check config/locales for any ones not defined here) # ------------------------------------------------------- @@ -50,7 +108,7 @@ name: 'Español', default_language: false} ] -languages.map{ |l| Language.create!(l) if Language.find_by(abbreviation: l[:abbreviation]).nil? } +languages.map { |l| create(:language, l) } # Scan through the locale files and add an entry if a file is present but # not defined in this seed file @@ -128,7 +186,7 @@ {name: 'grant_api_to_orgs'} ] -perms.map{ |p| Perm.create!(p) if Perm.find_by(name: p[:name]).nil? } +perms.map{ |p| create(:perm, p) } # Guidance Themes # ------------------------------------------------------- @@ -148,7 +206,7 @@ {title: 'Budget'}, {title: 'Related Policies'} ] -themes.map{ |t| Theme.create!(t) if Theme.find_by(title: t[:title]).nil? } +themes.map{ |t| create(:theme, t) } # Token Permission Types # ------------------------------------------------------- @@ -158,15 +216,15 @@ {token_type: 'templates', text_description: 'allows a user access to the templates api endpoint'}, {token_type: 'statistics', text_description: 'allows a user access to the statistics api endpoint'} ] -token_permission_types.map{ |tpt| TokenPermissionType.create!(tpt) if TokenPermissionType.find_by(token_type: tpt[:token_type]).nil? } +token_permission_types.map{ |tpt| create(:token_permission_type, tpt) } # Create our generic organisation, a funder and a University # ------------------------------------------------------- orgs = [ - {name: Rails.configuration.branding[:organisation][:name], - abbreviation: Rails.configuration.branding[:organisation][:abbreviation], + {name: Branding.fetch(:organisation, :name), + abbreviation: Branding.fetch(:organisation, :abbreviation), org_type: 4, links: {"org":[]}, - language_id: Language.find_by(abbreviation: 'en_GB'), + language: Language.find_by(abbreviation: 'en_GB'), token_permission_types: TokenPermissionType.all}, {name: 'Government Agency', abbreviation: 'GA', @@ -177,7 +235,7 @@ org_type: 1, links: {"org":[]}, language: Language.find_by(abbreviation: 'en_GB')} ] -orgs.map{ |o| Org.create!(o) if Org.find_by(abbreviation: o[:abbreviation]).nil? } +orgs.map{ |o| create(:org, o) } # Create a Super Admin associated with our generic organisation, # an Org Admin for our funder and an Org Admin and User for our University @@ -188,7 +246,7 @@ surname: "Admin", password: "password123", password_confirmation: "password123", - org: Org.find_by(abbreviation: Rails.configuration.branding[:organisation][:abbreviation]), + org: Org.find_by(abbreviation: Branding.fetch(:organisation, :abbreviation)), language: Language.find_by(abbreviation: FastGettext.locale), perms: Perm.all, accept_terms: true, @@ -226,7 +284,7 @@ accept_terms: true, confirmed_at: Time.zone.now} ] -users.map{ |u| User.create!(u) if User.find_by(email: u[:email]).nil? } +users.map{ |u| create(:user, u) } # Create a Guidance Group for our organisation and the funder # ------------------------------------------------------- @@ -240,7 +298,7 @@ optional_subset: false, published: true} ] -guidance_groups.map{ |gg| GuidanceGroup.create!(gg) if GuidanceGroup.find_by(name: gg[:name]).nil? } +guidance_groups.map{ |gg| create(:guidance_group, gg) } # Initialize with the generic Roadmap guidance and a sample funder guidance # ------------------------------------------------------- @@ -343,7 +401,7 @@ published: true, themes: [Theme.find_by(title: 'Data Description')]} ] -guidances.map{ |g| Guidance.create!(g) if Guidance.find_by(text: g[:text]).nil? } +guidances.map{ |g| create(:guidance, g) } # Create a default template for the curation centre and one for the example funder # ------------------------------------------------------- @@ -354,8 +412,6 @@ org: Org.find_by(abbreviation: Rails.configuration.branding[:organisation][:abbreviation]), is_default: true, version: 0, - migrated: false, - dmptemplate_id: 1, visibility: Template.visibilities[:publicly_visible], links: {"funder":[],"sample_plan":[]}}, @@ -364,9 +420,7 @@ org: Org.find_by(abbreviation: 'GA'), is_default: false, version: 0, - migrated: false, visibility: Template.visibilities[:organisationally_visible], - dmptemplate_id: 2, links: {"funder":[],"sample_plan":[]}}, {title: "Department of Testing Award", @@ -374,22 +428,12 @@ org: Org.find_by(abbreviation: 'GA'), is_default: false, version: 0, - migrated: false, visibility: Template.visibilities[:organisationally_visible], - dmptemplate_id: 3, links: {"funder":[],"sample_plan":[]}} ] # Template creation calls defaults handler which sets is_default and # published to false automatically, so update them after creation -templates.map do |t| - if Template.find_by(title: t[:title]).nil? - tmplt = Template.create!(t) - tmplt.published = t[:published] - tmplt.is_default = t[:is_default] - tmplt.visibility = t[:visibility] - tmplt.save! - end -end +templates.each { |atts| create(:template, atts) } # Create 2 phases for the funder's template and one for our generic template # ------------------------------------------------------- @@ -413,11 +457,11 @@ modifiable: false, template: Template.find_by(title: "Department of Testing Award")} ] -phases.map{ |p| Phase.create!(p) if Phase.find_by(title: p[:title]).nil? } +phases.map{ |p| create(:phase, p) } generic_template_phase_1 = Phase.find_by(title: "Generic Data Management Planning Template") -funder_template_phase_1 = Phase.find_by(title: "Preliminary Statement of Work") -funder_template_phase_2 = Phase.find_by(title: "Detailed Overview") +funder_template_phase_1 = Phase.find_by(title: "Preliminary Statement of Work") +funder_template_phase_2 = Phase.find_by(title: "Detailed Overview") # Create sections for the 2 templates and their phases # ------------------------------------------------------- @@ -461,7 +505,7 @@ # Section of old version of Funder Template {title: "Data Collection and Preservation", - number: 1, + number: 11, published: false, modifiable: true, phase: Phase.find_by(title: "Detailed Overview")}, @@ -473,7 +517,7 @@ modifiable: true, phase: funder_template_phase_1}, {title: "Data Description", - number: 1, + number: 2, published: false, modifiable: true, phase: funder_template_phase_1}, @@ -485,27 +529,27 @@ modifiable: false, phase: funder_template_phase_2}, {title: "Data Format and Storage", - number: 1, + number: 2, published: true, modifiable: false, phase: funder_template_phase_2}, {title: "Collection Process", - number: 1, + number: 3, published: true, modifiable: false, phase: funder_template_phase_2}, {title: "Ethical Standards", - number: 1, + number: 4, published: true, modifiable: false, phase: funder_template_phase_2}, {title: "Preservation and Reuse Policies", - number: 1, + number: 5, published: true, modifiable: false, phase: funder_template_phase_2} ] -sections.map{ |s| Section.create!(s) if Section.find_by(title: s[:title]).nil? } +sections.map{ |s| create(:section, s) } text_area = QuestionFormat.find_by(title: "Text area") @@ -686,9 +730,9 @@ modifiable: false, themes: [Theme.find_by(title: "Preservation"), Theme.find_by(title: "Data Sharing")]} ] -questions.map{ |q| Question.create!(q) if Question.find_by(text: q[:text]).nil? } +questions.map{ |q| create(:question, q) } -drop_down_question = Question.find_by(text: "Where will you store your data during the research period?") +drop_down_question = Question.find_by(text: "Where will you store your data during the research period?") multi_select_question = Question.find_by(text: "What type(s) of data will you collect?") radio_button_question = Question.find_by(text: "Please select the appropriate formats.") @@ -753,71 +797,3 @@ is_default: false} ] question_options.map{ |q| QuestionOption.create!(q) if QuestionOption.find_by(text: q[:text]).nil? } - -# Create plans -# ------------------------------------------------------- -=begin -plans = [ - {title: "Sample plan", - template: Template.find_by(title: "Department of Testing Award"), - grant_number: "FUNDER-GRANT-123", - identifier: "987654321", - description: "This is a sample plan based on a funder template", - principal_investigator: "John Doe", - principal_investigator_identifier: "ORCID: 12346-000-1234", - data_contact: "john.doe@example.com", - funder_name: "Example Government Agency", - visibility: 0} -] -plans.map{ |p| Plan.create!(p) if Plan.find_by(title: "Sample plan").nil? } - -plan = Plan.find_by(title: "Sample plan") -user = User.find_by(email: "org_user@example.com") - -answers = [ - {text: "We will collect data from various sources and create our own analysis.", - plan: plan, - user: user, - question: Question.find_by(text: "Provide an overview of the dataset.")}, - {text: "We will primarily collect images and video from our telescope and other instruments", - plan: plan, - user: user, - question: Question.find_by(text: "What types/formats of data will you collect?")}, - {text: "We will store the data on our departmental server and then move it to a commercial data repository afterward.", - plan: plan, - user: user, - question: Question.find_by(text: "How will you store the data and how will it be preserved?")}, - - {text: "We want people to be able to access it. ", - plan: plan, - user: user, - question: Question.find_by(text: "What is your policy for long term access to your dataset?")}, - {plan: plan, - user: user, - question: drop_down_question, - question_options: [QuestionOption.find_by(text: "institutional servers")]}, - {plan: plan, - user: user, - question: multi_select_question, - question_options: [QuestionOption.find_by(text: "image/video"), - QuestionOption.find_by(text: "other")]}, - {plan: plan, - user: user, - question: radio_button_question, - question_options: [QuestionOption.find_by(text: "archive files (e.g. tar, zip)"), - QuestionOption.find_by(text: "csv files")]}, - {text: "Yes", - plan: plan, - user: user, - question: Question.find_by(text: "Will software accompany your dataset?")}, - {text: "On a local server", - plan: plan, - user: user, - question: Question.find_by(text: "Where will you store your data during the research period?")}, - {text: "2018-05-01 00:00:01", - plan: plan, - user: user, - question: Question.find_by(text: "When will your data be available for public consumption?")} -] -answers.map{ |a| Answer.create!(a) if Answer.where(plan: a[:plan], user: a[:user], question: a[:question]).empty? } -=end diff --git a/lib/assets/javascripts/application.js b/lib/assets/javascripts/application.js index 196a5ee..53f88e0 100644 --- a/lib/assets/javascripts/application.js +++ b/lib/assets/javascripts/application.js @@ -21,6 +21,7 @@ import './views/notes/index'; import './views/org_admin/phases/new_edit'; import './views/org_admin/phases/preview'; +import './views/org_admin/phases/show'; import './views/org_admin/question_options/index'; import './views/org_admin/questions/sharedEventHandlers'; import './views/org_admin/sections/index'; diff --git a/lib/assets/javascripts/utils/ariatiseForm.js b/lib/assets/javascripts/utils/ariatiseForm.js index 3048118..b1f4b5a 100644 --- a/lib/assets/javascripts/utils/ariatiseForm.js +++ b/lib/assets/javascripts/utils/ariatiseForm.js @@ -50,7 +50,11 @@ // Otherwise if the element is required validate based on its type } else if ($(el).attr('aria-required') === 'true') { if ($(el).is('input')) { - return $(el).attr('type'); // available types at https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Form__types + const type = $(el).attr('type'); // available types at https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Form__types + if (type === 'checkbox' && $(el).closest('fieldset').length > 0) { + return 'multi-checkbox'; + } + return type; } else if ($(el).is('select')) { return 'select'; } else if ($(el).is('.tinymce')) { @@ -75,23 +79,17 @@ }; const addAsterisk = (el) => { const asterisk = '* '; + const type = getValidationTypeForElement(el); if (isObject(el)) { - switch (getValidationTypeForElement(el)) { - case 'checkbox': { - el.after(asterisk); - break; - } - case 'radio': { - const legend = el.closest('fieldset').find('legend'); - legend.html(`${asterisk}${legend.text()}`); - break; - } - default: { - const label = el.closest('.form-group').find('label'); - if (isObject(label)) { - $(label[0]).before(asterisk); - } - break; + if (el.closest('fieldset').length > 0 && el.closest('fieldset').find('legend').length > 0) { + const legend = el.closest('fieldset').find('legend'); + legend.html(`${asterisk} ${legend.html()}`); + } else if (type === 'checkbox' || type === 'radio') { + el.after(asterisk); + } else { + const label = el.closest('.form-group').find('label'); + if (isObject(label)) { + $(label[0]).before(asterisk); } } } @@ -110,24 +108,28 @@ case 'tinymce': return Tinymce.findEditorById($(el).attr('id')).getContent(); case 'checkbox': + case 'multi-checkbox': return ($(el).is(':checked') ? 'checked' : ''); default: return $(el).val(); } }; -const isValid = (type, value) => { +const isValid = (el) => { // TODO add more validation for each new type coming along by: // 1. defining a function at dmproadmap.utils.validate // 2. adding the case in the switch below + const type = getValidationTypeForElement(el); + const value = getValue(type, el); // See if a specific data-validation was specified switch (type) { case 'text': - return validator.isValidText(value); case 'textarea': - return validator.isValidText(value); case 'tinymce': + case 'select': + case 'radio': + case 'js-combobox': return validator.isValidText(value); case 'number': return validator.isValidNumber(value); @@ -135,13 +137,10 @@ return validator.isValidEmail(value); case 'password': return validator.isValidPassword(value); - case 'radio': - return validator.isValidText(value); - case 'select': case 'checkbox': - return validator.isValidText(value); - case 'js-combobox': - return validator.isValidText(value); + return validator.isValidCheckbox(el); + case 'multi-checkbox': + return validator.isValidMultiCheckbox(el); default: return false; } @@ -150,7 +149,6 @@ const getDefaultValidationMessage = (type) => { switch (type) { case 'text': - return getConstant('VALIDATION_MESSAGE_TEXT'); case 'textarea': return getConstant('VALIDATION_MESSAGE_TEXT'); case 'number': @@ -163,6 +161,8 @@ return getConstant('VALIDATION_MESSAGE_RADIO'); case 'checkbox': return getConstant('VALIDATION_MESSAGE_CHECKBOX'); + case 'multi-checkbox': + return getConstant('VALIDATION_MESSAGE_MULTI_CHECKBOX'); case 'js-combobox': return getConstant('VALIDATION_MESSAGE_SELECT'); default: @@ -202,14 +202,13 @@ const target = $(el); const id = target.attr('id'); const typ = getValidationTypeForElement(target); - target.attr('validation-help-block', `help-${id}`); const help = blockHelp(`help-${id}`, getValidationMessage(el)); - // If its a radio then the 'required' status should apply to the group - if (typ === 'radio') { - target.closest('.form-group').before(help); - // If its a checkbox, add the help block below the checkbox container - } else if (typ === 'checkbox') { - target.closest('.checkbox').after(help); + + target.attr('validation-help-block', `help-${id}`); + if (target.closest('fieldset').length > 0 && target.closest('fieldset').find('legend').length > 0) { + target.closest('fieldset').find('legend').after(help); + } else if (typ === 'radio' || typ === 'checkbox') { + target.closest('.form-group').after(help); } else { target.after(help); } @@ -223,12 +222,10 @@ let anyInvalid = false; let firstInvalid; validatable.each((i, el) => { - const type = getValidationTypeForElement(el); const required = ($(el).attr('aria-required') && $(el).attr('aria-required') === 'true'); - // If the field is required OR its not empty if (required || $(el).val().trim() !== '') { - if (isValid(type, getValue(type, el))) { + if (isValid($(el))) { valid(el); } else { anyInvalid = true; diff --git a/lib/assets/javascripts/utils/isValidInputType.js b/lib/assets/javascripts/utils/isValidInputType.js index 0a54d99..d116019 100644 --- a/lib/assets/javascripts/utils/isValidInputType.js +++ b/lib/assets/javascripts/utils/isValidInputType.js @@ -1,4 +1,4 @@ -import { isString, isNumber } from './isType'; +import { isObject, isString, isNumber } from './isType'; import getConstant from '../constants'; /* Validates whether or not the value passed matches to a valid email @@ -63,3 +63,17 @@ } return false; }; + +export const isValidCheckbox = (el) => { + if (isObject(el)) { + return el.is(':checked'); + } + return false; +}; + +export const isValidMultiCheckbox = (el) => { + if (isObject(el) && isObject(el.closest('fieldset'))) { + return el.closest('fieldset').find('input:checked').length > 0; + } + return false; +}; diff --git a/lib/assets/javascripts/utils/notificationHelper.js b/lib/assets/javascripts/utils/notificationHelper.js index 9380dbc..7e29d57 100644 --- a/lib/assets/javascripts/utils/notificationHelper.js +++ b/lib/assets/javascripts/utils/notificationHelper.js @@ -1,32 +1,58 @@ import { isString, isObject } from './isType'; + /* Helpers that will display the specified message in in the notification area at the top of the page */ -export const renderNotice = (msg) => { + +export function hideNotifications() { + $('#notification-area') + .addClass('hide') + .removeClass('notification-area--floating'); +} + +function renderMessage(options = {}) { const notificationArea = $('#notification-area'); - if (isString(msg) && isObject(notificationArea)) { - notificationArea.removeClass('alert-warning').addClass('alert-info'); + if (isString(options.message) && isObject(notificationArea)) { + notificationArea + .removeClass('alert-info', 'alert-warning') + .addClass(options.className); + + if (options.floating) { + notificationArea.addClass('notification-area--floating'); + } + notificationArea.find('i, span').remove(); notificationArea.append(` - ${msg}`); + + ${options.message} + `); + notificationArea.removeClass('hide'); + + if (options.autoDismiss) { + setTimeout(() => { hideNotifications(); }, 5000); + } } -}; +} -export const renderAlert = (msg) => { - const notificationArea = $('#notification-area'); +export function renderNotice(msg, options = {}) { + renderMessage({ + message: msg, + icon: 'check-circle', + className: 'alert-info', + floating: options.floating === true, + autoDismiss: options.autoDismiss === true, + }); +} - if (isString(msg) && isObject(notificationArea)) { - notificationArea.removeClass('alert-info').addClass('alert-warning'); - notificationArea.find('i, span').remove(); - notificationArea.append(` - ${msg}`); - notificationArea.removeClass('hide'); - } -}; - -export const hideNotifications = () => { - $('#notification-area').addClass('hide'); -}; +export function renderAlert(msg, options = {}) { + renderMessage({ + message: msg, + icon: 'times-circle', + className: 'alert-warning', + floating: options.floating === true, + autoDismiss: options.autoDismiss === true, + }); +} diff --git a/lib/assets/javascripts/utils/panelHeading.js b/lib/assets/javascripts/utils/panelHeading.js index 194657f..670c048 100644 --- a/lib/assets/javascripts/utils/panelHeading.js +++ b/lib/assets/javascripts/utils/panelHeading.js @@ -1,5 +1,8 @@ $(() => { $('.heading-button').on('click', (e) => { - $(e.currentTarget).find('i.fa').toggleClass('fa-plus').toggleClass('fa-minus'); + $(e.currentTarget) + .find('i.fa-plus, i.fa-minus') + .toggleClass('fa-plus') + .toggleClass('fa-minus'); }); }); diff --git a/lib/assets/javascripts/views/devise/registrations/edit.js b/lib/assets/javascripts/views/devise/registrations/edit.js index d976b2e..bc56b57 100644 --- a/lib/assets/javascripts/views/devise/registrations/edit.js +++ b/lib/assets/javascripts/views/devise/registrations/edit.js @@ -15,7 +15,7 @@ const sensitiveInfoCheck = (event) => { const originalEmail = $('#original_email').val(); const originalOrg = $('#original_org').val(); - const email = $('#personal_details_registration_form #user_email').val(); + const email = $('#personal_details_registration_form [name="user[email]"]').val(); const org = $('#personal_details_registration_form #user_org_id').val(); const pwd = $('#password-confirmation input[name="user[current_password]"]').val(); const orgConfirm = $('#confirm_org_change').is(':checked'); @@ -23,7 +23,6 @@ $('#email-change').addClass('hide'); $('#org-change').addClass('hide'); - // If the Email has changed show the Password confirmation if (isString(originalEmail) && isString(email)) { if (originalEmail.toLowerCase() !== email.toLowerCase() && !isValidPassword(pwd)) { @@ -53,7 +52,8 @@ // Devise seems to require both the password and current_password so sync them // when the user enters their password in the modal $('#password-confirmation input[name="user[current_password]"]').change((e) => { - $('#password-confirmation #user_password').val($(e.target).val()); + $('#password-confirmation input[name="user[password]"]') + .val($(e.target).val()); }); // Submit the form when the user clicks the confirmation button on the modal diff --git a/lib/assets/javascripts/views/org_admin/phases/new_edit.js b/lib/assets/javascripts/views/org_admin/phases/new_edit.js index e04c4cd..478ac38 100644 --- a/lib/assets/javascripts/views/org_admin/phases/new_edit.js +++ b/lib/assets/javascripts/views/org_admin/phases/new_edit.js @@ -14,7 +14,7 @@ Tinymce.init({ selector: '.phase' }); ariatiseForm({ selector: '.phase_form' }); - const parentSelector = '#sections_accordion'; + const parentSelector = '.section-group'; const initQuestion = (context) => { const target = $(context); @@ -117,7 +117,7 @@ }); // Handle the section that has focus on initial page load - const currentSection = $('#sections_accordion .in'); + const currentSection = $('.sectiongroup .in'); if (currentSection.length > 0) { initSection(`#${currentSection.attr('id')}`); } diff --git a/lib/assets/javascripts/views/org_admin/phases/show.js b/lib/assets/javascripts/views/org_admin/phases/show.js new file mode 100644 index 0000000..ed5b270 --- /dev/null +++ b/lib/assets/javascripts/views/org_admin/phases/show.js @@ -0,0 +1,46 @@ +import 'jquery-ui/ui/widgets/sortable'; +import { renderAlert } from '../../../utils/notificationHelper'; + +$(() => { + // Is there already one prefix section on this Phase? + // + // draggableSections - A jQuery object, the sortable element. + // + // Returns Boolean + function prefixSectionExists(draggableSections) { + return !!draggableSections + .has('[data-modifiable=true]:nth-child(1)').length && + !!draggableSections.has('[data-modifiable=true]:nth-child(2)').length; + } + + // Initialize the draggable-sections element as a jQuery sortable. + // Read the docs here for more info: http://api.jqueryui.com/sortable/ + $('.draggable-sections').sortable({ + handle: 'i.fa-arrows', + axis: 'y', + cursor: 'move', + beforeStop() { + if (prefixSectionExists($(this))) { + // Prevent the sort action from completing. Moves element back to source + $(this).sortable('cancel'); + + renderAlert(`You can only place one section before the funder template. + Multiple can go afterwards.`, { + floating: true, autoDismiss: true, + }); + } + }, + update() { + // Collect the section-id from each section element on the page. + const sectionIds = $('.section[data-section-id]') + .map((i, element) => $(element).data('section-id')).toArray(); + + // Post the section IDs to the server in their new order on the page. + $.rails.ajax({ + url: $(this).data('url'), + method: 'post', + data: { sort_order: sectionIds }, + }); + }, + }); +}); diff --git a/lib/assets/javascripts/views/org_admin/question_options/index.js b/lib/assets/javascripts/views/org_admin/question_options/index.js index 0b939b9..19ec44d 100644 --- a/lib/assets/javascripts/views/org_admin/question_options/index.js +++ b/lib/assets/javascripts/views/org_admin/question_options/index.js @@ -11,7 +11,8 @@ const source = e.target; const last = $(source).closest('[data-attribute="question_options"]').find('[data-attribute="question_option"]').last(); const cloned = last.clone(true); - const array = $(cloned).find('[id$="_number"]').prop('id').match(/_[\d]_+/); + const array = $(cloned).find('[id$="_number"]').prop('id').match(/_[\d]*?_+/); + if (array) { const index = Number(array[0].replace(/_/g, '')); // Reset values for the new cloned inputs diff --git a/lib/assets/javascripts/views/orgs/admin_edit.js b/lib/assets/javascripts/views/orgs/admin_edit.js index 165aa40..c3cda41 100644 --- a/lib/assets/javascripts/views/orgs/admin_edit.js +++ b/lib/assets/javascripts/views/orgs/admin_edit.js @@ -22,6 +22,7 @@ Tinymce.init({ selector: '#org_feedback_email_msg' }); toggleFeedback(); enableValidations($('#edit_org_profile_form .form-group:not(.link-input)')); + enableValidations($('#edit_org_feedback_form .form-group')); // Only enable validations on the links section if it has values initially $('.link').each((idx, el) => { const linkVal = $(el).find('input[name="link_link"]').val(); diff --git a/lib/assets/package-lock.json b/lib/assets/package-lock.json index 37da536..09ead50 100644 --- a/lib/assets/package-lock.json +++ b/lib/assets/package-lock.json @@ -5434,10 +5434,10 @@ "jquery": "3.2.1" } }, - "jquery-ui-dist": { + "jquery-ui": { "version": "1.12.1", - "resolved": "https://registry.npmjs.org/jquery-ui-dist/-/jquery-ui-dist-1.12.1.tgz", - "integrity": "sha1-XAgV08xvkP9fqvWyaKbiO0ypBPo=" + "resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.12.1.tgz", + "integrity": "sha1-vLQEXI3QU5wTS8FIjN0+dop6nlE=" }, "jquery-ujs": { "version": "1.2.2", diff --git a/lib/assets/package.json b/lib/assets/package.json index 730cfef..6849adf 100644 --- a/lib/assets/package.json +++ b/lib/assets/package.json @@ -24,7 +24,7 @@ "font-awesome": "^4.7.0", "jquery": "3.2.1", "jquery-accessible-autocomplete-list-aria": "1.5.5", - "jquery-ui-dist": "1.12.1", + "jquery-ui": "^1.12.1", "jquery-ujs": "1.2.2", "js-cookie": "2.1.4", "moment": "^2.20.1", diff --git a/lib/assets/stylesheets/application.scss b/lib/assets/stylesheets/application.scss index d260a65..1eff42f 100644 --- a/lib/assets/stylesheets/application.scss +++ b/lib/assets/stylesheets/application.scss @@ -24,3 +24,21 @@ clear: left; margin-bottom: 10px; } + +// TODO: Move this into it's own file once CSS has been fixed +.notification-area { +} +.notification-area--floating { + position: fixed; + top: 4rem; + z-index: 1000; + // Take up width of the page on mobile + right: 5vw; + width: 90vw; + + // Non-mobile settings + @media (min-width: #{$screen-sm-min}) { + right: 4rem; + width: 300px; + } +} diff --git a/lib/assets/stylesheets/dmproadmap.scss b/lib/assets/stylesheets/dmproadmap.scss index 5b2909c..e4c162e 100644 --- a/lib/assets/stylesheets/dmproadmap.scss +++ b/lib/assets/stylesheets/dmproadmap.scss @@ -1,3 +1,5 @@ +@import "dmproadmap/notifications"; + /**** Main layout configuration ****/ /* For sticky footer */ diff --git a/lib/assets/stylesheets/overrides.scss b/lib/assets/stylesheets/overrides.scss index 6383889..c12b71c 100644 --- a/lib/assets/stylesheets/overrides.scss +++ b/lib/assets/stylesheets/overrides.scss @@ -200,7 +200,7 @@ } /* FONTAWESOME STYLING */ -.fa { +.fa:not(.small) { font-size: 2rem; } .fa-reverse { @@ -318,12 +318,12 @@ } /* footer */ -#footer-navbar { +footer { + position: absolute; bottom: 0; margin-top: 20px; margin-bottom: -20px; width: 100%; - position: absolute; } @@ -436,7 +436,7 @@ left: 0; width: 100%; background: $white; - z-index: 99; + z-index: 1100; } .combobox-suggestion { color: $grey; @@ -552,9 +552,24 @@ /* generic styling */ fieldset { border: none; + + input[type="radio"], + input[type="checkbox"] { + margin: 0px; + vertical-align: middle; + } + + .checkbox, + .radio { + margin-top: 0px; + margin-bottom: 3px; + } } + legend { border: none; + font-size: 14px; + font-weight: bold; margin-bottom: 5px; text-transform: capitalize; } @@ -568,7 +583,6 @@ -webkit-box-shadow: 0px 0px 0px 0px #000; box-shadow: 0px 0px 0px 0px #000; } - legend.project-details { font-weight: bold; font-size: 100%; @@ -585,18 +599,18 @@ } /* Skip to main content link styling */ -div.skip a { - position:absolute; - left:-10000px; - top:auto; - width:1px; - height:1px; +div.skip a { + position:absolute; + left:-10000px; + top:auto; + width:1px; + height:1px; overflow: hidden !important; -} - -div.skip a:focus { - width:auto; - height:auto; +} + +div.skip a:focus { + width:auto; + height:auto; overflow:visible !important; } diff --git a/lib/assets/webpack.config.js b/lib/assets/webpack.config.js index 651ba9c..8726bbc 100644 --- a/lib/assets/webpack.config.js +++ b/lib/assets/webpack.config.js @@ -5,6 +5,7 @@ const rootPath = `${__dirname}/../..`; const destPath = `${rootPath}/public`; +const watch = process.argv.indexOf('--no-watch') === -1; const production = process.argv.indexOf('-p') !== -1; const jsOutputFile = production ? 'javascripts/[name]-[hash].js' : 'javascripts/[name].js'; const cssOutputFile = production ? 'stylesheets/[name]-[hash].css' : 'stylesheets/[name].css'; @@ -97,11 +98,13 @@ }); } }); - this.plugin('done', (stats) => { // Changes value for ASSET_FINGERPRINT hash + this.plugin('done', (stats) => { + // Changes value for ASSET_FINGERPRINT hash const output = `ASSET_FINGERPRINT = "${stats.hash}"`; - fs.writeFileSync(`${rootPath}/config/initializers/fingerprint.rb`, output, 'utf-8'); + fs.writeFileSync(`${rootPath}/config/initializers/fingerprint.rb`, + output, 'utf-8'); }); }, ], - watch: !production, + watch: watch, }; diff --git a/lib/branding.rb b/lib/branding.rb index 9427e98..2400d5c 100644 --- a/lib/branding.rb +++ b/lib/branding.rb @@ -5,15 +5,18 @@ # Loads branding config from YAML file. # - # @param keys [Array] A list of the keys to return configs for. + # keys - A list of the keys to return configs for. # - # @example Return a value + # Examples: + # + # # Return a value # Branding.fetch(:settings, :should_work) # => true # Branding.fetch(:settings, :email) # => 'user@example.com' # Branding.fetch(:settings, :missing) # => nil - # @return [Object] The value of the config + # + # Returns Object def fetch(*keys) keys = keys.map(&:to_sym) Rails.configuration.branding.dig(*keys) end -end \ No newline at end of file +end diff --git a/lib/cleanup.rb b/lib/cleanup.rb new file mode 100644 index 0000000..5b71994 --- /dev/null +++ b/lib/cleanup.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# Private: Utility module providing tools to clean up the codebase over time. +module Cleanup +end diff --git a/lib/cleanup/deprecators.rb b/lib/cleanup/deprecators.rb new file mode 100644 index 0000000..fd59e57 --- /dev/null +++ b/lib/cleanup/deprecators.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Cleanup + # Private: Custom deprecator classes to use while we refactor the code for + # better readability. + module Deprecators + end +end diff --git a/lib/cleanup/deprecators/get_deprecator.rb b/lib/cleanup/deprecators/get_deprecator.rb new file mode 100644 index 0000000..cde5b4a --- /dev/null +++ b/lib/cleanup/deprecators/get_deprecator.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Cleanup + module Deprecators + + # Used to deprecate methods with non-idiomatic getter names. + # + # There are methods in the code with non-idiomatic method names. Typically, + # Ruby getters are named as nouns for the attribute they are returning. + # Prefer User#status over User#get_status. + # + class GetDeprecator + + ## + # Default message to display to developer when deprecated method called. + MESSAGE = "%s is deprecated. "\ + "Instead, you should use: %s. "\ + "Read #{__FILE__} for more information." + + # Message printed to STDOUT when a deprecated method is called. + def deprecation_warning(deprecated_method, _message, _backtrace = nil) + new_method = deprecated_method.to_s.gsub(/^get\_/, '') + message = format(MESSAGE, + deprecated_method: deprecated_method, + new_method: new_method) + Kernel.warn(message) + end + + end + end +end diff --git a/lib/cleanup/deprecators/predicate_deprecator.rb b/lib/cleanup/deprecators/predicate_deprecator.rb new file mode 100644 index 0000000..19ca2a5 --- /dev/null +++ b/lib/cleanup/deprecators/predicate_deprecator.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Cleanup + module Deprecators + + # Used to deprecate methods that are predicate methods without question mks + # + # There are methods in the code with non-idiomatic method names. Typically, + # Ruby predicates are named as verbs in the infinitive with a question mark. + # + class PredicateDeprecator + + ## + # Default message to display to developer when deprecated method called. + MESSAGE = "%s is deprecated. "\ + "Instead, you should use: %s. "\ + "Read #{__FILE__} for more information." + + # Message printed to STDOUT when a deprecated method is called. + def deprecation_warning(deprecated_method, _message, _backtrace = nil) + new_method = "#{deprecated_method.to_s}?" + message = format(MESSAGE, + deprecated_method: deprecated_method, + new_method: new_method) + Kernel.warn(message) + end + + end + end +end diff --git a/lib/cleanup/deprecators/set_deprecator.rb b/lib/cleanup/deprecators/set_deprecator.rb new file mode 100644 index 0000000..cd5a4aa --- /dev/null +++ b/lib/cleanup/deprecators/set_deprecator.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Cleanup + module Deprecators + + # Used to deprecate methods with non-idiomatic setter names. + # + # There are methods in the code with non-idiomatic method names. Typically, + # Ruby getters are named as nouns for the attribute they are returning. + # Prefer User#status= over User#set_status. + # + class SetDeprecator + + ## + # Default message to display to developer when deprecated method called. + MESSAGE = "%s is deprecated. "\ + "Instead, you should use: %s. "\ + "Read #{__FILE__} for more information." + + # Message printed to STDOUT when a deprecated method is called. + def deprecation_warning(deprecated_method, _message, _backtrace = nil) + new_method = deprecated_method.to_s.gsub(/^set\_/, '').gsub(/\Z/, '=') + message = format(MESSAGE, + deprecated_method: deprecated_method, + new_method: new_method) + Kernel.warn(message) + end + + end + end +end diff --git a/lib/data_cleanup.rb b/lib/data_cleanup.rb new file mode 100644 index 0000000..5f105fd --- /dev/null +++ b/lib/data_cleanup.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require_relative "data_cleanup/model_check" +require_relative "data_cleanup/instance_check" +require_relative "data_cleanup/reporting" +require_relative "data_cleanup/rules" + +module DataCleanup + + COLOR_CODES = { red: 31, green: 32 } + + module_function + + def logger + @logger ||= Logger.new(Rails.root.join("log", "validations.log")) + end + + def display(message, inline: false, color: nil) + message = "#{message}\n" unless inline + message = "\e[#{COLOR_CODES[color]}m#{message}\e[0m" if color + print message + end +end diff --git a/lib/data_cleanup/README.md b/lib/data_cleanup/README.md new file mode 100644 index 0000000..0574bf3 --- /dev/null +++ b/lib/data_cleanup/README.md @@ -0,0 +1,80 @@ +# DataCleanups + +This module is used to clean database records that have become invalid since data validation rules have changed within the app. + +## Rake tasks + +This module adds two rake tasks, one for finding invalid records, and one for fixing them. + +### Usage + +Find invalid records. + +#### Warning: + +This will iterate over every record on your database. It could easily take over an hour to run. + +``` bash +$ rake data_cleanup:find_invalid_records +``` + +Find invalid records for a given list of models. + +``` bash +$ rake data_cleanup:find_invalid_records INCLUDE=Note,User +``` + +...or... + +``` bash +$ rake data_cleanup:find_invalid_records EXCLUDE=Annotation +``` + +--- + +Fix invalid records based on the rules defined in `lib/data_cleanup/rules`. + +#### Warning: + +This will update the records on your database, often using `update_all`. Make sure you: + +- **have a backup of your database** +- **you are comfortable you understand what each of these rules are doing to your data** + +``` bash +$ rake data_cleanup:clean_invalid_records +``` + +Or to clean a given table... + +``` bash +$ rake data_cleanup:clean_invalid_records INCLUDE=Question +``` + +To avoid a given table... + +``` bash +$ rake data_cleanup:clean_invalid_records EXAMPLE=Plan +``` + +## Rules + +Each type of data error is fixed separately. + +These are defined in `lib/data_cleanup/rules`. + +### Creating a new rule + +You can create a new rule by running the following genrator: + +``` bash +$ rails g data_cleanup_rule user/fix_missing_emails +``` + +This will create a file `lib/data_cleanup/rules/user/fix_missing_emails.rb` which contains the rules for updating users with missing emails. + +Feel free to add your own rules where neccesary to fix your own data. + +## Logging output + +Output from the Rake tasks will be logged to `log/validations.log`. diff --git a/lib/data_cleanup/instance_check.rb b/lib/data_cleanup/instance_check.rb new file mode 100644 index 0000000..02d9c5e --- /dev/null +++ b/lib/data_cleanup/instance_check.rb @@ -0,0 +1,29 @@ +require_relative "reporting" + +module DataCleanup + # Check whether a given database record is valid or not + class InstanceCheck + # frozen_string_literal: true + + def call(instance) + DataCleanup.logger.info("Checking #{instance.class}##{instance.id}...") + Reporting.total_record_count += 1 + begin + if instance.invalid? + DataCleanup.logger.info(<<~TEXT) + Instance #{instance.class}##{instance.id} invalid! + Errors: #{instance.errors.full_messages.to_sentence} + TEXT + Reporting.invalid_record_count += 1 + Reporting.invalid_records << instance + DataCleanup.display("F", inline: true) + else + DataCleanup.logger.info("Instance #{instance.class}##{instance.id} valid!") + DataCleanup.display(".", inline: true) + end + rescue Dragonfly::Job::Fetch::NotFound + DataCleanup.display(".", inline: true) + end + end + end +end \ No newline at end of file diff --git a/lib/data_cleanup/model_check.rb b/lib/data_cleanup/model_check.rb new file mode 100644 index 0000000..b1b793b --- /dev/null +++ b/lib/data_cleanup/model_check.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true +module DataCleanup + # Checks whether all records for a given model are valid or not + class ModelCheck + + require_relative "reporting" + + attr_reader :model + + def initialize(model) + @model = model + end + + def call + rule, models = ARGV[1].to_s.split("=") + case rule + when 'INCLUDE' + return unless model.model_name.in?(models.split(",")) + when 'EXCLUDE' + return if model.model_name.in?(models.split(",")) + end + DataCleanup.display "Checking #{model.model_name.plural}:" + model.find_in_batches do |batch| + instance_check = InstanceCheck.new + batch.each { |instance| instance_check.(instance) } + end + DataCleanup.display "" + end + end +end diff --git a/lib/data_cleanup/reporting.rb b/lib/data_cleanup/reporting.rb new file mode 100644 index 0000000..50c13e0 --- /dev/null +++ b/lib/data_cleanup/reporting.rb @@ -0,0 +1,37 @@ +module DataCleanup + # Report the status of the data validations after checks have been run. + module Reporting + mattr_accessor :total_record_count + mattr_accessor :invalid_record_count + mattr_accessor :invalid_records + mattr_accessor :issues_found + + self.total_record_count = 0 + self.invalid_record_count = 0 + self.invalid_records = [] + self.issues_found = [] + + module_function + + # Prepare the report for printing to log and STDOUT + def prepare! + invalid_records.each do |record| + record.errors.full_messages.each do |issue| + desc = "#{record.class.model_name} was invalid: #{issue}" + issues_found << desc unless issues_found.include?(desc) + end + end + end + + def report + issues_found.each do |issue| + DataCleanup.display issue + DataCleanup.logger.info issue + end + color = invalid_record_count.zero? ? :green : :red + DataCleanup.display(<<~TEXT, color: color) + Invalid records: #{invalid_record_count} / #{total_record_count} + TEXT + end + end +end diff --git a/lib/data_cleanup/rules.rb b/lib/data_cleanup/rules.rb new file mode 100644 index 0000000..155b86b --- /dev/null +++ b/lib/data_cleanup/rules.rb @@ -0,0 +1,7 @@ +require_relative "rules/base" + +module DataCleanup + module Rules + + end +end diff --git a/lib/data_cleanup/rules/annotation/fix_blank_text.rb b/lib/data_cleanup/rules/annotation/fix_blank_text.rb new file mode 100644 index 0000000..7589096 --- /dev/null +++ b/lib/data_cleanup/rules/annotation/fix_blank_text.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +module DataCleanup + module Rules + # Fix blank text on Annotation + module Annotation + + class FixBlankText < Rules::Base + + def description + "Fix blank text on Annotation" + end + + def call + ::Annotation.where(text: "").find_in_batches do |batches| + batches.each do |annotation| + log("Destroying Annotation##{annotation.id} since it has no text") + annotation.destroy + end + end + end + + end + + end + end +end diff --git a/lib/data_cleanup/rules/annotation/fix_duplicate_type.rb b/lib/data_cleanup/rules/annotation/fix_duplicate_type.rb new file mode 100644 index 0000000..be5dd7a --- /dev/null +++ b/lib/data_cleanup/rules/annotation/fix_duplicate_type.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true +module DataCleanup + module Rules + # Fix duplicate type on Annotation + module Annotation + + class FixDuplicateType < Rules::Base + + def description + "Fix duplicate type on Annotation" + end + + def call + ::Annotation.group(:question_id, :type, :org_id) + .count + .select { |k,v| v > 1 } + .each do |array, count| + + question_id = array.first + type = array.second + org_id = array.third + + log("Destroying all duplicate Annotations with question_id: #{question_id} "\ + "type: #{type} and org_id: #{org_id}") + + ::Annotation.where(question_id: question_id, type: type, org_id: org_id) + .order(updated_at: :desc) + .offset(1) + .destroy_all + end + end + + end + + end + end +end diff --git a/lib/data_cleanup/rules/answer/fix_blank_user.rb b/lib/data_cleanup/rules/answer/fix_blank_user.rb new file mode 100644 index 0000000..a17575d --- /dev/null +++ b/lib/data_cleanup/rules/answer/fix_blank_user.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true +module DataCleanup + module Rules + # Fix blank user on Answer + module Answer + class FixBlankUser < Rules::Base + + def description + "Fix blank user on Answer" + end + + def call + ::Answer.joins("LEFT OUTER JOIN users ON users.id = answers.user_id") + .where(users: { id: nil }) + .includes(plan: { roles: :user }).each do |answer| + + if answer.plan.owner.present? + log("Updating Answer##{answer.id} with user: #{answer.plan.owner}") + answer.update(user: answer.plan.owner) + elsif answer.plan.roles.any? + user = answer.plan.roles.first.user + log("Updating Answer##{answer.id} with user: #{user}") + answer.update(user: user) + else + log("Destroying orphaned Answer##{answer.id}") + answer.destroy + end + end + + end + + end + end + end +end diff --git a/lib/data_cleanup/rules/answer/fix_duplicate_question.rb b/lib/data_cleanup/rules/answer/fix_duplicate_question.rb new file mode 100644 index 0000000..d028842 --- /dev/null +++ b/lib/data_cleanup/rules/answer/fix_duplicate_question.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module DataCleanup + module Rules + # Fix duplicate question on Answer + module Answer + class FixDuplicateQuestion < Rules::Base + + def description + "Fix duplicate question on Answer" + end + + def call + # Take all answers that have duplicate question/plan combo... + dataset = ::Answer.group(:question_id, :plan_id) + .count.select { |k,v| v > 1 } + # Values looks like [{ [123, 199] => 2}, ...] + dataset.each do |values, count| + log("Destroying all Answers that are duplicates of earlier answers on the same question.") + # ... and destroy all duplicates, keeping the latest record + ::Answer.where(question: values.first, plan_id: values.last) + .order("updated_at DESC") + .offset(1) + .destroy_all + end + end + end + end + end +end diff --git a/lib/data_cleanup/rules/base.rb b/lib/data_cleanup/rules/base.rb new file mode 100644 index 0000000..cc828e4 --- /dev/null +++ b/lib/data_cleanup/rules/base.rb @@ -0,0 +1,22 @@ +module DataCleanup + module Rules + # Base class for rules to clean invalid database records + class Base + + def log(message) + DataCleanup.logger.info(message) + end + + # Description of the rule and how it's fixing the data + def description + self.class.name.humanize + end + + # Run this rule and fix data in the database. + def call + raise NotImplementedError, "Please define call() in #{self}" + end + + end + end +end diff --git a/lib/data_cleanup/rules/exported_plan/fix_blank_plan.rb b/lib/data_cleanup/rules/exported_plan/fix_blank_plan.rb new file mode 100644 index 0000000..5bb309f --- /dev/null +++ b/lib/data_cleanup/rules/exported_plan/fix_blank_plan.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module DataCleanup + module Rules + # Fix blank plan on ExportedPlan + module ExportedPlan + class FixBlankPlan < Rules::Base + + def description + "Fix blank plan on ExportedPlan" + end + + def call + # Find all exported plans where the corresponding plan doesn't exist. + ::ExportedPlan + .joins("LEFT OUTER JOIN plans on plans.id = exported_plans.plan_id") + .where(plans: { id: nil }).each do |exported_plan| + log("Destroying ExportedPlan##{exported_plan.id} where plan is nil") + exported_plan.destroy + end + end + end + end + end +end diff --git a/lib/data_cleanup/rules/org/fix_blank_abbreviation.example.yml b/lib/data_cleanup/rules/org/fix_blank_abbreviation.example.yml new file mode 100644 index 0000000..1133989 --- /dev/null +++ b/lib/data_cleanup/rules/org/fix_blank_abbreviation.example.yml @@ -0,0 +1,9 @@ +- + id: 1234 + name: University of Edinburgh + abbreviation: UoE +- + id: 1235 + name: De Montfort University + abbreviation: "DMU" +# ... \ No newline at end of file diff --git a/lib/data_cleanup/rules/org/fix_blank_abbreviation.rb b/lib/data_cleanup/rules/org/fix_blank_abbreviation.rb new file mode 100644 index 0000000..89c999b --- /dev/null +++ b/lib/data_cleanup/rules/org/fix_blank_abbreviation.rb @@ -0,0 +1,29 @@ +module DataCleanup + module Rules + module Org + class FixBlankAbbreviation < Rules::Base + + YAML_FILE_PATH = Rails.root.join("lib", "data_cleanup", "rules", "org", + "fix_blank_abbreviation.yml") + + def description + "Fix blank abbreviation on Org" + end + + def call + if File.exists?(YAML_FILE_PATH) + YAML.load_file(YAML_FILE_PATH).each do |attributes| + attributes = attributes.with_indifferent_access + name = attributes['name'] + abbreviation = attributes['abbreviation'] + log("Adding abbreviation #{abbreviation} to Org '#{name}'") + ::Org.where(name: name).update_all(abbreviation: abbreviation) + end + else + raise "Please create a YAML file at #{YAML_FILE_PATH}" + end + end + end + end + end +end diff --git a/lib/data_cleanup/rules/org/fix_blank_feedback_email_msg.rb b/lib/data_cleanup/rules/org/fix_blank_feedback_email_msg.rb new file mode 100644 index 0000000..bc1bcac --- /dev/null +++ b/lib/data_cleanup/rules/org/fix_blank_feedback_email_msg.rb @@ -0,0 +1,28 @@ +module DataCleanup + module Rules + module Org + class FixBlankFeedbackEmailMsg < Rules::Base + + DEFAULT_MSG = <<~HTML +

Hello %{user_name}.

+

+ Your plan "%{plan_name}" has been submitted for feedback from an administrator + at your organisation. If you have questions + pertaining to this action, please contact us at %{organisation_email}. +

+ HTML + + def description + "Fix orgs feedback_email_message is blank" + end + + def call + ids = ::Org.where(feedback_enabled: true, feedback_email_msg: "").pluck(:id) + log("Adding default feedback_enabled for orgs: #{ids}") + ::Org.where(feedback_enabled: true, feedback_email_msg: "") + .update_all(feedback_email_msg: DEFAULT_MSG) + end + end + end + end +end diff --git a/lib/data_cleanup/rules/org/fix_blank_feedback_email_subject.rb b/lib/data_cleanup/rules/org/fix_blank_feedback_email_subject.rb new file mode 100644 index 0000000..ac637f9 --- /dev/null +++ b/lib/data_cleanup/rules/org/fix_blank_feedback_email_subject.rb @@ -0,0 +1,21 @@ +module DataCleanup + module Rules + module Org + class FixBlankFeedbackEmailSubject < Rules::Base + + DEFAULT_SUBJECT = "%{application_name}: Your plan has been submitted for feedback" + + def description + "Fix orgs where feedback_email_subject is blank" + end + + def call + ids = ::Org.where(feedback_enabled: true, feedback_email_subject: "").pluck(:id) + log("Adding default feedback_email_subject for orgs: #{ids}") + ::Org.where(feedback_enabled: true, feedback_email_subject: "") + .update_all(feedback_email_subject: DEFAULT_SUBJECT) + end + end + end + end +end diff --git a/lib/data_cleanup/rules/org/fix_blank_language.rb b/lib/data_cleanup/rules/org/fix_blank_language.rb new file mode 100644 index 0000000..b738e8a --- /dev/null +++ b/lib/data_cleanup/rules/org/fix_blank_language.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module DataCleanup + module Rules + module Org + class FixBlankLanguage < Rules::Base + + DEFAULT_LANGUAGE = Language.find_by(abbreviation: FastGettext.default_locale) + + def description + "Fix blank language on Org" + end + + def call + ids = ::Org.where(language: nil).pluck(:id) + log("Setting language to #{DEFAULT_LANGUAGE} for Orgs: #{ids}") + ::Org.where(language: nil).update_all(language_id: DEFAULT_LANGUAGE.id) + end + end + end + end +end diff --git a/lib/data_cleanup/rules/org/fix_invalid_email.rb b/lib/data_cleanup/rules/org/fix_invalid_email.rb new file mode 100644 index 0000000..d5498a8 --- /dev/null +++ b/lib/data_cleanup/rules/org/fix_invalid_email.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true +module DataCleanup + module Rules + # Fix invalid email on Org + module Org + class FixInvalidEmail < Rules::Base + + def description + "Fix invalid email on Org" + end + + def call + orgs_with_contact_email = ::Org.all.select(&:contact_email?) + orgs_with_invalid_contact_email = orgs_with_contact_email.select do |org| + validator = EmailValidator.new(allow_nil: true, attributes: :contact_email) + validator.validate_each(org, :contact_email, org.contact_email) + end + + orgs_with_invalid_contact_email.each do |org| + log("Removing contact email from Org##{org.id}") + org.contact_email = nil + org.save(validate: false) + end + + ::Org.where(contact_email: "").update_all(contact_email: nil) + end + end + end + end +end diff --git a/lib/data_cleanup/rules/org/fix_name_with_spaces.rb b/lib/data_cleanup/rules/org/fix_name_with_spaces.rb new file mode 100644 index 0000000..5c6bfc7 --- /dev/null +++ b/lib/data_cleanup/rules/org/fix_name_with_spaces.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +module DataCleanup + module Rules + # Fix name with space on Org + module Org + class FixNameWithSpace < Rules::Base + + def description + "Fix name with leading or trailing space on Org" + end + + def call + ::Org.update_all("name = TRIM(name)") + end + end + end + end +end diff --git a/lib/data_cleanup/rules/phase/fix_duplicate_number.rb b/lib/data_cleanup/rules/phase/fix_duplicate_number.rb new file mode 100644 index 0000000..a35b1dc --- /dev/null +++ b/lib/data_cleanup/rules/phase/fix_duplicate_number.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module DataCleanup + module Rules + # Fix duplicate number on Phase + module Phase + class FixDuplicateNumber < Rules::Base + + def description + "Fix duplicate number on Phase" + end + + def call + data = ::Phase.group(:number, :template_id) + .count + .select { |k,v| v > 1 } + data.each do |values, count| + number, template_id = *values + ids = ::Phase.where(template_id: template_id) + .order("number, id") + .pluck(:id) + template = ::Template.find(template_id) + log("Reordering Phase number within Template##{template.id}") + ::Phase.update_numbers!(*ids, parent: template) + end + end + end + end + end +end diff --git a/lib/data_cleanup/rules/plan/fix_blank_title.rb b/lib/data_cleanup/rules/plan/fix_blank_title.rb new file mode 100644 index 0000000..0c1f594 --- /dev/null +++ b/lib/data_cleanup/rules/plan/fix_blank_title.rb @@ -0,0 +1,20 @@ +module DataCleanup + module Rules + module Plan + class FixBlankTitle < Rules::Base + + def description + "Fix blank title on Plan" + end + + def call + ids = ::Plan.where(title: [nil, '']).ids + ::Plan.find(ids).each do |plan| + log("Adding default title to Plan##{plan.id}") + plan.update(title: "My plan (#{plan.template.title})") + end + end + end + end + end +end diff --git a/lib/data_cleanup/rules/question/fix_duplicate_number.rb b/lib/data_cleanup/rules/question/fix_duplicate_number.rb new file mode 100644 index 0000000..65fb88d --- /dev/null +++ b/lib/data_cleanup/rules/question/fix_duplicate_number.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true +module DataCleanup + module Rules + # Fix duplicate number on Question + module Question + class FixDuplicateNumber < Rules::Base + + def description + "Fix duplicate number on Question" + end + + def call + data = ::Question.group(:number, :section_id) + .count + .select { |k,v| v > 1 } + data.each do |values, count| + number, section_id = *values + ids = ::Question.where(section_id: section_id) + .order("number ASC, created_at ASC") + .pluck(:id) + section = ::Section.find(section_id) + log("Reordering Question number in Section##{section.id}") + ::Question.update_numbers!(*ids, parent: section) + end + end + end + end + end +end diff --git a/lib/data_cleanup/rules/question_format/fix_blank_description.rb b/lib/data_cleanup/rules/question_format/fix_blank_description.rb new file mode 100644 index 0000000..cda21e1 --- /dev/null +++ b/lib/data_cleanup/rules/question_format/fix_blank_description.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true +module DataCleanup + module Rules + # Fix blank description on QuestionFormat + module QuestionFormat + class FixBlankDescription < Rules::Base + + def description + "Fix blank description on QuestionFormat" + end + + def call + ::QuestionFormat.where(description: "").each do |qf| + log("Adding default description to QuestionFormat##{qf.id}") + qf.update!(description: "#{qf.title} format") + end + end + end + end + end +end diff --git a/lib/data_cleanup/rules/region/fix_blank_description.rb b/lib/data_cleanup/rules/region/fix_blank_description.rb new file mode 100644 index 0000000..b5389fa --- /dev/null +++ b/lib/data_cleanup/rules/region/fix_blank_description.rb @@ -0,0 +1,19 @@ +module DataCleanup + module Rules + module Region + class FixBlankDescription < Rules::Base + + def description + "Fix blank description on region" + end + + def call + ::Region.where(description: [nil, '']).each do |region| + log("Adding default description to Region##{region.id}") + region.update!(description: "#{region.name} region") + end + end + end + end + end +end diff --git a/lib/data_cleanup/rules/role/fix_blank_plan.rb b/lib/data_cleanup/rules/role/fix_blank_plan.rb new file mode 100644 index 0000000..757b084 --- /dev/null +++ b/lib/data_cleanup/rules/role/fix_blank_plan.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +module DataCleanup + module Rules + # Fix blank plan on Role + module Role + class FixBlankPlan < Rules::Base + + def description + "Fix blank plan on Role" + end + + def call + ids = ::Role.where(plan: nil).ids + log("Destroying Roles without Plan: #{ids}") + ::Role.destroy(ids) + end + end + end + end +end diff --git a/lib/data_cleanup/rules/section/fix_duplicate_number.rb b/lib/data_cleanup/rules/section/fix_duplicate_number.rb new file mode 100644 index 0000000..aa58c85 --- /dev/null +++ b/lib/data_cleanup/rules/section/fix_duplicate_number.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true +module DataCleanup + module Rules + # Fix duplicate number on Section + module Section + class FixDuplicateNumber < Rules::Base + + def description + "Fix duplicate number on Section" + end + + def call + # A set of unique phase_ids that contain Sections with duplicate numbers + phase_ids = ::Section.group(:number, :phase_id) + .count + .select { |k,v| v > 1 } + .collect { |values, count| values.last } + .uniq + + phase_ids.each do |phase_id| + log("Re-setting number order for Sections in Phase##{phase_id}") + phase = ::Phase.find(phase_id) + sections = phase.sections + + # When every section within a Phase is modifiable, or when none is... + if sections.all?(&:modifiable?) || sections.all?(&:template?) + update_homogenous_set(phase) + + # When some of the sections are modifiable then... + else + update_heterogenous_set(phase) + end + end + end + + private + + # Re-sort the Phase's Sections by number, then by the ID + def update_homogenous_set(phase) + ids = phase.sections.order("number ASC, id ASC").ids + ::Section.update_numbers!(*ids, parent: phase) + end + + def update_heterogenous_set(phase) + # Array of duplicate Section numbers within this Phase + numbers = phase.sections + .group(:number) + .count + .select { |number, count| count > 1 } + .collect(&:first) + + # If there are duplicates in the #1 position + if numbers.include?(1) + # There should only be, if any, one prefixed modifiable Section + prefix = phase.sections.modifiable.where(number: 1).limit(1).ids + + # In the off-chance that there is more than one prefix Section, stick them + # after the unmodifiable block + erratic = phase.sections.modifiable.where(number: 1).offset(1).ids + + # Collect the unmodifiable Section ids in the order the should be displayed + unmodifiable = phase.sections.not_modifiable.order('number, id').ids + + # Then any additional Sections that come after the main block... + modifiable = phase.sections.modifiable.order('number, id').ids + + # Create one Array with all of the ids in the correct order. + ids = prefix + erratic + unmodifiable + modifiable + ::Section.update_numbers!(*ids, parent: phase) + + else + # Sort ids based on the number order. If there are duplicates, then take the + # earliest first. Unmodifiable sections from the template should be grouped + # together before the additional, modifiable sections are appended afterwards. + ids = phase.sections.not_modifiable.order("number, id").ids + ids += phase.sections.modifiable.order("number, id").ids + ::Section.update_numbers!(*ids, parent: phase) + end + end + + end + end + end +end diff --git a/lib/data_cleanup/rules/template/fix_blank_locale.rb b/lib/data_cleanup/rules/template/fix_blank_locale.rb new file mode 100644 index 0000000..786a7be --- /dev/null +++ b/lib/data_cleanup/rules/template/fix_blank_locale.rb @@ -0,0 +1,18 @@ +module DataCleanup + module Rules + module Template + class FixBlankLocale < Rules::Base + + def description + "Fix blank locale on template" + end + + def call + ids = ::Template.where(locale: nil).ids + log("Setting locale to #{FastGettext.default_locale} for Templates #{ids}") + ::Template.where(id: ids).update_all(locale: FastGettext.default_locale) + end + end + end + end +end diff --git a/lib/data_cleanup/rules/user_identifier/fix_blank_user.rb b/lib/data_cleanup/rules/user_identifier/fix_blank_user.rb new file mode 100644 index 0000000..f012258 --- /dev/null +++ b/lib/data_cleanup/rules/user_identifier/fix_blank_user.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +module DataCleanup + module Rules + # Fix blank user on UserIdentifier + module UserIdentifier + class FixBlankUser < Rules::Base + + def description + "Fix UserIdentifier records with no User" + end + + def call + ids = ::UserIdentifier.where(user: nil).ids + log("Destroying UserIdentifier where ids: #{ids} (no user)") + ::UserIdentifier.destroy(ids) + end + end + end + end +end diff --git a/lib/generators/data_cleanup_rule/USAGE b/lib/generators/data_cleanup_rule/USAGE new file mode 100644 index 0000000..7264472 --- /dev/null +++ b/lib/generators/data_cleanup_rule/USAGE @@ -0,0 +1,8 @@ +Description: + Add a new rule for cleaning invalid database records + +Example: + rails generate data_cleanup_rule user/fix_missing_emails + + This will create: + lib/tasks/data_cleanup/rules/user/fix_missing_emails.rb diff --git a/lib/generators/data_cleanup_rule/data_cleanup_rule_generator.rb b/lib/generators/data_cleanup_rule/data_cleanup_rule_generator.rb new file mode 100644 index 0000000..6e48031 --- /dev/null +++ b/lib/generators/data_cleanup_rule/data_cleanup_rule_generator.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# Generator class for creating a new Rule to clean DB records. +class DataCleanupRuleGenerator < Rails::Generators::NamedBase + source_root File.expand_path('templates', __dir__) + + # Copy the Rule template and create a new Rule file + def add_rule_file + template "rule.rb.erb", rule_path.to_s + end + + private + + # The name of the model we're fixing (e.g. 'User') + # + # Returns String + def model_name + file_path.split("/").first.classify + end + + # The name of the Rule class we're creating (e.g. 'User::FixBlankEmail') + # + # Returns String + def rule_class_name + file_path.split("/").last.classify + end + + # The file path for the new Rule class + # + # Returns String + def rule_path + Rails.root.join("lib", "data_cleanup", "rules", "#{file_path}.rb") + end + + # A default description to populate the Rule#description method + # + # Returns String + def default_description + format("%s on %s", + rule_name: rule_class_name.underscore + .split("_") + .join(" ") + .capitalize, + model_name: model_name.classify) + end +end diff --git a/lib/generators/data_cleanup_rule/templates/rule.rb.erb b/lib/generators/data_cleanup_rule/templates/rule.rb.erb new file mode 100644 index 0000000..bad37d1 --- /dev/null +++ b/lib/generators/data_cleanup_rule/templates/rule.rb.erb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +module DataCleanup + module Rules + # <%= default_description %> + module <%= model_name %> + class <%= rule_class_name %> < Rules::Base + + def description + "<%= default_description %>" + end + + def call + ::<%= model_name %>.where(nil).update_all({}) + end + end + end + end +end diff --git a/lib/global_helpers.rb b/lib/global_helpers.rb index b630113..8a01937 100644 --- a/lib/global_helpers.rb +++ b/lib/global_helpers.rb @@ -1,14 +1,17 @@ module GlobalHelpers ## - # takes in a string which is meant to be constant, and looks it up in the default - # (en_GB) locale. This should ensure that the back-end remains constant and consistantly called + # Takes in a string which is meant to be constant, and looks it up in the default + # (en_GB) locale. This should ensure that the back-end remains constant and + # consistantly called # - # @param [String] str the string which will be looked up in the localisation - # @return [String] the constant which the string defines + # str - The String which will be looked up in the localisation + # + # Returns String def constant(str) I18n.t("magic_strings.#{str}", locale: I18n.default_locale) end + # overloading the method # came across a wierd issue where the function would refused to be called from # class functions of other classes... but it will work if this is a class function diff --git a/lib/tasks/assets.rake b/lib/tasks/assets.rake new file mode 100644 index 0000000..dd52bb6 --- /dev/null +++ b/lib/tasks/assets.rake @@ -0,0 +1,21 @@ +require 'fileutils' +namespace :assets do + + # Clear out Rails's assets precompile task + Rake::Task["assets:precompile"].clear + + desc "Pre-compile assets for production. Overwrite the Rails assets:precompile" + task :precompile do + FileUtils.cd("lib/assets") do + webpack_options = [] + # Don't watch asset files for further changes + webpack_options << "--no-watch" + # Add the production flag, if env is production + webpack_options << "-p" if ENV["RAILS_ENV"] == "production" + # Ensure all dependencies are installed + system("npm install") + # Run the webpack command via npm + system("npm run bundle -- #{webpack_options.join(" ")}") + end + end +end diff --git a/lib/tasks/auto_annotate_models.rake b/lib/tasks/auto_annotate_models.rake new file mode 100644 index 0000000..77299bd --- /dev/null +++ b/lib/tasks/auto_annotate_models.rake @@ -0,0 +1,54 @@ +# NOTE: only doing this in development as some production environments (Heroku) +# NOTE: are sensitive to local FS writes, and besides -- it's just not proper +# NOTE: to have a dev-mode tool do its thing in production. +if Rails.env.development? + require 'annotate' + task :set_annotation_options do + # You can override any of these by setting an environment variable of the + # same name. + Annotate.set_defaults( + 'routes' => 'false', + 'position_in_routes' => 'before', + 'position_in_class' => 'before', + 'position_in_test' => 'before', + 'position_in_fixture' => 'before', + 'position_in_factory' => 'before', + 'position_in_serializer' => 'before', + 'show_foreign_keys' => 'true', + 'show_complete_foreign_keys' => 'false', + 'show_indexes' => 'true', + 'simple_indexes' => 'false', + 'model_dir' => 'app/models', + 'root_dir' => '', + 'include_version' => 'false', + 'require' => '', + 'exclude_tests' => 'true', + 'exclude_fixtures' => 'true', + 'exclude_factories' => 'false', + 'exclude_serializers' => 'true', + 'exclude_scaffolds' => 'true', + 'exclude_controllers' => 'true', + 'exclude_helpers' => 'true', + 'exclude_sti_subclasses' => 'false', + 'ignore_model_sub_dir' => 'false', + 'ignore_columns' => nil, + 'ignore_routes' => 'true', + 'ignore_unknown_models' => 'false', + 'hide_limit_column_types' => 'integer,boolean', + 'hide_default_column_types' => 'json,jsonb,hstore', + 'skip_on_db_migrate' => 'false', + 'format_bare' => 'true', + 'format_rdoc' => 'false', + 'format_markdown' => 'false', + 'sort' => 'false', + 'force' => 'false', + 'classified_sort' => 'true', + 'trace' => 'false', + 'wrapper_open' => nil, + 'wrapper_close' => nil, + 'with_comment' => true + ) + end + + Annotate.load_tasks +end diff --git a/lib/tasks/data_cleanup.rake b/lib/tasks/data_cleanup.rake new file mode 100644 index 0000000..f84dfc0 --- /dev/null +++ b/lib/tasks/data_cleanup.rake @@ -0,0 +1,50 @@ +require "data_cleanup" + +namespace :data_cleanup do + + desc "Check each record on the DB is valid and report" + task :find_invalid_records => :environment do + DataCleanup.logger.info("\n== Finding invalid records =======================\n") + models.each do |model| + DataCleanup::ModelCheck.new(model).call + end + DataCleanup::Reporting.prepare! + DataCleanup::Reporting.report + end + + desc "Clean invalid records on the database" + task :clean_invalid_records => :environment do + DataCleanup.logger.info("\n== Cleaning invalid records =======================\n") + Dir[rule_paths].each do |rule_path| + load rule_path + klass_name = rule_path.split("rules/").last.gsub(".rb", '').classify + model_name = klass_name.split("::").first + opt, models = ARGV[1].to_s.split("=") + if opt.present? && opt =='INCLUDE' + next unless model_name.in?(models.split(",")) + elsif opt.present? && opt =='EXCLUDE' + next if model_name.in?(models.split(",")) + elsif opt.blank? + # :noop: + else + raise ArgumentError, "Unknown option: #{opt}" + end + rule_class = DataCleanup::Rules.const_get(klass_name) + rule = rule_class.new + puts rule.description + rule.call + end + end + + private + + def rule_paths + @rule_paths ||= Rails.root.join("lib", "data_cleanup", "rules", "*", "*.rb") + end + + def models + Dir[Rails.root.join("app", "models", "*.rb")].map do |model_path| + model_path.split("/").last.gsub(".rb", "").classify.constantize + end.sort_by(&:name) + end +end diff --git a/lib/tasks/doc.rake b/lib/tasks/doc.rake new file mode 100644 index 0000000..96a0c4f --- /dev/null +++ b/lib/tasks/doc.rake @@ -0,0 +1,38 @@ +namespace :tomdoc do + desc "Removes the docs from the ./doc directory" + task :clear do + FileUtils.rm_r(Rails.root.join("doc")) + end + + desc "Builds documentation in the ./doc directory" + task :app do + puts "Please wait..." + options = [] + # Parse documentation as Tomdoc (https://tomdoc.org) + options << "--plugin tomdoc" + # Specify which file to use for the main index (README) + options << "--readme README.md" + # Hides return types specified as 'void'. + options << "--hide-void-return" + # Add a custom title to the HTML docs + options << "--title 'DMP Roadmap'" + # Include protected methods + options << "--protected" + # Include private methods + options << "--private" + # Set methods with no Return value to 'void' + options << "--default-return 'void'" + system("yard doc #{options.join(" ")}") + end + + desc "Builds documentation in the ./doc directory" + task :open do + `open doc/index.html` + end +end + +task tomdoc: ["tomdoc:clear", "tomdoc:app", "tomdoc:open"] do +end + +# Clear Rails' default doc tasks first. +task("doc:app").clear.enhance(["tomdoc:app"]) diff --git a/lib/tasks/factory_bot.rake b/lib/tasks/factory_bot.rake new file mode 100644 index 0000000..d12fab7 --- /dev/null +++ b/lib/tasks/factory_bot.rake @@ -0,0 +1,13 @@ +namespace :factory_bot do + desc "Verify that all FactoryBot factories are valid" + task lint: :environment do + if Rails.env.test? + # DatabaseCleaner.cleaning do + FactoryBot.lint + # end + else + system("bundle exec rake factory_bot:lint RAILS_ENV='test'") + exit $?.exitstatus + end + end +end diff --git a/lib/tasks/translatable.rake b/lib/tasks/translatable.rake index bde060b..8444d40 100644 --- a/lib/tasks/translatable.rake +++ b/lib/tasks/translatable.rake @@ -12,13 +12,13 @@ puts "You must provide a ISO-639 language code, name: `rake translatable:add_language[ja,日本語]`" end end - + desc 'Remove the specified language from the database' task :remove_language_from_db, [:code] => [:environment] do |t, args| if args[:code].present? lang = Language.find_by(abbreviation: args[:code]) default = Language.find_by(default_language: true) || Language.first - + if lang.present? # Set any users/orgs who had the language to the default User.where(language_id: lang.id).update_all(language_id: default.present? ? default.id : nil) @@ -32,20 +32,20 @@ puts "You must provide the ISO-639 language code for the language: e.g. `translatable:remove_language[ja]`" end end - + desc 'Find diffs between main app.pot and specified locale' task :diffs, [:code] => [:environment] do |t, args| if args[:code].present? locale_file = "config/locale/#{args[:code]}/app.po" msgids, orphaned = [], [] - + puts "scanning config/locale/app.pot for msgids ..." File.open('config/locale/app.pot').each do |line| if line.start_with?('msgid ') msgids << line unless msgids.include?(line) end end - + puts "comparing msgids with those in #{locale_file} ..." File.open(locale_file).each do |line| if line.start_with?('msgid ') @@ -56,7 +56,7 @@ end end end - + puts "The following msgids were found in the core app.pot file but NOT in the #{args[:code]} version:" msgids.map{ |id| puts "\n\t#{id}" } puts "---------------------------------------------------------------------" @@ -66,14 +66,17 @@ puts "You must specify a locale code (e.g. en_US or fr)" end end - - desc 'Find all translatable text and update all pot/po files' - task :find, [:code] => [:environment] do |t, args| - app_pot_filename = 'config/locale/app.pot' + + desc 'Find all translatable text and update all pot/po files' + task :find, [:domain] => [:environment] do |t, args| + if args[:domain].blank? + args[:domain] = 'app' # default domain is app + end + pot_filename = "config/locale/#{args[:domain]}.pot" translatables = [] - + puts "Scanning files for translatable text" - files_to_translate.each do |file| + files_to_translate(args[:domain]).each do |file| # Ignore node_modules files unless file.include?('node_modules') puts " scanning #{file}" @@ -81,35 +84,35 @@ end end translatables = translatables.flatten.uniq.sort{ |a,b,| a <=> b } - + unless translatables.empty? - process_po_file(app_pot_filename, translatables) - + process_po_file(pot_filename, translatables) + puts "Searching for localization files" - localization_files.each do |app_po| - process_po_file(app_po, translatables) + localization_files(args[:domain]).each do |domain_po| + process_po_file(domain_po, translatables) end else puts "No translatable text found!" end end - + MSGID = /msgid[\s]+\"(.*)\"/ MSGSTR = /msgstr[\s]+\"(.*)\"/ TRANSLATABLE = /(_\((.|\n)*?[\'\"]\)[\]\)\s\}\,\n\%]+)/ CONTEXTUALIZED_TRANSLATABLE = /(n_\([\'\"](.*?)[\'\"]\,\s*[\'\"](.*?)[\'\"])/ UNESCAPED_QUOTE = /(? #{file_name}.bak" cp(file_name, "#{file_name}.bak") - + puts "Reading #{file_name} ..." file = File.read(file_name) header, hash = po_to_hash(file) - + consolidate_translatables(hash, translatable_text) update_revision_date(header) @@ -118,14 +121,14 @@ file.write "#{update_revision_date(header)}\n#{hash_to_po(hash)}" end end - + # Convert the PO/POT to a hash `hash['organisation'] = { text: 'organization', fuzzy: true, obsolete: false }` # The fuzzy and obsolete flags get updated in `consolidate_translatables` def po_to_hash(file) hash = {} if file.present? # split the file into sections based on the blank line separator - chunks = file.to_s.split(/[\r\n]{2}/) + chunks = file.to_s.split(/[\r\n]{2}/) chunks.each do |chunk| if chunk.match(MSGID) msgid = chunk.match(MSGID).to_s.sub(/^msgid\s\"/, '').sub(/\"$/, '') @@ -141,7 +144,7 @@ # Return the header portion of the original file and the resulting msgid/msgstr hash return chunks[0], hash end - + # Convert the hash to PO/POT format def hash_to_po(hash) lines = "" @@ -158,7 +161,7 @@ end lines end - + # Scan the file contents for translatable text def scan_for_translations(file) # Look for `_('text')` style markup @@ -172,7 +175,7 @@ translatables << parts[1] if parts[1].present? end # Clean up the translatable text entries - translatables.map do |entry| + translatables.map do |entry| entry.sub(/^n?_\([\'\"]/, ''). # remove the gettext markup from front of line sub(/[\'\"]{1}[\)\]\}\,\s\n\%]*$/, ''). # remove the gettext markup from end of line gsub(/[\\]+[\"]/, "\""). # remove double escaped quotes (e.g. \\\") @@ -198,14 +201,19 @@ return hash end - def files_to_translate - Dir.glob("{app,lib,config,locale}/**/*.{rb,erb,md,haml,slim,rhtml}") + # TODO: exclude app/views/branded + def files_to_translate(domain) + if domain == 'app' + Dir.glob("{app,lib,config,locale}/**/*.{rb,erb,md,haml,slim,rhtml}") + else + Dir.glob("{app/views/branded}/**/*.{rb,erb,md,haml,slim,rhtml}") + end end - - def localization_files - Dir.glob("{config/locale}/**/app.po") + + def localization_files(domain) + Dir.glob("{config/locale}/**/#{domain}.po") end - + # Update the PO/POT file's revision data with today's date def update_revision_date(header_text) return header_text.include?('"PO-Revision-Date:') ? header_text.sub(/\"PO\-Revision\-Date\:.*\n/, "\"PO-Revision-Date: #{Time.now.to_s.sub(' -', '-')}\\n\"\n") : header_text diff --git a/lib/tasks/upgrade.rake b/lib/tasks/upgrade.rake index 14ecfbe..9aec1be 100644 --- a/lib/tasks/upgrade.rake +++ b/lib/tasks/upgrade.rake @@ -434,15 +434,15 @@ end end end - + desc "Add the 'other' org if it is not present." task add_other_org: :environment do puts "Checking for existence of an 'Other' org. Unaffiliated users should be affiliated with this org" - + # Get the helpdesk email from the branding YAML branding = YAML.load(File.open('./config/branding.yml')) if branding.present? && branding['defaults'].present? && branding['defaults']['organisation'].present? && branding['defaults']['organisation']['helpdesk_email'].present? - email = branding['defaults']['organisation']['helpdesk_email'] + email = branding['defaults']['organisation']['helpdesk_email'] name = branding['defaults']['organisation']['name'].present? ? "#{branding['defaults']['organisation']['name']} helpdesk" : 'Helpdesk' else email = 'other.organisation@example.org' @@ -464,7 +464,7 @@ is_other: true, }) end - + unaffiliated = User.where(org_id: nil) unless unaffiliated.empty? puts "The following users are not associated with an org. Assigning them to the 'Other' org." diff --git a/spec/factories/annotations.rb b/spec/factories/annotations.rb new file mode 100644 index 0000000..c698c89 --- /dev/null +++ b/spec/factories/annotations.rb @@ -0,0 +1,30 @@ +# == Schema Information +# +# Table name: annotations +# +# id :integer not null, primary key +# text :text +# type :integer default(0), not null +# created_at :datetime +# updated_at :datetime +# org_id :integer +# question_id :integer +# +# Indexes +# +# index_annotations_on_question_id (question_id) +# +# Foreign Keys +# +# fk_rails_... (org_id => orgs.id) +# fk_rails_... (question_id => questions.id) +# + +FactoryBot.define do + factory :annotation do + question + org + text { Faker::Lorem.paragraph } + type { [0,1].sample } + end +end diff --git a/spec/factories/answers.rb b/spec/factories/answers.rb new file mode 100644 index 0000000..b6674f2 --- /dev/null +++ b/spec/factories/answers.rb @@ -0,0 +1,33 @@ +# == Schema Information +# +# Table name: answers +# +# id :integer not null, primary key +# lock_version :integer default(0) +# text :text +# created_at :datetime +# updated_at :datetime +# plan_id :integer +# question_id :integer +# user_id :integer +# +# Indexes +# +# index_answers_on_plan_id (plan_id) +# index_answers_on_question_id (question_id) +# +# Foreign Keys +# +# fk_rails_... (plan_id => plans.id) +# fk_rails_... (question_id => questions.id) +# fk_rails_... (user_id => users.id) +# + +FactoryBot.define do + factory :answer do + text { Faker::Lorem.paragraph } + plan + user + question + end +end diff --git a/spec/factories/exported_plans.rb b/spec/factories/exported_plans.rb new file mode 100644 index 0000000..d1a1c22 --- /dev/null +++ b/spec/factories/exported_plans.rb @@ -0,0 +1,21 @@ +# == Schema Information +# +# Table name: exported_plans +# +# id :integer not null, primary key +# format :string +# created_at :datetime not null +# updated_at :datetime not null +# phase_id :integer +# plan_id :integer +# user_id :integer +# + +FactoryBot.define do + factory :exported_plan do + user + plan + phase_id { create(:phase).id } + format { ExportedPlan::VALID_FORMATS.sample } + end +end diff --git a/spec/factories/guidance_groups.rb b/spec/factories/guidance_groups.rb new file mode 100644 index 0000000..627cff7 --- /dev/null +++ b/spec/factories/guidance_groups.rb @@ -0,0 +1,32 @@ +# == Schema Information +# +# Table name: guidance_groups +# +# id :integer not null, primary key +# name :string +# optional_subset :boolean default(FALSE), not null +# published :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# org_id :integer +# +# Indexes +# +# index_guidance_groups_on_org_id (org_id) +# +# Foreign Keys +# +# fk_rails_... (org_id => orgs.id) +# + +FactoryBot.define do + factory :guidance_group do + name { Faker::Lorem.unique.word } + org + published true + optional_subset false + trait :unpublished do + published false + end + end +end diff --git a/spec/factories/guidances.rb b/spec/factories/guidances.rb new file mode 100644 index 0000000..43a8e68 --- /dev/null +++ b/spec/factories/guidances.rb @@ -0,0 +1,27 @@ +# == Schema Information +# +# Table name: guidances +# +# id :integer not null, primary key +# published :boolean +# text :text +# created_at :datetime not null +# updated_at :datetime not null +# guidance_group_id :integer +# +# Indexes +# +# index_guidances_on_guidance_group_id (guidance_group_id) +# +# Foreign Keys +# +# fk_rails_... (guidance_group_id => guidance_groups.id) +# + +FactoryBot.define do + factory :guidance do + text { Faker::Lorem.sentence } + guidance_group + published false + end +end diff --git a/spec/factories/identifier_schemes.rb b/spec/factories/identifier_schemes.rb new file mode 100644 index 0000000..2f3deb9 --- /dev/null +++ b/spec/factories/identifier_schemes.rb @@ -0,0 +1,23 @@ +# == Schema Information +# +# Table name: identifier_schemes +# +# id :integer not null, primary key +# active :boolean +# description :string +# logo_url :text +# name :string +# user_landing_url :text +# created_at :datetime +# updated_at :datetime +# + +FactoryBot.define do + factory :identifier_scheme do + name { Faker::Company.unique.name[0..29] } + description { Faker::StarWars.quote } + logo_url { Faker::Internet.url } + user_landing_url { Faker::Internet.url } + active true + end +end diff --git a/spec/factories/languages.rb b/spec/factories/languages.rb new file mode 100644 index 0000000..70af9db --- /dev/null +++ b/spec/factories/languages.rb @@ -0,0 +1,26 @@ +# == Schema Information +# +# Table name: languages +# +# id :integer not null, primary key +# abbreviation :string +# default_language :boolean +# description :string +# name :string +# + +FactoryBot.define do + factory :language do + name "English" + description "Test language English" + abbreviation { ("a".."z").to_a.shuffle.take(2).join } + default_language false + trait :with_dialect do + abbreviation { + pre = ("a".."z").to_a.shuffle.take(2).join + suf = ("A".."Z").to_a.shuffle.take(2).join + [pre, suf].join("_") + } + end + end +end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb new file mode 100644 index 0000000..3fcf2b3 --- /dev/null +++ b/spec/factories/notes.rb @@ -0,0 +1,31 @@ +# == Schema Information +# +# Table name: notes +# +# id :integer not null, primary key +# archived :boolean default(FALSE), not null +# archived_by :integer +# text :text +# created_at :datetime +# updated_at :datetime +# answer_id :integer +# user_id :integer +# +# Indexes +# +# index_notes_on_answer_id (answer_id) +# +# Foreign Keys +# +# fk_rails_... (answer_id => answers.id) +# fk_rails_... (user_id => users.id) +# + +FactoryBot.define do + factory :note do + user + text { Faker::Lorem.sentence } + answer + archived false + end +end diff --git a/spec/factories/notifications.rb b/spec/factories/notifications.rb new file mode 100644 index 0000000..fd60bc7 --- /dev/null +++ b/spec/factories/notifications.rb @@ -0,0 +1,34 @@ +# == Schema Information +# +# Table name: notifications +# +# id :integer not null, primary key +# body :text +# dismissable :boolean +# expires_at :date +# level :integer +# notification_type :integer +# starts_at :date +# title :string +# created_at :datetime not null +# updated_at :datetime not null +# + +FactoryBot.define do + factory :notification do + notification_type { :global } + title { Faker::Lorem.sentence } + level { :info } + body { Faker::Lorem.paragraph } + dismissable false + starts_at { Time.current } + expires_at { starts_at + 2.days } + + trait :active do + starts_at { Date.today } + end + trait :dismissable do + dismissable true + end + end +end diff --git a/spec/factories/org_identifiers.rb b/spec/factories/org_identifiers.rb new file mode 100644 index 0000000..dbfafef --- /dev/null +++ b/spec/factories/org_identifiers.rb @@ -0,0 +1,26 @@ +# == Schema Information +# +# Table name: org_identifiers +# +# id :integer not null, primary key +# attrs :string +# identifier :string +# created_at :datetime +# updated_at :datetime +# identifier_scheme_id :integer +# org_id :integer +# +# Foreign Keys +# +# fk_rails_... (identifier_scheme_id => identifier_schemes.id) +# fk_rails_... (org_id => orgs.id) +# + +FactoryBot.define do + factory :org_identifier do + identifier { Faker::Lorem.word } + attrs { Hash.new } + org + identifier_scheme + end +end diff --git a/spec/factories/orgs.rb b/spec/factories/orgs.rb new file mode 100644 index 0000000..3d19e1b --- /dev/null +++ b/spec/factories/orgs.rb @@ -0,0 +1,70 @@ +# == Schema Information +# +# Table name: orgs +# +# id :integer not null, primary key +# abbreviation :string +# contact_email :string +# contact_name :string +# feedback_email_msg :text +# feedback_email_subject :string +# feedback_enabled :boolean default(FALSE) +# is_other :boolean default(FALSE), not null +# links :text +# logo_name :string +# logo_uid :string +# name :string +# org_type :integer default(0), not null +# sort_name :string +# target_url :string +# created_at :datetime not null +# updated_at :datetime not null +# language_id :integer +# region_id :integer +# +# Foreign Keys +# +# fk_rails_... (language_id => languages.id) +# fk_rails_... (region_id => regions.id) +# + +FactoryBot.define do + factory :org do + name { Faker::Company.unique.name } + links { { "org" => [] } } + abbreviation { SecureRandom.hex(4) } + feedback_enabled false + region { Region.first || create(:region) } + language { Language.first || create(:language) } + is_other false + + trait :institution do + institution true + end + trait :funder do + funder true + end + trait :organisation do + organisation true + end + trait :research_institute do + research_institute true + end + trait :project do + project true + end + trait :school do + school true + end + + transient do + templates 0 + end + + after :create do |org, evaluator| + create_list(:template, evaluator.templates, :published, org: org) + end + end +end + + diff --git a/spec/factories/perms.rb b/spec/factories/perms.rb new file mode 100644 index 0000000..8138a87 --- /dev/null +++ b/spec/factories/perms.rb @@ -0,0 +1,55 @@ +# == Schema Information +# +# Table name: perms +# +# id :integer not null, primary key +# name :string +# created_at :datetime not null +# updated_at :datetime not null +# + +FactoryBot.define do + factory :perm do + name { Faker::Company.catch_phrase } + + trait :add_organisations do + name 'add_organisations' + initialize_with { Perm.find_or_create_by(name: name) } + end + + trait :change_org_affiliation do + name 'change_org_affiliation' + initialize_with { Perm.find_or_create_by(name: name) } + end + + trait :grant_permissions do + name 'grant_permissions' + initialize_with { Perm.find_or_create_by(name: name) } + end + + trait :modify_templates do + name 'modify_templates' + initialize_with { Perm.find_or_create_by(name: name) } + end + + trait :modify_guidance do + name 'modify_guidance' + initialize_with { Perm.find_or_create_by(name: name) } + end + + trait :use_api do + name 'use_api' + initialize_with { Perm.find_or_create_by(name: name) } + end + + trait :change_org_details do + name 'change_org_details' + initialize_with { Perm.find_or_create_by(name: name) } + end + + trait :grant_api_to_orgs do + name 'grant_api_to_orgs' + initialize_with { Perm.find_or_create_by(name: name) } + end + end +end diff --git a/spec/factories/phases.rb b/spec/factories/phases.rb new file mode 100644 index 0000000..c51c4f2 --- /dev/null +++ b/spec/factories/phases.rb @@ -0,0 +1,39 @@ +# == Schema Information +# +# Table name: phases +# +# id :integer not null, primary key +# description :text +# modifiable :boolean +# number :integer +# title :string +# created_at :datetime +# updated_at :datetime +# template_id :integer +# +# Indexes +# +# index_phases_on_template_id (template_id) +# +# Foreign Keys +# +# fk_rails_... (template_id => templates.id) +# + +FactoryBot.define do + factory :phase do + title { Faker::Lorem.sentence } + description { Faker::Lorem.paragraph } + sequence(:number) + template + modifiable true + + transient do + sections 0 + end + + after(:create) do |phase, evaluator| + create_list(:section, evaluator.sections, phase: phase) + end + end +end diff --git a/spec/factories/plans.rb b/spec/factories/plans.rb new file mode 100644 index 0000000..c91a105 --- /dev/null +++ b/spec/factories/plans.rb @@ -0,0 +1,79 @@ +# == Schema Information +# +# Table name: plans +# +# id :integer not null, primary key +# complete :boolean default(FALSE) +# data_contact :string +# data_contact_email :string +# data_contact_phone :string +# description :text +# feedback_requested :boolean default(FALSE) +# funder_name :string +# grant_number :string +# identifier :string +# principal_investigator :string +# principal_investigator_email :string +# principal_investigator_identifier :string +# principal_investigator_phone :string +# title :string +# visibility :integer default(3), not null +# created_at :datetime +# updated_at :datetime +# template_id :integer +# +# Indexes +# +# index_plans_on_template_id (template_id) +# +# Foreign Keys +# +# fk_rails_... (template_id => templates.id) +# + +FactoryBot.define do + factory :plan do + title { Faker::Company.bs } + template + grant_number { SecureRandom.rand(1_000) } + identifier { SecureRandom.hex } + description { Faker::Lorem.paragraph } + principal_investigator { Faker::Name.name } + funder_name { Faker::Company.name } + data_contact_email { Faker::Internet.safe_email } + principal_investigator_email { Faker::Internet.safe_email } + feedback_requested false + complete false + transient do + answers 0 + guidance_groups 0 + end + trait :creator do + after(:create) { |obj| obj.roles << create(:role, creator: true) } + end + trait :organisationally_visible do + visibility "organisationally_visible" + end + + trait :publicly_visible do + visibility "publicly_visible" + end + + trait :is_test do + visibility "is_test" + end + + trait :privately_visible do + visibility "privately_visible" + end + + after(:create) do |plan, evaluator| + create_list(:answer, evaluator.answers, plan: plan) + end + + after(:create) do |plan, evaluator| + plan.guidance_groups << create_list(:guidance_group, evaluator.guidance_groups) + end + + end +end diff --git a/spec/factories/prefs.rb b/spec/factories/prefs.rb new file mode 100644 index 0000000..0ed9168 --- /dev/null +++ b/spec/factories/prefs.rb @@ -0,0 +1,14 @@ +# == Schema Information +# +# Table name: prefs +# +# id :integer not null, primary key +# settings :text +# user_id :integer +# + +FactoryBot.define do + factory :pref do + + end +end diff --git a/spec/factories/question_formats.rb b/spec/factories/question_formats.rb new file mode 100644 index 0000000..335fe64 --- /dev/null +++ b/spec/factories/question_formats.rb @@ -0,0 +1,71 @@ +# == Schema Information +# +# Table name: question_formats +# +# id :integer not null, primary key +# description :text +# formattype :integer default(0) +# option_based :boolean default(FALSE) +# title :string +# created_at :datetime not null +# updated_at :datetime not null +# + +FactoryBot.define do + factory :question_format do + title { Faker::Lorem.words(3).join } + description { "http://test.host" } + formattype { QuestionFormat::FORMAT_TYPES.sample } + + # Ensures duplicates aren't created + initialize_with do + QuestionFormat.find_or_create_by(title: title, + formattype: formattype) + end + + trait :textarea do + title "Text area" + formattype "textarea" + end + + trait :textfield do + title "Text field" + formattype "textfield" + end + + trait :radiobuttons do + title "Radio buttons" + formattype "radiobuttons" + option_based true + end + + trait :checkbox do + title "Check box" + formattype "checkbox" + option_based true + end + + trait :dropdown do + title "Drop down" + formattype "dropdown" + option_based true + end + + trait :multiselectbox do + title "Multi select box" + formattype "multiselectbox" + option_based true + end + + trait :date do + title "Date" + formattype "date" + end + + trait :rda_metadata do + title "RDA Metadata" + formattype "rda_metadata" + end + + end +end diff --git a/spec/factories/question_options.rb b/spec/factories/question_options.rb new file mode 100644 index 0000000..ed91536 --- /dev/null +++ b/spec/factories/question_options.rb @@ -0,0 +1,29 @@ +# == Schema Information +# +# Table name: question_options +# +# id :integer not null, primary key +# is_default :boolean +# number :integer +# text :string +# created_at :datetime +# updated_at :datetime +# question_id :integer +# +# Indexes +# +# index_question_options_on_question_id (question_id) +# +# Foreign Keys +# +# fk_rails_... (question_id => questions.id) +# + +FactoryBot.define do + factory :question_option do + question + text { Faker::Lorem.sentence } + sequence(:number) + is_default false + end +end diff --git a/spec/factories/questions.rb b/spec/factories/questions.rb new file mode 100644 index 0000000..6015297 --- /dev/null +++ b/spec/factories/questions.rb @@ -0,0 +1,74 @@ +# == Schema Information +# +# Table name: questions +# +# id :integer not null, primary key +# default_value :text +# modifiable :boolean +# number :integer +# option_comment_display :boolean default(TRUE) +# text :text +# created_at :datetime +# updated_at :datetime +# question_format_id :integer +# section_id :integer +# +# Indexes +# +# index_questions_on_section_id (section_id) +# +# Foreign Keys +# +# fk_rails_... (question_format_id => question_formats.id) +# fk_rails_... (section_id => sections.id) +# + +FactoryBot.define do + factory :question do + section + question_format + text { Faker::Lorem.paragraph } + sequence(:number) + modifiable false + + transient do + options 0 + end + + after(:create) do |question, evaluator| + create_list(:question_option, evaluator.options, question: question) + end + + trait :textarea do + question_format { create(:question_format, :textarea) } + end + + trait :textfield do + question_format { create(:question_format, :textfield) } + end + + trait :radiobuttons do + question_format { create(:question_format, :radiobuttons) } + end + + trait :checkbox do + question_format { create(:question_format, :checkbox) } + end + + trait :dropdown do + question_format { create(:question_format, :dropdown) } + end + + trait :multiselectbox do + question_format { create(:question_format, :multiselectbox) } + end + + trait :date do + question_format { create(:question_format, :date) } + end + + trait :rda_metadata do + question_format { create(:question_format, :rda_metadata) } + end + end +end diff --git a/spec/factories/regions.rb b/spec/factories/regions.rb new file mode 100644 index 0000000..c5d5ddf --- /dev/null +++ b/spec/factories/regions.rb @@ -0,0 +1,18 @@ +# == Schema Information +# +# Table name: regions +# +# id :integer not null, primary key +# abbreviation :string +# description :string +# name :string +# super_region_id :integer +# + +FactoryBot.define do + factory :region do + name { Faker::Address.country } + abbreviation { SecureRandom.hex(2) } + description { Faker::Lorem.sentence } + end +end diff --git a/spec/factories/roles.rb b/spec/factories/roles.rb new file mode 100644 index 0000000..f4422af --- /dev/null +++ b/spec/factories/roles.rb @@ -0,0 +1,52 @@ +# == Schema Information +# +# Table name: roles +# +# id :integer not null, primary key +# access :integer default(0), not null +# active :boolean default(TRUE) +# created_at :datetime +# updated_at :datetime +# plan_id :integer +# user_id :integer +# +# Indexes +# +# index_roles_on_plan_id (plan_id) +# index_roles_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (plan_id => plans.id) +# fk_rails_... (user_id => users.id) +# + +FactoryBot.define do + factory :role do + user + plan + access 0 + active true + trait :active do + active true + end + trait :inactive do + active false + end + trait :creator do + creator true + end + trait :administrator do + administrator true + end + trait :editor do + editor true + end + trait :commenter do + commenter true + end + trait :reviewer do + reviewer true + end + end +end diff --git a/spec/factories/sections.rb b/spec/factories/sections.rb new file mode 100644 index 0000000..b7272ae --- /dev/null +++ b/spec/factories/sections.rb @@ -0,0 +1,31 @@ +# == Schema Information +# +# Table name: sections +# +# id :integer not null, primary key +# description :text +# modifiable :boolean +# number :integer +# title :string +# created_at :datetime +# updated_at :datetime +# phase_id :integer +# +# Indexes +# +# index_sections_on_phase_id (phase_id) +# +# Foreign Keys +# +# fk_rails_... (phase_id => phases.id) +# + +FactoryBot.define do + factory :section do + title { Faker::Lorem.sentence } + description { Faker::Lorem.paragraph } + sequence(:number) + phase + modifiable false + end +end diff --git a/spec/factories/splash_logs.rb b/spec/factories/splash_logs.rb new file mode 100644 index 0000000..816a854 --- /dev/null +++ b/spec/factories/splash_logs.rb @@ -0,0 +1,15 @@ +# == Schema Information +# +# Table name: splash_logs +# +# id :integer not null, primary key +# destination :string +# created_at :datetime not null +# updated_at :datetime not null +# + +FactoryBot.define do + factory :splash_log do + + end +end diff --git a/spec/factories/templates.rb b/spec/factories/templates.rb new file mode 100644 index 0000000..bcd4113 --- /dev/null +++ b/spec/factories/templates.rb @@ -0,0 +1,83 @@ +# == Schema Information +# +# Table name: templates +# +# id :integer not null, primary key +# archived :boolean +# customization_of :integer +# description :text +# is_default :boolean +# links :text +# locale :string +# published :boolean +# title :string +# version :integer +# visibility :integer +# created_at :datetime +# updated_at :datetime +# family_id :integer +# org_id :integer +# +# Indexes +# +# index_templates_on_customization_of_and_version_and_org_id (customization_of,version,org_id) UNIQUE +# index_templates_on_family_id (family_id) +# index_templates_on_family_id_and_version (family_id,version) UNIQUE +# index_templates_on_org_id (org_id) +# template_organisation_dmptemplate_index (org_id,family_id) +# +# Foreign Keys +# +# fk_rails_... (org_id => orgs.id) +# + +FactoryBot.define do + factory :template do + org + title { Faker::Lorem.sentence } + description { Faker::Lorem.paragraph } + locale "en_GB" + is_default false + published false + archived false + sequence(:version) + family_id { rand(10_000) } + + trait :publicly_visible do + after(:create) do |template| + template.update(visibility: Template.visibilities[:publicly_visible]) + end + end + + trait :organisationally_visible do + after(:create) do |template| + template.update(visibility: Template.visibilities[:organisationally_visible]) + end + end + + trait :archived do + archived true + end + + trait :default do + is_default true + end + + trait :published do + published true + end + + trait :unpublished do + published false + end + + transient do + phases 0 + end + + after(:create) do |template, evaluator| + create_list(:phase, evaluator.phases, template: template) + end + + end +end diff --git a/spec/factories/themes.rb b/spec/factories/themes.rb new file mode 100644 index 0000000..bd117b0 --- /dev/null +++ b/spec/factories/themes.rb @@ -0,0 +1,19 @@ +# == Schema Information +# +# Table name: themes +# +# id :integer not null, primary key +# description :text +# locale :string +# title :string +# created_at :datetime not null +# updated_at :datetime not null +# + +FactoryBot.define do + factory :theme do + title { Faker::Lorem.sentence } + description { Faker::Lorem.paragraph } + locale "en_GB" + end +end diff --git a/spec/factories/token_permission_types.rb b/spec/factories/token_permission_types.rb new file mode 100644 index 0000000..4677cdf --- /dev/null +++ b/spec/factories/token_permission_types.rb @@ -0,0 +1,17 @@ +# == Schema Information +# +# Table name: token_permission_types +# +# id :integer not null, primary key +# text_description :text +# token_type :string +# created_at :datetime +# updated_at :datetime +# + +FactoryBot.define do + factory :token_permission_type do + token_type { Faker::Lorem.word } + text_description { Faker::Lorem.sentence } + end +end diff --git a/spec/factories/user_identifiers.rb b/spec/factories/user_identifiers.rb new file mode 100644 index 0000000..d338189 --- /dev/null +++ b/spec/factories/user_identifiers.rb @@ -0,0 +1,28 @@ +# == Schema Information +# +# Table name: user_identifiers +# +# id :integer not null, primary key +# identifier :string +# created_at :datetime +# updated_at :datetime +# identifier_scheme_id :integer +# user_id :integer +# +# Indexes +# +# index_user_identifiers_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (identifier_scheme_id => identifier_schemes.id) +# fk_rails_... (user_id => users.id) +# + +FactoryBot.define do + factory :user_identifier do + identifier { SecureRandom.hex } + user + identifier_scheme + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb new file mode 100644 index 0000000..d6cf3dd --- /dev/null +++ b/spec/factories/users.rb @@ -0,0 +1,58 @@ +# == Schema Information +# +# Table name: users +# +# id :integer not null, primary key +# accept_terms :boolean +# active :boolean default(TRUE) +# api_token :string +# confirmation_sent_at :datetime +# confirmation_token :string +# confirmed_at :datetime +# current_sign_in_at :datetime +# current_sign_in_ip :string +# email :string(80) default(""), not null +# encrypted_password :string default("") +# firstname :string +# invitation_accepted_at :datetime +# invitation_created_at :datetime +# invitation_sent_at :datetime +# invitation_token :string +# invited_by_type :string +# last_sign_in_at :datetime +# last_sign_in_ip :string +# other_organisation :string +# recovery_email :string +# remember_created_at :datetime +# reset_password_sent_at :datetime +# reset_password_token :string +# sign_in_count :integer default(0) +# surname :string +# created_at :datetime not null +# updated_at :datetime not null +# invited_by_id :integer +# language_id :integer +# org_id :integer +# +# Indexes +# +# index_users_on_email (email) UNIQUE +# index_users_on_org_id (org_id) +# +# Foreign Keys +# +# fk_rails_... (language_id => languages.id) +# fk_rails_... (org_id => orgs.id) +# + +FactoryBot.define do + factory :user do + org + language { Language.first || create(:language) } + firstname { Faker::Name.unique.first_name } + surname { Faker::Name.unique.last_name } + email { Faker::Internet.unique.safe_email } + password { "password" } + accept_terms { true } + end +end diff --git a/spec/features/plans_spec.rb b/spec/features/plans_spec.rb new file mode 100644 index 0000000..55b34ed --- /dev/null +++ b/spec/features/plans_spec.rb @@ -0,0 +1,63 @@ +require "rails_helper" + +RSpec.describe "Plans", type: :feature do + + before do + @default_template = create(:template, :default, :published) + @org = create(:org) + @research_org = create(:org, :organisation, :research_institute, + templates: 1) + @funding_org = create(:org, :funder, templates: 1) + @template = create(:template, org: @org) + @user = create(:user, org: @org) + sign_in(@user) + end + + scenario "User creates a new Plan", :js do + # Action + click_link "Create plan" + fill_in :plan_title, with: "My test plan" + fill_in :plan_org_name, with: @research_org.name + + find('#suggestion-2-0').click + fill_in :plan_funder_name, with: @funding_org.name + find('#suggestion-3-0').click + click_button "Create plan" + + # Expectations + expect(@user.plans).to be_one + @plan = Plan.last + expect(current_path).to eql(plan_path(@plan)) + + ## + # User updates plan content... + + # Action + expect(page).to have_css("input[type=text][value='#{@plan.title}']") + + within "#edit_plan_#{@plan.id}" do + fill_in "Grant number", with: "1234" + fill_in "Project abstract", with: "Plan abstract..." + fill_in "ID", with: "ABCDEF" + fill_in "ORCID iD", with: "My ORCID" + fill_in "Phone", with: "07787 000 0000" + click_button "Submit" + end + + # Reload the plan to get the latest from memory + @plan.reload + + expect(current_path).to eql(overview_plan_path(@plan)) + expect(@plan.title).to eql("My test plan") + expect(@plan.funder_name).to eql(@funding_org.name) + expect(@plan.grant_number).to eql("1234") + expect(@plan.description).to eql("Plan abstract...") + expect(@plan.identifier).to eql("ABCDEF") + name = [@user.firstname, @user.surname].join(" ") + expect(@plan.principal_investigator).to eql(name) + expect(@plan.principal_investigator_identifier).to eql("My ORCID") + expect(@plan.principal_investigator_email).to eql(@user.email) + expect(@plan.principal_investigator_phone).to eql("07787 000 0000") + end + +end diff --git a/spec/features/questions/checkbox_questions_spec.rb b/spec/features/questions/checkbox_questions_spec.rb new file mode 100644 index 0000000..f49df90 --- /dev/null +++ b/spec/features/questions/checkbox_questions_spec.rb @@ -0,0 +1,46 @@ +require "rails_helper" + +require "rails_helper" + +RSpec.describe "Questions::Check box questions" do + + before do + @default_template = create(:template, :default, :published) + @phase = create(:phase, template: @default_template) + # Create a couple of Sections + @section = create(:section, phase: @phase) + + @question = create(:question, :checkbox, section: @section, options: 2) + @user = create(:user) + @plan = create(:plan, template: @default_template) + create(:role, :creator, :editor, :commenter, user: @user, plan: @plan) + sign_in(@user) + end + + scenario "User answers a check box question", :js do + # Setup + visit overview_plan_path(@plan) + + # Action + click_link "Write plan" + + # Expectations + expect(current_path).to eql(edit_plan_path(@plan)) + # 4 sections x 3 questions + expect(page).to have_text("0/1 answered") + + # Action + find("#section-panel-1").click + # Fill in the answer form... + within("#answer-form-#{@question.id}") do + check @question.question_options.first.text + click_button "Save" + end + + # Expectations + expect(page).to have_text "Answered just now" + expect(page).to have_text "1/1 answered" + expect(Answer.where(question_id: @question.id)).to be_any + end + +end diff --git a/spec/features/questions/date_questions_spec.rb b/spec/features/questions/date_questions_spec.rb new file mode 100644 index 0000000..f0a7e19 --- /dev/null +++ b/spec/features/questions/date_questions_spec.rb @@ -0,0 +1,46 @@ +require "rails_helper" + +RSpec.describe "Questions::Date questions" do + + before do + @default_template = create(:template, :default, :published) + @phase = create(:phase, template: @default_template) + # Create a couple of Sections + @section = create(:section, phase: @phase) + + @question = create(:question, :date, section: @section) + @user = create(:user) + @plan = create(:plan, template: @default_template) + create(:role, :creator, :editor, :commenter, user: @user, plan: @plan) + sign_in(@user) + end + + scenario "User answers a date question", :js do + skip "Not sure how to test date fields?" + + # Setup + visit overview_plan_path(@plan) + + # Action + click_link "Write plan" + + # Expectations + expect(current_path).to eql(edit_plan_path(@plan)) + # 4 sections x 3 questions + expect(page).to have_text("0/1 answered") + + # Action + find("#section-panel-1").click + # Fill in the answer form... + within("#answer-form-#{@question.id}") do + fill_in :answer_text, with: Date.today.to_s + click_button "Save" + end + + # Expectations + expect(page).to have_text "Answered just now" + expect(page).to have_text "1/1 answered" + expect(Answer.where(question_id: @question.id)).to be_any + end + +end diff --git a/spec/features/questions/dropdown_questions_spec.rb b/spec/features/questions/dropdown_questions_spec.rb new file mode 100644 index 0000000..55316e3 --- /dev/null +++ b/spec/features/questions/dropdown_questions_spec.rb @@ -0,0 +1,46 @@ +require "rails_helper" + +require "rails_helper" + +RSpec.describe "Questions::Dropdown questions" do + + before do + @default_template = create(:template, :default, :published) + @phase = create(:phase, template: @default_template) + # Create a couple of Sections + @section = create(:section, phase: @phase) + + @question = create(:question, :dropdown, section: @section, options: 2) + @user = create(:user) + @plan = create(:plan, template: @default_template) + create(:role, :creator, :editor, :commenter, user: @user, plan: @plan) + sign_in(@user) + end + + scenario "User answers a dropdown select question", :js do + # Setup + visit overview_plan_path(@plan) + + # Action + click_link "Write plan" + + # Expectations + expect(current_path).to eql(edit_plan_path(@plan)) + # 4 sections x 3 questions + expect(page).to have_text("0/1 answered") + + # Action + find("#section-panel-1").click + # Fill in the answer form... + within("#answer-form-#{@question.id}") do + select @question.question_options.first.text + click_button "Save" + end + + # Expectations + expect(page).to have_text "Answered just now" + expect(page).to have_text "1/1 answered" + expect(Answer.where(question_id: @question.id)).to be_any + end + +end diff --git a/spec/features/questions/multiselectbox_questions_spec.rb b/spec/features/questions/multiselectbox_questions_spec.rb new file mode 100644 index 0000000..1b4d602 --- /dev/null +++ b/spec/features/questions/multiselectbox_questions_spec.rb @@ -0,0 +1,45 @@ +require "rails_helper" + +RSpec.describe "Questions::Multi-select questions" do + + before do + @default_template = create(:template, :default, :published) + @phase = create(:phase, template: @default_template) + # Create a couple of Sections + @section = create(:section, phase: @phase) + + @question = create(:question, :multiselectbox, + section: @section, options: 2) + @user = create(:user) + @plan = create(:plan, template: @default_template) + create(:role, :creator, :editor, :commenter, user: @user, plan: @plan) + sign_in(@user) + end + + scenario "User answers a multi select question", :js do + # Setup + visit overview_plan_path(@plan) + + # Action + click_link "Write plan" + + # Expectations + expect(current_path).to eql(edit_plan_path(@plan)) + # 4 sections x 3 questions + expect(page).to have_text("0/1 answered") + + # Action + find("#section-panel-1").click + # Fill in the answer form... + within("#answer-form-#{@question.id}") do + select @question.question_options.first.text + click_button "Save" + end + + # Expectations + expect(page).to have_text "Answered just now" + expect(page).to have_text "1/1 answered" + expect(Answer.where(question_id: @question.id)).to be_any + end + +end diff --git a/spec/features/questions/radiobuttons_questions_spec.rb b/spec/features/questions/radiobuttons_questions_spec.rb new file mode 100644 index 0000000..3af49f4 --- /dev/null +++ b/spec/features/questions/radiobuttons_questions_spec.rb @@ -0,0 +1,44 @@ +require "rails_helper" + +RSpec.describe "Questions::Tadio button questions" do + + before do + @default_template = create(:template, :default, :published) + @phase = create(:phase, template: @default_template) + # Create a couple of Sections + @section = create(:section, phase: @phase) + + @question = create(:question, :radiobuttons, section: @section, options: 2) + @user = create(:user) + @plan = create(:plan, template: @default_template) + create(:role, :creator, :editor, :commenter, user: @user, plan: @plan) + sign_in(@user) + end + + scenario "User answers a radio button question", :js do + # Setup + visit overview_plan_path(@plan) + + # Action + click_link "Write plan" + + # Expectations + expect(current_path).to eql(edit_plan_path(@plan)) + # 4 sections x 3 questions + expect(page).to have_text("0/1 answered") + + # Action + find("#section-panel-1").click + # Fill in the answer form... + within("#answer-form-#{@question.id}") do + choose @question.question_options.first.text + click_button "Save" + end + + # Expectations + expect(page).to have_text "Answered just now" + expect(page).to have_text "1/1 answered" + expect(Answer.where(question_id: @question.id)).to be_any + end + +end diff --git a/spec/features/questions/rda_metadata_questions_spec.rb b/spec/features/questions/rda_metadata_questions_spec.rb new file mode 100644 index 0000000..0906f2a --- /dev/null +++ b/spec/features/questions/rda_metadata_questions_spec.rb @@ -0,0 +1,46 @@ +require "rails_helper" + +RSpec.describe "Questions::RDA Metadata" do + + before do + @default_template = create(:template, :default, :published) + @phase = create(:phase, template: @default_template) + # Create a couple of Sections + @section = create(:section, phase: @phase) + + @question = create(:question, :rda_metadata, section: @section) + @user = create(:user) + @plan = create(:plan, template: @default_template) + create(:role, :creator, :editor, :commenter, user: @user, plan: @plan) + sign_in(@user) + end + + scenario "User answers a RDA Metadata question", :js do + skip "Not sure how to test RDA Metadata fields?" + + # Setup + visit overview_plan_path(@plan) + + # Action + click_link "Write plan" + + # Expectations + expect(current_path).to eql(edit_plan_path(@plan)) + # 4 sections x 3 questions + expect(page).to have_text("0/1 answered") + + # Action + find("#section-panel-1").click + # Fill in the answer form... + within("#answer-form-#{@question.id}") do + fill_in :answer_text, with: Date.today.to_s + click_button "Save" + end + + # Expectations + expect(page).to have_text "Answered just now" + expect(page).to have_text "1/1 answered" + expect(Answer.where(question_id: @question.id)).to be_any + end + +end diff --git a/spec/features/questions/textarea_questions_spec.rb b/spec/features/questions/textarea_questions_spec.rb new file mode 100644 index 0000000..07c5782 --- /dev/null +++ b/spec/features/questions/textarea_questions_spec.rb @@ -0,0 +1,44 @@ +require "rails_helper" + +RSpec.describe "Questions::Text area questions" do + + before do + @default_template = create(:template, :default, :published) + @phase = create(:phase, template: @default_template) + # Create a couple of Sections + @section = create(:section, phase: @phase) + + @question = create(:question, :textarea, section: @section) + @user = create(:user) + @plan = create(:plan, template: @default_template) + create(:role, :creator, :editor, :commenter, user: @user, plan: @plan) + sign_in(@user) + end + + scenario "User answers a Text Area question", :js do + # Setup + visit overview_plan_path(@plan) + + # Action + click_link "Write plan" + + # Expectations + expect(current_path).to eql(edit_plan_path(@plan)) + # 4 sections x 3 questions + expect(page).to have_text("0/1 answered") + + # Action + find("#section-panel-1").click + # Fill in the answer form... + within("#answer-form-#{@question.id}") do + tinymce_fill_in "answer-text-#{@question.id}", "My test answer" + click_button "Save" + end + + # Expectations + expect(page).to have_text "Answered just now" + expect(page).to have_text "1/1 answered" + expect(Answer.where(question_id: @question.id)).to be_any + end + +end diff --git a/spec/features/questions/textfield_questions_spec.rb b/spec/features/questions/textfield_questions_spec.rb new file mode 100644 index 0000000..7c403b2 --- /dev/null +++ b/spec/features/questions/textfield_questions_spec.rb @@ -0,0 +1,44 @@ +require "rails_helper" + +RSpec.describe "Questions::Text Field questions" do + + before do + @default_template = create(:template, :default, :published) + @phase = create(:phase, template: @default_template) + # Create a couple of Sections + @section = create(:section, phase: @phase) + + @question = create(:question, :textfield, section: @section) + @user = create(:user) + @plan = create(:plan, template: @default_template) + create(:role, :creator, :editor, :commenter, user: @user, plan: @plan) + sign_in(@user) + end + + scenario "User answers a Text field question", :js do + # Setup + visit overview_plan_path(@plan) + + # Action + click_link "Write plan" + + # Expectations + expect(current_path).to eql(edit_plan_path(@plan)) + # 4 sections x 3 questions + expect(page).to have_text("0/1 answered") + + # Action + find("#section-panel-1").click + # Fill in the answer form... + within("#answer-form-#{@question.id}") do + fill_in :answer_text, with: "My test answer" + click_button "Save" + end + + # Expectations + expect(page).to have_text "Answered just now" + expect(page).to have_text "1/1 answered" + expect(Answer.where(question_id: @question.id)).to be_any + end + +end diff --git a/spec/features/registrations_spec.rb b/spec/features/registrations_spec.rb new file mode 100644 index 0000000..b6bdb9d --- /dev/null +++ b/spec/features/registrations_spec.rb @@ -0,0 +1,62 @@ +require 'rails_helper' + +RSpec.describe "Registrations", type: :feature do + + let!(:org) { create(:org) } + + let(:user_attributes) { attributes_for(:user) } + + scenario "User creates a new acccount", :js do + user_count = User.count + + # Setup + visit root_path + + # Action + click_link "Create account" + within("#create-account-form") do + fill_in "First Name", with: user_attributes[:firstname] + fill_in "Last Name", with: user_attributes[:surname] + fill_in "Email", with: user_attributes[:email] + fill_in "Organisation", with: org.name + # Click from the dropdown autocomplete + find("#suggestion-1-0").click + fill_in "Password", with: user_attributes[:password] + check "Show password" + check "I accept the terms and conditions" + end + click_button "Create account" + + # Expectations + expect(current_path).to eql(plans_path) + expect(page).to have_text(user_attributes[:firstname]) + expect(page).to have_text(user_attributes[:surname]) + end + + scenario "User attempts to create a new acccount with invalid atts", :js do + user_count = User.count + + # Setup + visit root_path + + # Action + click_link "Create account" + within("#create-account-form") do + fill_in "First Name", with: user_attributes[:firstname] + fill_in "Last Name", with: user_attributes[:surname] + fill_in "Email", with: "invalid-email" + fill_in "Organisation", with: org.name + # Click from the dropdown autocomplete + find("#suggestion-1-0").click + fill_in "Password", with: user_attributes[:password] + check "Show password" + check "I accept the terms and conditions" + end + click_button "Create account" + + # Expectations + expect(current_path).to eql(root_path) + expect(User.count).to be_zero + end + +end \ No newline at end of file diff --git a/spec/features/sessions_spec.rb b/spec/features/sessions_spec.rb new file mode 100644 index 0000000..0bcb45d --- /dev/null +++ b/spec/features/sessions_spec.rb @@ -0,0 +1,39 @@ +require 'rails_helper' + +RSpec.feature "Sessions", type: :feature do + + let(:user) { create(:user) } + + + scenario "User signs in successfully with email and password", :js do + # Setup + visit root_path + + # Action + fill_in :signin_user_email, with: user.email + fill_in :signin_user_password, with: user.password + click_button "Sign in" + + # Expectation + expect(current_path).to eql(plans_path) + expect(page).to have_text(user.firstname) + expect(page).to have_text(user.surname) + end + + scenario "User fails sign in with email and password", :js do + # Setup + visit root_path + + # Action + fill_in :signin_user_email, with: user.email + fill_in :signin_user_password, with: "rong-password" + click_button "Sign in" + + # Expectation + expect(current_path).to eql(root_path) + expect(page).not_to have_text(user.firstname) + expect(page).not_to have_text(user.surname) + expect(page).to have_text("Error") + end + +end diff --git a/spec/features/templates/templates_copying_spec.rb b/spec/features/templates/templates_copying_spec.rb new file mode 100644 index 0000000..4773d9e --- /dev/null +++ b/spec/features/templates/templates_copying_spec.rb @@ -0,0 +1,44 @@ +require 'rails_helper' + +RSpec.feature "Templates::Copying", type: :feature do + + let!(:org) { create(:org, :funder, :organisation) } + + let!(:parent_template) { + create(:template, :published, org: org) + } + + let!(:user) { create(:user, org: org) } + + before do + create_list(:phase, 2, template: parent_template).each do |phase| + create_list(:section, 2, phase: phase).each do |section| + create_list(:question, 2, section: section) + end + end + user.perms << create(:perm, :modify_templates) + user.perms << create(:perm, :add_organisations) + sign_in user + visit org_admin_templates_path + end + + scenario "Admin copies an existing Template", :js do + # Setup + click_link org.name + + # Action + within("#template_#{parent_template.id}") do + click_button "Actions" + click_link "Copy" + end + + # Expectations + expect(Template.count).to eql(2) + new_template = Template.last + expect(new_template.title).to include(parent_template.title) + expect(new_template.phases).to have_exactly(2).items + expect(new_template.sections).to have_exactly(4).items + expect(new_template.questions).to have_exactly(8).items + end + +end diff --git a/spec/features/templates_spec.rb b/spec/features/templates_spec.rb new file mode 100644 index 0000000..baa081b --- /dev/null +++ b/spec/features/templates_spec.rb @@ -0,0 +1,57 @@ +require "rails_helper" + +RSpec.describe "Templates", type: :feature do + + before do + @org = create(:org) + @template = create(:template, org: @org, phases: 2) + @template.phases.each { |phase| create_list(:section, 2, phase: phase) } + @user = create(:user, org: @org) + @user.perms << create(:perm, :modify_templates) + sign_in(@user) + end + + scenario "Org admin edits a template", :js do + # Action + click_link "Admin" + click_link "Templates" + + # Expectations + expect(current_path).to eql(organisational_org_admin_templates_path) + + # Action + click_button "Actions" + click_link "Edit" + + # Expectations + expect(current_path).to eql(edit_org_admin_template_path(@template)) + + # Action + within '#phase_1' do + click_link "Edit phase" + end + + # Expectations + path = edit_org_admin_template_phase_path(@template, @template.phases.first) + expect(current_path).to eql(path) + + # Action + # Open the panel for a new Section + find("a[href='#new_section']").click + + within "#collapseSectionNew" do + fill_in :new_section_section_title, with: "My new section" + tinymce_fill_in :new_section_section_description, + "This is the description of my new section" + click_button "Save" + end + + # Expectations + last_section = Section.last + expect(@template.sections.count).to eql(5) + expect(last_section.title).to eql("My new section") + expect(last_section.description).to match("This is the description of my new section") + expect(last_section.description).to match("

") + end + +end diff --git a/spec/models/annotation_spec.rb b/spec/models/annotation_spec.rb new file mode 100644 index 0000000..08d2671 --- /dev/null +++ b/spec/models/annotation_spec.rb @@ -0,0 +1,75 @@ +require 'rails_helper' + +RSpec.describe Annotation, type: :model do + + context "validations" do + + subject { build(:annotation) } + + it { is_expected.to validate_presence_of(:text) } + + it { is_expected.to validate_presence_of(:org) } + + it { is_expected.to validate_presence_of(:question) } + + it { is_expected.to validate_presence_of(:type) } + + end + + describe "#to_s" do + + let!(:annotation) { build(:annotation) } + + subject { annotation.to_s } + + it { is_expected.to eql(annotation.text) } + + end + + describe "#deep_copy" do + + context "when question_id option is nil" do + + before do + @annotation = create(:annotation) + @new_annotation = @annotation.deep_copy + end + + it "creates a different record" do + expect(@new_annotation).not_to eql(@annotation) + end + + it "copies the text attribute" do + expect(@new_annotation.text).to eql(@annotation.text) + end + + it "copies the type attribute" do + expect(@new_annotation.type).to eql(@annotation.type) + end + + it "copies the org_id attribute" do + expect(@new_annotation.org_id).to eql(@annotation.org_id) + end + + it "sets question_id to nil" do + expect(@new_annotation.question_id).to be_nil + end + + end + + context "when question_id option is set" do + + before do + @annotation = create(:annotation) + @new_annotation = @annotation.deep_copy(question_id: 1) + end + + it "sets question_id to nil" do + expect(@new_annotation.question_id).to eql(1) + end + + end + + end + +end diff --git a/spec/models/answer_spec.rb b/spec/models/answer_spec.rb new file mode 100644 index 0000000..67ff202 --- /dev/null +++ b/spec/models/answer_spec.rb @@ -0,0 +1,311 @@ +require 'rails_helper' + +RSpec.describe Answer, type: :model do + + context "validations" do + subject { build(:answer) } + + it { is_expected.to validate_presence_of(:plan) } + + it { is_expected.to validate_presence_of(:user) } + + it { is_expected.to validate_presence_of(:question) } + + it { is_expected.to validate_uniqueness_of(:question) + .scoped_to(:plan_id) + .with_message("must be unique") } + end + + describe ".deep_copy" do + + let!(:answer) { build(:answer) } + + subject { Answer.deep_copy(answer) } + + it "creates a new record" do + expect(subject).not_to eql(answer) + end + + it "copies the lock_version attribute" do + expect(subject.lock_version).to eql(answer.lock_version) + end + + it "copies the text attribute" do + expect(subject.text).to eql(answer.text) + end + + it "copies the plan_id attribute" do + expect(subject.plan_id).to eql(answer.plan_id) + end + + it "copies the question_id attribute" do + expect(subject.question_id).to eql(answer.question_id) + end + + it "copies the user_id attribute" do + expect(subject.user_id).to eql(answer.user_id) + end + end + + describe "#has_question_option" do + + let!(:answer) { create(:answer) } + + let!(:question_option) { create(:question_option) } + + subject { answer.has_question_option(question_option.id) } + + context "when answer has QuestionOption" do + + before do + answer.question_options << question_option + end + + it { is_expected.to eql(true) } + + end + + context "when answer doesn't have QuestionOption" do + + it { is_expected.to eql(false) } + + end + + end + + describe "#is_valid?" do + + let!(:answer) { create(:answer) } + + subject { answer.is_valid? } + + context "question present, question format is option and options empty" do + + before do + answer.question.update(question_format: + create(:question_format, option_based: true)) + end + + it { is_expected.to eql(false) } + + end + + context "question present, question format is option and options present" do + + before do + answer.question.update(question_format: + create(:question_format, option_based: true)) + + answer.question_options << create_list(:question_option, 2) + end + + it { is_expected.to eql(true) } + + end + + context "question present, question format not option and text empty" do + + before do + answer.question.update(question_format: + create(:question_format, option_based: false)) + + answer.text = "" + end + + it { is_expected.to eql(false) } + + end + + context "question present, question format not option and text present" do + + before do + answer.question.update(question_format: + create(:question_format, option_based: false)) + + answer.text = "This is an answer" + end + + it { is_expected.to eql(true) } + + end + + context "question absent" do + + before do + answer.update(question: nil) + end + + it { is_expected.to eql(false) } + + end + + end + + describe "#non_archived_notes" do + + before do + @answer = create(:answer) + @notes = create_list(:note, 3, answer: @answer, archived: false) + @archived_notes = create_list(:note, 3, answer: @answer, archived: true) + @other_notes = create_list(:note, 3) + end + + subject { @answer.non_archived_notes } + + it "includes the non-archived notes" do + @notes.each do |note| + expect(subject).to include(note) + end + end + + it "excludes the archived notes" do + @archived_notes.each do |note| + expect(subject).not_to include(note) + end + end + + it "excludes notes belonging to other Answers" do + @other_notes.each do |note| + expect(subject).not_to include(note) + end + end + + end + + describe "#is_blank?" do + + context "when text is nil" do + + let!(:answer) { build(:answer, text: nil) } + + subject { answer } + + it { is_expected.to be_is_blank } + + end + + context "when text is ''" do + + let!(:answer) { build(:answer, text: "") } + + subject { answer } + + it { is_expected.to be_is_blank } + + end + + context "when text is plain text" do + + let!(:answer) { build(:answer, text: "Foo bar") } + + subject { answer } + + it { is_expected.not_to be_is_blank } + + end + + context "when text is html text" do + + let!(:answer) { build(:answer, text: "

Foo bar

") } + + subject { answer } + + it { is_expected.not_to be_is_blank } + + end + + end + + describe "#answer_hash" do + + let!(:answer) { build(:answer) } + + let(:default_json) { {'standards' => {}, 'text' => ''} } + + subject { answer.answer_hash } + + context "when text is nil" do + + before do + answer.text = nil + end + + it { is_expected.to eql(default_json) } + + end + + context "when text is blank" do + + before do + answer.text = '' + end + + it { is_expected.to eql(default_json) } + + end + + context "when text is valid JSON" do + + before do + answer.text = { name: "foo", bar: "baz" }.to_json + end + + it { is_expected.to eql({ "name" => "foo", "bar" => "baz" }) } + + end + + context "when text is HTML" do + + before do + answer.text = '

foo bar

' + end + + it { is_expected.to eql(default_json) } + + end + + end + + describe "#update_answer_hash" do + + let!(:answer) { build(:answer) } + + subject { answer.answer_hash } + + context "when standards parameter is present" do + + before do + answer.update_answer_hash({foo: 'bar'}) + end + + it { is_expected.to eql({"standards" => {"foo" => "bar"}, "text" => ""}) } + + end + + context "when both params are absent" do + + before do + answer.update_answer_hash() + end + + it { is_expected.to eql({"standards" => {}, "text" => ""}) } + + end + + context "when both params are present" do + + before do + answer.update_answer_hash({foo: 'bar'}, "baz") + end + + it { is_expected.to eql({ + "standards" => {"foo" => "bar"}, + "text" => "baz" + }) + } + + end + + end + +end diff --git a/spec/models/guidance_group_spec.rb b/spec/models/guidance_group_spec.rb new file mode 100644 index 0000000..f57957b --- /dev/null +++ b/spec/models/guidance_group_spec.rb @@ -0,0 +1,215 @@ +require 'rails_helper' + +RSpec.describe GuidanceGroup, type: :model do + + context "validations" do + + it { is_expected.to validate_presence_of(:name) } + + it { is_expected.to validate_presence_of(:org) } + + it { is_expected.to allow_value(true).for(:optional_subset) } + + it { is_expected.to allow_value(true).for(:published) } + + it { is_expected.to allow_value(false).for(:optional_subset) } + + it { is_expected.to allow_value(false).for(:published) } + + end + + context "associations" do + + it { is_expected.to belong_to :org } + + it { is_expected.to have_many :guidances } + + end + + describe ".can_view?" do + + let!(:user) { create(:user) } + + let!(:guidance_group) { create(:guidance_group) } + + subject { GuidanceGroup.can_view?(user, guidance_group) } + + + context "when owned by an Org which the user is a member" do + + let!(:guidance_group) { create(:guidance_group, org: user.org) } + + it { is_expected.to eql(true) } + + end + + context "when owned by a curation center" do + + let!(:org) do + create(:org, + abbreviation: Rails.configuration + .branding.dig(:organisation, :abbreviation)) + end + + let!(:guidance_group) { create(:guidance_group, org: org) } + + it { is_expected.to eql(true) } + + end + + context "when owned by a institution org" do + + let!(:guidance_group) do + create(:guidance_group, org: create(:org, :institution)) + end + + it { is_expected.to eql(false) } + + end + + context "when owned by a funder org" do + + let!(:guidance_group) do + create(:guidance_group, org: create(:org, :funder)) + end + + it { is_expected.to eql(true) } + + end + + context "when owned by a organisation org" do + + let!(:guidance_group) do + create(:guidance_group, org: create(:org, :organisation)) + end + + it { is_expected.to eql(false) } + + end + + context "when owned by a research_institute org" do + + let!(:guidance_group) do + create(:guidance_group, org: create(:org, :research_institute)) + end + + it { is_expected.to eql(false) } + + end + + context "when owned by a project org" do + + let!(:guidance_group) do + create(:guidance_group, org: create(:org, :project)) + end + + it { is_expected.to eql(false) } + + end + + context "when owned by a school org" do + + let!(:guidance_group) do + create(:guidance_group, org: create(:org, :school)) + end + + it { is_expected.to eql(false) } + + end + end + + describe ".all_viewable" do + + let!(:user) { create(:user) } + + subject { GuidanceGroup.all_viewable(user) } + + context "when is owned by managing curation center" do + + let!(:org) do + create(:org, + abbreviation: Rails.configuration + .branding.dig(:organisation, :abbreviation)) + end + + let!(:guidance_group) { create(:guidance_group, org: org) } + + it "includes guidance group" do + expect(subject).to include(guidance_group) + end + + end + + context "when is owned by institution Org" do + + let!(:org) { create(:org, :institution) } + + let!(:guidance_group) { create(:guidance_group, org: org) } + + it "excludes guidance group" do + expect(subject).not_to include(guidance_group) + end + + end + + context "when is owned by funder Org" do + + let!(:org) { create(:org, :funder) } + + let!(:guidance_group) { create(:guidance_group, org: org) } + + it "includes guidance group" do + expect(subject).to include(guidance_group) + end + + end + + context "when is owned by organisation Org" do + + let!(:org) { create(:org, :organisation) } + + let!(:guidance_group) { create(:guidance_group, org: org) } + + it "excludes guidance group" do + expect(subject).not_to include(guidance_group) + end + + end + + context "when is owned by research_institute Org" do + + let!(:org) { create(:org, :research_institute) } + + let!(:guidance_group) { create(:guidance_group, org: org) } + + it "excludes guidance group" do + expect(subject).not_to include(guidance_group) + end + + end + + context "when is owned by project Org" do + + let!(:org) { create(:org, :project) } + + let!(:guidance_group) { create(:guidance_group, org: org) } + + it "excludes guidance group" do + expect(subject).not_to include(guidance_group) + end + + end + + context "when is owned by school Org" do + + let!(:org) { create(:org, :school) } + + let!(:guidance_group) { create(:guidance_group, org: org) } + + it "excludes guidance group" do + expect(subject).not_to include(guidance_group) + end + + end + end +end diff --git a/spec/models/guidance_spec.rb b/spec/models/guidance_spec.rb new file mode 100644 index 0000000..5ed6c60 --- /dev/null +++ b/spec/models/guidance_spec.rb @@ -0,0 +1,310 @@ +require 'rails_helper' + +RSpec.describe Guidance, type: :model do + + context "validations" do + + it { is_expected.to validate_presence_of(:text) } + + it { is_expected.to validate_presence_of(:guidance_group) } + + it { is_expected.to allow_value(true).for(:published) } + + it { is_expected.to allow_value(false).for(:published) } + + end + + context "associations" do + + it { is_expected.to belong_to :guidance_group } + + it do + is_expected.to have_and_belong_to_many(:themes) + .join_table("themes_in_guidance") + end + end + + + describe ".can_view?" do + + let!(:user) { create(:user) } + + subject { Guidance.can_view?(user, @guidance.id) } + + context "when guidance_id is invalid" do + + before do + @guidance = Guidance.new(guidance_group: create(:guidance_group)) + end + + it { is_expected.to eql(false) } + + end + + context "when guidance's group is nil" do + + before do + @guidance = Guidance.new + end + + it { is_expected.to eql(false) } + + end + + context "when owned by a curation center" do + + before do + @org = create(:org, + abbreviation: Rails.configuration + .branding.dig(:organisation, :abbreviation)) + @guidance_group = create(:guidance_group, org: @org) + @guidance = create(:guidance, guidance_group: @guidance_group) + end + + it { is_expected.to eql(true) } + + end + + context "when owned by a institution org" do + + before do + @org = create(:org, :institution) + @guidance_group = create(:guidance_group, org: @org) + @guidance = create(:guidance, guidance_group: @guidance_group) + end + + it { is_expected.to eql(false) } + + end + + context "when owned by a funder org" do + + before do + @org = create(:org, :funder) + @guidance_group = create(:guidance_group, org: @org) + @guidance = create(:guidance, guidance_group: @guidance_group) + end + + it { is_expected.to eql(true) } + + end + + context "when owned by a organisation org" do + + before do + @org = create(:org, :organisation) + @guidance_group = create(:guidance_group, org: @org) + @guidance = create(:guidance, guidance_group: @guidance_group) + end + + it { is_expected.to eql(false) } + + end + + context "when owned by a research_institute org" do + + before do + @org = create(:org, :research_institute) + @guidance_group = create(:guidance_group, org: @org) + @guidance = create(:guidance, guidance_group: @guidance_group) + end + + it { is_expected.to eql(false) } + + end + + context "when owned by a project org" do + + before do + @org = create(:org, :project) + @guidance_group = create(:guidance_group, org: @org) + @guidance = create(:guidance, guidance_group: @guidance_group) + end + + it { is_expected.to eql(false) } + + end + + context "when owned by a school org" do + + before do + @org = create(:org, :school) + @guidance_group = create(:guidance_group, org: @org) + @guidance = create(:guidance, guidance_group: @guidance_group) + end + + it { is_expected.to eql(false) } + + end + + context "when owned by an Org which the user is a member" do + + before do + @org = user.org + @guidance_group = create(:guidance_group, org: @org) + @guidance = create(:guidance, guidance_group: @guidance_group) + end + + it { is_expected.to eql(true) } + + end + + end + + describe ".all_viewable" do + + let!(:user) { create(:user) } + + subject { Guidance.all_viewable(user) } + + context "when is owned by managing curation center" do + + before do + @org = create(:org, + abbreviation: Rails.configuration + .branding.dig(:organisation, :abbreviation)) + @guidance_group = create(:guidance_group, org: @org) + @guidance = create(:guidance, guidance_group: @guidance_group) + end + + it "includes guidance" do + expect(subject).to include(@guidance) + end + + end + + context "when is owned by institution Org" do + + before do + @org = create(:org, :institution) + @guidance_group = create(:guidance_group, org: @org) + @guidance = create(:guidance, guidance_group: @guidance_group) + end + + it "excludes guidance" do + expect(subject).not_to include(@guidance) + end + + end + + context "when is owned by funder Org" do + + before do + @org = create(:org, :funder) + @guidance_group = create(:guidance_group, org: @org) + @guidance = create(:guidance, guidance_group: @guidance_group) + end + + it "includes guidance" do + expect(subject).to include(@guidance) + end + + end + + context "when is owned by organisation Org" do + + before do + @org = create(:org, :organisation) + @guidance_group = create(:guidance_group, org: @org) + @guidance = create(:guidance, guidance_group: @guidance_group) + end + + it "excludes guidance" do + expect(subject).not_to include(@guidance) + end + + end + + context "when is owned by research_institute Org" do + + before do + @org = create(:org, :research_institute) + @guidance_group = create(:guidance_group, org: @org) + @guidance = create(:guidance, guidance_group: @guidance_group) + end + + it "excludes guidance" do + expect(subject).not_to include(@guidance) + end + + end + + context "when is owned by project Org" do + + before do + @org = create(:org, :project) + @guidance_group = create(:guidance_group, org: @org) + @guidance = create(:guidance, guidance_group: @guidance_group) + end + + it "excludes guidance" do + expect(subject).not_to include(@guidance) + end + + end + + context "when is owned by school Org" do + + before do + @org = create(:org, :school) + @guidance_group = create(:guidance_group, org: @org) + @guidance = create(:guidance, guidance_group: @guidance_group) + end + + it "excludes guidance" do + expect(subject).not_to include(@guidance) + end + + end + + context "when is owned by User's Org'" do + + before do + @org = user.org + @guidance_group = create(:guidance_group, org: @org) + @guidance = create(:guidance, guidance_group: @guidance_group) + end + + it "includes guidance" do + expect(subject).to include(@guidance) + end + + end + end + + describe "#in_group_belonging_to?" do + + let!(:org) { create(:org) } + + subject { guidance.in_group_belonging_to?(org.id) } + + context "when guidance_group is nil" do + + let!(:guidance) { Guidance.new } + + it { is_expected.to eql(false) } + + end + + context "when guidance group belongs to given Org" do + + let!(:guidance_group) { create(:guidance_group, org: org) } + + let!(:guidance) { create(:guidance, guidance_group: guidance_group) } + + it { is_expected.to eql(true) } + + end + + context "when guidance group doesn't belong to given Org" do + + let!(:guidance_group) { create(:guidance_group) } + + let!(:guidance) { create(:guidance, guidance_group: guidance_group) } + + it { is_expected.to eql(false) } + + end + end + +end diff --git a/spec/models/identifier_scheme_spec.rb b/spec/models/identifier_scheme_spec.rb new file mode 100644 index 0000000..27d0262 --- /dev/null +++ b/spec/models/identifier_scheme_spec.rb @@ -0,0 +1,27 @@ +require 'rails_helper' + +RSpec.describe IdentifierScheme, type: :model do + + context "validations" do + + it { is_expected.to validate_presence_of(:name) } + + it { is_expected.to validate_length_of(:name).is_at_most(30) } + + it { is_expected.to allow_value(true).for(:name) } + + it { is_expected.to allow_value(false).for(:name) } + + it { is_expected.to_not allow_value(nil).for(:name) } + + end + + context "associations" do + + it { is_expected.to have_many :user_identifiers } + + it { is_expected.to have_many(:users).through(:user_identifiers) } + + end + +end diff --git a/spec/models/language_spec.rb b/spec/models/language_spec.rb new file mode 100644 index 0000000..49da5ce --- /dev/null +++ b/spec/models/language_spec.rb @@ -0,0 +1,102 @@ +require 'rails_helper' + +RSpec.describe Language, type: :model do + + context "validations" do + + subject { build(:language) } + + it { is_expected.to validate_presence_of(:name) } + + it { is_expected.to validate_length_of(:name).is_at_most(20) } + + it { is_expected.to validate_presence_of(:abbreviation) } + + it { is_expected.to validate_uniqueness_of(:abbreviation) + .with_message("must be unique") } + + it { is_expected.to allow_values('en', 'en_GB').for(:abbreviation) } + + it { is_expected.not_to allow_value('NOOP', 'en_', 'EN') + .for(:abbreviation) } + + it { is_expected.to validate_length_of(:abbreviation).is_at_most(5) } + + end + + context "associations" do + + it { is_expected.to have_many :users } + + it { is_expected.to have_many :orgs } + + end + + describe ".sorted_by_abbreviation" do + + before do + create(:language, abbreviation: "aa") + create(:language, abbreviation: "ab") + create(:language, abbreviation: "ac") + end + + it "sorts Languages by abbreviation in alphabetical order" do + l1 = Language.find_by(abbreviation: "aa") + expect(Language.sorted_by_abbreviation.first).to eql(l1) + + l2 = Language.find_by(abbreviation: "ab") + expect(Language.sorted_by_abbreviation.second).to eql(l2) + + l3 = Language.find_by(abbreviation: "ac") + expect(Language.sorted_by_abbreviation.third).to eql(l3) + end + + end + + describe ".default" do + + subject { Language.default } + + context "when langauge is default_language" do + + let!(:language) { create(:language, default_language: true) } + + it { is_expected.to eql(language) } + + end + + context "when language is not default_language" do + + let!(:language) { create(:language, default_language: false) } + + it { is_expected.not_to eql(language) } + + end + + end + + describe ".id_for" do + + subject { Language.id_for("fu") } + + context "when abbreviation is valid" do + + let!(:language) { create(:language, abbreviation: "fu") } + + it "returns the id for language with that abbreviation" do + expect(subject).to eql(language.id) + end + + end + + context "when abbreviation is invalid" do + + it "returns empty array" do + expect(subject).to be_empty + end + + end + + end + +end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb new file mode 100644 index 0000000..791c990 --- /dev/null +++ b/spec/models/note_spec.rb @@ -0,0 +1,27 @@ +require 'rails_helper' + +RSpec.describe Note, type: :model do + + context "validations" do + + it { is_expected.to validate_presence_of(:text) } + + it { is_expected.to validate_presence_of(:answer) } + + it { is_expected.to validate_presence_of(:user) } + + it { is_expected.to allow_values(true, false).for(:archived) } + + it { is_expected.not_to allow_value(nil).for(:archived) } + + end + + context "associations" do + + it { is_expected.to belong_to :answer } + + it { is_expected.to belong_to :user } + + end + +end diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb new file mode 100644 index 0000000..99fc176 --- /dev/null +++ b/spec/models/notification_spec.rb @@ -0,0 +1,196 @@ +require 'rails_helper' + +RSpec.describe Notification, type: :model do + + context "validations" do + + it { is_expected.to validate_presence_of(:notification_type) } + + it { is_expected.to validate_presence_of(:title) } + + it { is_expected.to validate_presence_of(:level) } + + it { is_expected.to validate_presence_of(:body) } + + it { is_expected.to allow_values(true, false).for(:dismissable) } + + it { is_expected.not_to allow_value(nil).for(:dismissable) } + + it { is_expected.to validate_presence_of(:starts_at) } + + it { is_expected.to validate_presence_of(:expires_at) } + + it { is_expected.to allow_value(Date.today).for(:starts_at) } + + it { is_expected.not_to allow_value(1.day.ago).for(:starts_at) } + + it { is_expected.to allow_value(2.days.from_now).for(:expires_at) } + + it { is_expected.not_to allow_value(Date.today).for(:expires_at) } + + end + + describe ".active" do + + subject { Notification.active } + + context "when now is before starts_at" do + + let!(:notification) { create(:notification, starts_at: 1.week.from_now) } + + it { is_expected.not_to include(notification) } + + end + + context "when now lies between starts_at and expires_at" do + + let!(:notification) do + record = build(:notification, starts_at: 1.day.ago, + expires_at: 1.day.from_now) + record.save(validate: false) + record + end + + it { is_expected.to include(notification) } + + end + + context "when now is after expires_at" do + + let!(:notification) do + create(:notification, starts_at: 1.week.from_now) + end + + it { is_expected.not_to include(notification) } + + end + end + + describe ".active_per_user" do + + context "when User is present and Notification is general" do + + let!(:notification) { create(:notification, :active) } + + let!(:user) { create(:user) } + + subject { Notification.active_per_user(user) } + + it { is_expected.to include(notification) } + + end + + context "when User is present and Notification belongs to User" do + + let!(:user) { create(:user) } + + let!(:notification) { create(:notification, :active) } + + before do + notification.users << user + end + + subject { Notification.active_per_user(user) } + + it { is_expected.not_to include(notification) } + + end + + context "when User is nil and Notification is dismissable" do + + let!(:user) { nil } + + let!(:notification) { create(:notification, :active, :dismissable) } + + subject { Notification.active_per_user(user) } + + it { is_expected.not_to include(notification) } + + end + + context "when User is nil and Notification is not dismissable" do + + let!(:user) { nil } + + let!(:notification) { create(:notification, :active) } + + subject { Notification.active_per_user(user) } + + it { is_expected.to include(notification) } + + end + end + + describe "#acknowledged?" do + + context "when dismissable, user present, and already acknowledged" do + + let!(:notification) { create(:notification, :dismissable) } + + let!(:user) { create(:user) } + + subject { notification.acknowledged?(user) } + + before do + notification.users << user + end + + it { is_expected.to eql(true) } + + end + + context "when not dismissable, user present, and already acknowledged" do + + let!(:notification) { create(:notification) } + + let!(:user) { create(:user) } + + subject { notification.acknowledged?(user) } + + before do + notification.users << user + end + + it { is_expected.to eql(false) } + + end + + context "when dismissable, user absent" do + + let!(:notification) { create(:notification, :dismissable) } + + let!(:user) { nil } + + subject { notification.acknowledged?(user) } + + it { is_expected.to eql(false) } + + end + + context "when dismissable, user absent, and not already acknowledged" do + + let!(:notification) { create(:notification, :dismissable) } + + let!(:user) { nil } + + subject { notification.acknowledged?(user) } + + it { is_expected.to eql(false) } + + end + + context "when not dismissable, user absent, and not already acknowledged" do + + let!(:notification) { create(:notification) } + + let!(:user) { nil } + + subject { notification.acknowledged?(user) } + + it { is_expected.to eql(false) } + + end + + end + +end diff --git a/spec/models/org_identifier_spec.rb b/spec/models/org_identifier_spec.rb new file mode 100644 index 0000000..ede69b1 --- /dev/null +++ b/spec/models/org_identifier_spec.rb @@ -0,0 +1,68 @@ +require 'rails_helper' + +RSpec.describe OrgIdentifier, type: :model do + + context "validations" do + + it do + # https://github.com/thoughtbot/shoulda-matchers/issues/682 + subject.identifier_scheme = create(:identifier_scheme) + is_expected.to validate_uniqueness_of(:identifier_scheme_id) + .scoped_to(:org_id) + .with_message("must be unique") + end + + it { is_expected.to validate_presence_of(:identifier) } + + it { is_expected.to validate_presence_of(:org) } + + it { is_expected.to validate_presence_of(:identifier_scheme) } + + end + + context "associations" do + + it { is_expected.to belong_to(:org) } + + it { is_expected.to belong_to(:identifier_scheme) } + + end + + describe "#attrs=" do + + context "when hash is a Hash" do + + let!(:org_identifier) { create(:org_identifier) } + + it "sets attrs to a String of JSON" do + org_identifier.attrs = { foo: "bar" } + expect(org_identifier.attrs).to eql({"foo" => "bar"}.to_json) + end + + end + + context "when hash is nil" do + + let!(:org_identifier) { create(:org_identifier) } + + it "sets attrs to empty JSON object" do + org_identifier.attrs = nil + expect(org_identifier.attrs).to eql({}.to_json) + end + + end + + context "when hash is a String" do + + let!(:org_identifier) { create(:org_identifier) } + + it "sets attrs to empty JSON object" do + org_identifier.attrs = '' + expect(org_identifier.attrs).to eql({}.to_json) + end + + end + + end + +end diff --git a/spec/models/org_spec.rb b/spec/models/org_spec.rb new file mode 100644 index 0000000..110c301 --- /dev/null +++ b/spec/models/org_spec.rb @@ -0,0 +1,390 @@ +require 'rails_helper' + +RSpec.describe Org, type: :model do + + context "validations" do + + it { is_expected.to validate_presence_of(:name) } + + it { + subject.name = "DMP Company" + is_expected.to validate_uniqueness_of(:name) + .with_message("must be unique") + } + + it { is_expected.to validate_presence_of(:abbreviation) } + + it { is_expected.to allow_values(true, false).for(:is_other) } + + it { is_expected.not_to allow_value(nil).for(:is_other) } + + it { is_expected.to validate_presence_of(:language) } + + it "validates presence of contact_email if feedback_enabled" do + subject.feedback_enabled = true + is_expected.to validate_presence_of(:contact_email) + end + + it "doesn't validate presence of contact_email if feedback_enabled nil" do + subject.feedback_enabled = false + is_expected.not_to validate_presence_of(:contact_email) + end + + # validates :contact_email, presence: { message: PRESENCE_MESSAGE, + # if: :feedback_enabled } + # + # validates :org_type, presence: { message: PRESENCE_MESSAGE } + # + # validates :feedback_enabled, inclusion: { in: BOOLEAN_VALUES, + # message: INCLUSION_MESSAGE } + # + # validates :feedback_email_subject, presence: { message: PRESENCE_MESSAGE, + # if: :feedback_enabled } + # + # validates :feedback_email_msg, presence: { message: PRESENCE_MESSAGE, + # if: :feedback_enabled } + # + end + + context "associations" do + + it { should belong_to(:language) } + + it { should belong_to(:region) } + + it { should have_many(:guidance_groups).dependent(:destroy) } + + it { should have_many(:templates) } + + it { should have_many(:users) } + + it { should have_many(:annotations) } + + it { should have_and_belong_to_many(:token_permission_types).join_table("org_token_permissions") } + + it { should have_many(:org_identifiers) } + + it { should have_many(:identifier_schemes).through(:org_identifiers) } + + end + + describe ".managing_orgs" do + + subject { Org.managing_orgs } + + context "when Org has same abbr as branding" do + + let!(:org) do + create(:org, + abbreviation: Rails.configuration + .branding.dig(:organisation, :abbreviation)) + + end + + it { is_expected.to include(org) } + + end + + context "when Org doesn't have same abbr as branding" do + + let!(:org) { create(:org, abbreviation: 'foo-bar') } + + it { is_expected.not_to include(org) } + + end + end + + describe "#get_locale" do + + let!(:org) { build(:org) } + + subject { org.get_locale } + + context "language present" do + + it { is_expected.to be_present } + + end + + context "language absent" do + + before do + org.language.abbreviation = nil + end + + it { is_expected.to be_nil } + + end + + end + + describe "#org_type_to_s" do + + subject { org.org_type_to_s } + + context "no organisation present" do + + let!(:org) { build(:org) } + + it { is_expected.to eql("None") } + + end + + context "organisation present" do + + context "when single organisation type and organisation type is Institution" do + + let!(:org) { build(:org, :institution) } + + it { is_expected.to eql("Institution") } + + end + + context "when single organisation type and organisation type is Funder" do + + let!(:org) { build(:org, :funder) } + + it { is_expected.to eql("Funder") } + + end + + context "when single organisation type and organisation type is Organisation" do + + let!(:org) { build(:org, :organisation) } + + it { is_expected.to eql("Organisation") } + + end + + context "when single organisation type and organisation type is Research Institute" do + + let!(:org) { build(:org, :research_institute) } + + it { is_expected.to eql("Research Institute") } + + end + + context "when single organisation type and organisation type is Project" do + + let!(:org) { build(:org, :project) } + + it { is_expected.to eql("Project") } + + end + + context "when single organisation type and organisation type is School" do + + let!(:org) { build(:org, :school) } + + it { is_expected.to eql("School") } + + end + + context "when organisation has multiple organisation types" do + + let!(:org) { build(:org, :funder, :school) } + + it { is_expected.to include("Funder","School") } + + end + + end + end + + describe "#funder_only?" do + + let!(:org) { build(:org) } + + subject { org.funder_only? } + + context "when organistation type is only Funder" do + + before do + org.funder = true; + end + + it { is_expected.to be true } + + end + + context "when multiple organistation types present" do + + before do + org.institution = true; + org.funder = true; + end + + it { is_expected.to be false } + + end + end + + describe "#to_s" do + let!(:org) { build(:org) } + + subject { org.to_s } + + it { is_expected.to_not be_blank } + + end + + describe "short_name" do + + let!(:org) { build(:org) } + + subject { org.short_name } + + context "when abbreviation present" do + + it { is_expected.to_not be_blank } + + end + + context "when abbreviation absent" do + + before do + org.abbreviation = nil + end + + it { is_expected.to_not be_blank } + + end + end + + describe "#published_templates" do + + let!(:org) { build(:org) } + + subject { org.published_templates } + + context "when template is published" do + + before do + @template = create(:template, published: true, org: org) + end + + it { is_expected.to include(@template) } + + end + + context "when template is not published" do + before do + @template = create(:template, published: false, org: org) + end + + it { is_expected.not_to include(@template) } + + end + end + + describe "#org_admins" do + + let!(:org) { create(:org) } + let!(:user) { create(:user, org: org) } + + subject { org.org_admins } + + + context "when user belongs to Org with perms absent" do + + before do + @perm = create(:perm) + user.org = org + end + + it { is_expected.to be_empty } + + end + + context "when user belongs to Org with grant_permissions perm" do + + before do + @perm = build(:perm) + @perm.name = "grant_permissions" + user.perms << @perm + end + + it { is_expected.to_not be_empty } + end + + context "when user belongs to Org with modify_templates perm" do + + before do + @perm = build(:perm) + @perm.name = "modify_templates" + user.perms << @perm + end + + it { is_expected.to_not be_empty } + end + + context "when user belongs to Org with modify_guidance perm" do + + before do + @perm = build(:perm) + @perm.name = "modify_guidance" + user.perms << @perm + end + + it { is_expected.to_not be_empty } + end + + context "when user belongs to Org with change_org_details perm present " do + + before do + @perm = build(:perm) + @perm.name = "change_org_details" + user.perms << @perm + end + + it { is_expected.to_not be_empty } + + end + end + + + describe "#plans" do + + let!(:org) { create(:org) } + let!(:user) { create(:user, org: org) } + let!(:plan) { create(:plan) } + + subject { org.plans } + + context "when user belongs to Org and plan owner with role :creator" do + + before do + plan.assign_creator(user) + end + + it { is_expected.to include(plan) } + + end + + context "when user belongs to Org and plan user with role :administrator" do + + pending("TBD") do + + before do + plan.assign_administrator(user) + end + + it { is_expected.to include(plan) } + end + end + + + context "when user belongs to Org and plan user with role :editor" do + + pending("TBD") do + + it { is_expected.not_to include(plan) } + + end + + end + end + + + +end diff --git a/spec/models/perm_spec.rb b/spec/models/perm_spec.rb new file mode 100644 index 0000000..f22a19f --- /dev/null +++ b/spec/models/perm_spec.rb @@ -0,0 +1,578 @@ +require 'rails_helper' + +RSpec.describe Perm, type: :model do + + context "validations" do + + it { is_expected.to validate_presence_of(:name) } + + it { is_expected.to validate_uniqueness_of(:name) + .with_message("must be unique") } + end + + context "associations" do + + it { is_expected.to have_and_belong_to_many(:users) } + + end + + describe ".add_orgs" do + + subject { Perm.add_orgs } + + context "when name is 'add_orgs'" do + + let!(:perm) { create(:perm, name: "add_organisations") } + + it { is_expected.to eql(perm) } + + end + + context "when name is 'change_affiliation'" do + + let!(:perm) { create(:perm, name: "change_org_affiliation") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'grant_permissions'" do + + let!(:perm) { create(:perm, name: "grant_permissions") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'modify_templates'" do + + let!(:perm) { create(:perm, name: "modify_templates") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'modify_guidance'" do + + let!(:perm) { create(:perm, name: "modify_guidance") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'use_api'" do + + let!(:perm) { create(:perm, name: "use_api") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'change_org_details'" do + + let!(:perm) { create(:perm, name: "change_org_details") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'grant_api'" do + + let!(:perm) { create(:perm, name: "grant_api_to_orgs") } + + it { is_expected.not_to eql(perm) } + + end + + end + + describe ".change_affiliation" do + + subject { Perm.change_affiliation } + + context "when name is 'add_orgs'" do + + let!(:perm) { create(:perm, name: "add_organisations") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'change_affiliation'" do + + let!(:perm) { create(:perm, name: "change_org_affiliation") } + + it { is_expected.to eql(perm) } + + end + + context "when name is 'grant_permissions'" do + + let!(:perm) { create(:perm, name: "grant_permissions") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'modify_templates'" do + + let!(:perm) { create(:perm, name: "modify_templates") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'modify_guidance'" do + + let!(:perm) { create(:perm, name: "modify_guidance") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'use_api'" do + + let!(:perm) { create(:perm, name: "use_api") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'change_org_details'" do + + let!(:perm) { create(:perm, name: "change_org_details") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'grant_api'" do + + let!(:perm) { create(:perm, name: "grant_api_to_orgs") } + + it { is_expected.not_to eql(perm) } + + end + + end + + describe ".grant_permissions" do + + subject { Perm.grant_permissions } + + context "when name is 'add_orgs'" do + + let!(:perm) { create(:perm, name: "add_organisations") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'change_affiliation'" do + + let!(:perm) { create(:perm, name: "change_org_affiliation") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'grant_permissions'" do + + let!(:perm) { create(:perm, name: "grant_permissions") } + + it { is_expected.to eql(perm) } + + end + + context "when name is 'modify_templates'" do + + let!(:perm) { create(:perm, name: "modify_templates") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'modify_guidance'" do + + let!(:perm) { create(:perm, name: "modify_guidance") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'use_api'" do + + let!(:perm) { create(:perm, name: "use_api") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'change_org_details'" do + + let!(:perm) { create(:perm, name: "change_org_details") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'grant_api'" do + + let!(:perm) { create(:perm, name: "grant_api_to_orgs") } + + it { is_expected.not_to eql(perm) } + + end + + end + + describe ".modify_templates" do + + subject { Perm.modify_templates } + + context "when name is 'add_orgs'" do + + let!(:perm) { create(:perm, name: "add_organisations") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'change_affiliation'" do + + let!(:perm) { create(:perm, name: "change_org_affiliation") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'grant_permissions'" do + + let!(:perm) { create(:perm, name: "grant_permissions") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'modify_templates'" do + + let!(:perm) { create(:perm, name: "modify_templates") } + + it { is_expected.to eql(perm) } + + end + + context "when name is 'modify_guidance'" do + + let!(:perm) { create(:perm, name: "modify_guidance") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'use_api'" do + + let!(:perm) { create(:perm, name: "use_api") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'change_org_details'" do + + let!(:perm) { create(:perm, name: "change_org_details") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'grant_api'" do + + let!(:perm) { create(:perm, name: "grant_api_to_orgs") } + + it { is_expected.not_to eql(perm) } + + end + + end + + describe ".modify_guidance" do + + subject { Perm.modify_guidance } + + context "when name is 'add_orgs'" do + + let!(:perm) { create(:perm, name: "add_organisations") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'change_affiliation'" do + + let!(:perm) { create(:perm, name: "change_org_affiliation") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'grant_permissions'" do + + let!(:perm) { create(:perm, name: "grant_permissions") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'modify_templates'" do + + let!(:perm) { create(:perm, name: "modify_templates") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'modify_guidance'" do + + let!(:perm) { create(:perm, name: "modify_guidance") } + + it { is_expected.to eql(perm) } + + end + + context "when name is 'use_api'" do + + let!(:perm) { create(:perm, name: "use_api") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'change_org_details'" do + + let!(:perm) { create(:perm, name: "change_org_details") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'grant_api'" do + + let!(:perm) { create(:perm, name: "grant_api_to_orgs") } + + it { is_expected.not_to eql(perm) } + + end + + end + + describe ".use_api" do + + subject { Perm.use_api } + + context "when name is 'add_orgs'" do + + let!(:perm) { create(:perm, name: "add_organisations") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'change_affiliation'" do + + let!(:perm) { create(:perm, name: "change_org_affiliation") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'grant_permissions'" do + + let!(:perm) { create(:perm, name: "grant_permissions") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'modify_templates'" do + + let!(:perm) { create(:perm, name: "modify_templates") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'modify_guidance'" do + + let!(:perm) { create(:perm, name: "modify_guidance") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'use_api'" do + + let!(:perm) { create(:perm, name: "use_api") } + + it { is_expected.to eql(perm) } + + end + + context "when name is 'change_org_details'" do + + let!(:perm) { create(:perm, name: "change_org_details") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'grant_api'" do + + let!(:perm) { create(:perm, name: "grant_api_to_orgs") } + + it { is_expected.not_to eql(perm) } + + end + + end + + describe ".change_org_details" do + + subject { Perm.change_org_details } + + context "when name is 'add_orgs'" do + + let!(:perm) { create(:perm, name: "add_organisations") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'change_affiliation'" do + + let!(:perm) { create(:perm, name: "change_org_affiliation") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'grant_permissions'" do + + let!(:perm) { create(:perm, name: "grant_permissions") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'modify_templates'" do + + let!(:perm) { create(:perm, name: "modify_templates") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'modify_guidance'" do + + let!(:perm) { create(:perm, name: "modify_guidance") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'use_api'" do + + let!(:perm) { create(:perm, name: "use_api") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'change_org_details'" do + + let!(:perm) { create(:perm, name: "change_org_details") } + + it { is_expected.to eql(perm) } + + end + + context "when name is 'grant_api'" do + + let!(:perm) { create(:perm, name: "grant_api") } + + it { is_expected.not_to eql(perm) } + + end + + end + + describe ".grant_api" do + + subject { Perm.grant_api } + + context "when name is 'add_orgs'" do + + let!(:perm) { create(:perm, name: "add_organisations") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'change_affiliation'" do + + let!(:perm) { create(:perm, name: "change_org_affiliation") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'grant_permissions'" do + + let!(:perm) { create(:perm, name: "grant_permissions") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'modify_templates'" do + + let!(:perm) { create(:perm, name: "modify_templates") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'modify_guidance'" do + + let!(:perm) { create(:perm, name: "modify_guidance") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'use_api'" do + + let!(:perm) { create(:perm, name: "use_api") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'change_org_details'" do + + let!(:perm) { create(:perm, name: "change_org_details") } + + it { is_expected.not_to eql(perm) } + + end + + context "when name is 'grant_api'" do + + let!(:perm) { create(:perm, name: "grant_api_to_orgs") } + + it { is_expected.to eql(perm) } + + end + end + +end diff --git a/spec/models/phase_spec.rb b/spec/models/phase_spec.rb new file mode 100644 index 0000000..d93d19a --- /dev/null +++ b/spec/models/phase_spec.rb @@ -0,0 +1,198 @@ +require 'rails_helper' + +RSpec.describe Phase, type: :model do + + context "validations" do + it { is_expected.to validate_presence_of(:title) } + + it { is_expected.to validate_presence_of(:number) } + + it { is_expected.to validate_presence_of(:template) } + + it { is_expected.to validate_uniqueness_of(:number) + .scoped_to(:template_id) + .with_message("must be unique") } + + it { is_expected.to allow_values(true, false).for(:modifiable) } + + it { is_expected.not_to allow_value(nil).for(:modifiable) } + + end + + context "associations" do + + it { is_expected.to belong_to(:template) } + + it { is_expected.to have_one :prefix_section } + + it { is_expected.to have_many :sections } + + it { is_expected.to have_many :template_sections } + + it { is_expected.to have_many :suffix_sections } + + end + + describe ".titles" do + + let!(:phase) { create(:phase) } + + let!(:template) { phase.template } + + subject { Phase.titles(template.id) } + + before do + @related_phases = create_list(:phase, 2, template: template) + @strange_phases = create_list(:phase, 2) + end + + it "returns related phases" do + @related_phases.each do |phase| + expect(subject).to include(phase) + end + end + + it "excludes phases of different Templates" do + @strange_phases.each do |phase| + expect(subject).not_to include(phase) + end + end + + end + + describe "#deep_copy" do + + let!(:phase) { create(:phase, modifiable: false) } + + let!(:options) { Hash.new } + + subject { phase.deep_copy(options) } + + context "when no options are provided" do + + before do + create_list(:section, 2, phase: phase) + end + + it "doesn't persist the record" do + expect(subject).to be_a_new_record + end + + it "copies the description attribute" do + expect(subject.description).to eql(phase.description) + end + + it "copies the modifiable attribute" do + expect(subject.modifiable).to eql(phase.modifiable) + end + + it "copies the number attribute" do + expect(subject.number).to eql(phase.number) + end + + it "copies the title attribute" do + expect(subject.title).to eql(phase.title) + end + + it "sets template_id to nil" do + expect(subject.template_id).to be_nil + end + + it "duplicates the sections belonging to the Phase" do + expect(subject.sections.count).to eql(subject.sections.count) + end + + end + + context "when modifiable option is true" do + + let!(:options) { { modifiable: true } } + + it "sets the modifiable flag to true" do + expect(subject.modifiable).to eql(true) + end + + end + + context "when template_id option is present" do + + let!(:options) { { template_id: create(:template).id } } + + it "sets the template_id to new value" do + expect(subject.template_id).to eql(options[:template_id]) + end + + end + + context "when save option is true" do + + let!(:options) { { save: true } } + + it "persists the record" do + expect(subject).to be_persisted + end + + end + + end + + describe "#num_answered_questions" do + + let!(:phase) { create(:phase) } + + subject { phase.num_answered_questions(plan) } + + context "when plan is nil" do + + let!(:plan) { nil } + + it "returns 0" do + expect(subject).to be_zero + end + + end + + context "when plan is present" do + + let!(:phase) { create(:phase, template: template) } + + let!(:section) { create(:section, phase: phase) } + + let!(:plan) { create(:plan) } + + let!(:template) { plan.template } + + before do + question = create(:question, section: section) + create(:answer, question: question, plan: plan, text: '') + + question = create(:question, section: section) + create(:answer, question: question, plan: plan) + + question = create(:question, section: section) + create(:answer, question: question, plan: plan) + end + + it "returns the sum of Plan's Phase's num_answered_questions" do + expect(subject).to eql(2) + end + + end + end + + describe "#num_questions" do + + let!(:phase) { create(:phase) } + + before do + create_list(:section, 2, phase: phase).each do |section| + create_list(:question, 2, section: section) + end + end + + it "returns the number of related questions" do + expect(phase.num_questions).to eql(4) + end + end + +end diff --git a/spec/models/plan_spec.rb b/spec/models/plan_spec.rb new file mode 100644 index 0000000..a0ed40e --- /dev/null +++ b/spec/models/plan_spec.rb @@ -0,0 +1,1719 @@ +require 'rails_helper' + +describe Plan do + + context "validations" do + it { is_expected.to validate_presence_of(:title) } + + it { is_expected.to validate_presence_of(:template) } + + it { is_expected.to allow_values(true, false).for(:feedback_requested) } + + it { is_expected.not_to allow_value(nil).for(:feedback_requested) } + + it { is_expected.to allow_values(true, false).for(:complete) } + + it { is_expected.not_to allow_value(nil).for(:complete) } + end + + context "associations" do + + it { is_expected.to belong_to :template } + + + it { is_expected.to have_many :phases } + + it { is_expected.to have_many :sections } + + it { is_expected.to have_many :questions } + + it { is_expected.to have_many :themes } + + it { is_expected.to have_many :answers } + + it { is_expected.to have_many :notes } + + it { is_expected.to have_many :roles } + + it { is_expected.to have_many :users } + + it { is_expected.to have_many :exported_plans } + + it { is_expected.to have_many :setting_objects } + + end + + describe ".publicly_visible" do + + subject { Plan.publicly_visible } + + context "when plan visibility is publicly_visible" do + + let!(:plan) { create(:plan, :publicly_visible) } + + it { is_expected.to include(plan) } + + end + + context "when plan visibility is organisationally_visible" do + + let!(:plan) { create(:plan, :organisationally_visible) } + + it { is_expected.not_to include(plan) } + + end + + context "when plan visibility is is_test" do + + let!(:plan) { create(:plan, :is_test) } + + it { is_expected.not_to include(plan) } + + end + + context "when plan visibility is privately_visible" do + + let!(:plan) { create(:plan, :privately_visible) } + + it { is_expected.not_to include(plan) } + + end + + end + + describe ".organisationally_visible" do + + subject { Plan.organisationally_visible } + + context "when plan visibility is publicly_visible" do + + let!(:plan) { create(:plan, :publicly_visible) } + + it { is_expected.not_to include(plan) } + + end + + context "when plan visibility is organisationally_visible" do + + let!(:plan) { create(:plan, :organisationally_visible) } + + it { is_expected.to include(plan) } + + end + + context "when plan visibility is is_test" do + + let!(:plan) { create(:plan, :is_test) } + + it { is_expected.not_to include(plan) } + + end + + context "when plan visibility is privately_visible" do + + let!(:plan) { create(:plan, :privately_visible) } + + it { is_expected.not_to include(plan) } + + end + + end + + describe ".privately_visible" do + + subject { Plan.privately_visible } + + context "when plan visibility is publicly_visible" do + + let!(:plan) { create(:plan, :publicly_visible) } + + it { is_expected.not_to include(plan) } + + end + + context "when plan visibility is organisationally_visible" do + + let!(:plan) { create(:plan, :organisationally_visible) } + + it { is_expected.not_to include(plan) } + + end + + context "when plan visibility is is_test" do + + let!(:plan) { create(:plan, :is_test) } + + it { is_expected.not_to include(plan) } + + end + + context "when plan visibility is privately_visible" do + + let!(:plan) { create(:plan, :privately_visible) } + + it { is_expected.to include(plan) } + + end + + end + + describe ".organisationally_or_publicly_visible" do + + let!(:user) { create(:user) } + + subject { Plan.organisationally_or_publicly_visible(user) } + + context "when user is creator" do + + before do + create(:role, :creator, user: user, plan: plan) + end + + let!(:plan) { create(:plan, :publicly_visible) } + + it { is_expected.not_to include(plan) } + + end + + context "when user is administrator" do + + before do + create(:role, :administrator, user: user, plan: plan) + end + + let!(:plan) { create(:plan, :publicly_visible) } + + it { is_expected.not_to include(plan) } + + end + + context "when user is commenter" do + + before do + create(:role, :commenter, user: user, plan: plan) + end + + let!(:plan) { create(:plan, :publicly_visible) } + + it { is_expected.not_to include(plan) } + + end + + context "when user is editor" do + + before do + create(:role, :editor, user: user, plan: plan) + end + + let!(:plan) { create(:plan, :publicly_visible) } + + it { is_expected.not_to include(plan) } + + end + + context "when plan visibility is publicly_visible" do + + let!(:plan) { create(:plan, :publicly_visible) } + + xit "TODO: Fix this spec" do + is_expected.to include(plan) + end + + end + + context "when plan visibility is organisationally_visible" do + + let!(:plan) { create(:plan, :organisationally_visible) } + + xit "TODO: Fix this spec" do + is_expected.to include(plan) + end + + end + + context "when plan visibility is is_test" do + + let!(:plan) { create(:plan, :is_test) } + + it { is_expected.not_to include(plan) } + + end + + context "when plan visibility is privately_visible" do + + let!(:plan) { create(:plan, :privately_visible) } + + it { is_expected.not_to include(plan) } + + end + + end + + describe ".is_test" do + + subject { Plan.is_test } + + context "when plan visibility is publicly_visible" do + + let!(:plan) { create(:plan, :publicly_visible) } + + it { is_expected.not_to include(plan) } + + end + + context "when plan visibility is organisationally_visible" do + + let!(:plan) { create(:plan, :organisationally_visible) } + + it { is_expected.not_to include(plan) } + + end + + context "when plan visibility is is_test" do + + let!(:plan) { create(:plan, :is_test) } + + it { is_expected.to include(plan) } + + end + + context "when plan visibility is privately_visible" do + + let!(:plan) { create(:plan, :privately_visible) } + + it { is_expected.not_to include(plan) } + + end + + end + + describe ".active" do + + let!(:plan) { create(:plan) } + + let!(:user) { create(:user) } + + subject { Plan.active(user) } + + context "where user role is active" do + + before do + create(:role, :active, :creator, user: user, plan: plan) + end + + it { is_expected.to include(plan) } + + end + + context "where user role is not active" do + + before do + create(:role, :inactive, :creator, user: user, plan: plan) + end + + it { is_expected.not_to include(plan) } + + end + + context "where user role is reviewer" do + + before do + create(:role, :active, :reviewer, user: user, plan: plan) + end + + it { is_expected.not_to include(plan) } + + end + + end + + describe ".load_for_phase" do + + let!(:template) { create(:template) } + + let!(:plan) { create(:plan, template: template) } + + let!(:phase) { create(:phase, template: template) } + + let!(:section) { create(:section, phase: phase) } + + let!(:question) { create(:question, section: section) } + + subject { Plan.load_for_phase(plan.id, phase.id) } + + context "when Plan ID is valid and Phase ID is valid child" do + + it "returns an Array" do + expect(subject).to be_an(Array) + end + + it "returns the Plan first" do + expect(subject.first).to eql(plan) + end + + it "returns the Phase second" do + expect(subject.second).to eql(phase) + end + + end + + context "when Plan ID is valid and Phase ID is not valid child" do + + let!(:phase) { create(:phase) } + + it "raises an exception" do + # TODO: This is not ideal behaviour. Fix this. + expect { subject }.to raise_error(NoMethodError) + end + + end + + context "when Plan ID is not valid" do + + let!(:plan) { stub(id: 0) } + + it "raises an exception" do + # TODO: This is not ideal behaviour. Fix this. + expect { subject }.to raise_error(NoMethodError) + end + + end + + end + + describe ".deep_copy" do + + let!(:plan) { create(:plan, answers: 2, guidance_groups: 2) } + + subject { Plan.deep_copy(plan) } + + it "prepends the title with 'Copy'" do + expect(subject.title).to include("Copy") + end + + it "copies the title from source" do + expect(subject.title).to include(plan.title) + end + + it "persists the record" do + expect(subject).to be_persisted + end + + it "creates new copies of the answers" do + expect(subject.answers).to have(2).items + end + + it "duplicates the guidance groups" do + expect(subject.guidance_groups).to have(2).items + end + end + + describe ".search" do + + subject { Plan.search("foo") } + + context "when Plan title matches term" do + + let!(:plan) { create(:plan, title: "foolike title") } + + it { is_expected.to include(plan) } + + end + + context "when Template title matches term" do + + let!(:template) { create(:template, title: "foolike title") } + + let!(:plan) { create(:plan, template: template) } + + it { is_expected.to include(plan) } + + end + + context "when neither title matches term" do + + let!(:plan) { create(:plan, description: "foolike desc") } + + it { is_expected.not_to include(plan) } + + end + + end + + describe "#answer" do + + let!(:plan) { create(:plan, answers: 1) } + + let!(:question) { create(:question) } + + subject { plan.answer(question.id, create_if_missing) } + + + context "when create_if_missing is true and answer exists on the DB" do + + let!(:create_if_missing) { true } + + let!(:answer) { create(:answer, plan: plan, question: question) } + + it "returns the existing Answer" do + expect(subject).to eql(answer) + end + + end + + context "when create_if_missing is true and answer doesn't exist on the DB" do + + let!(:create_if_missing) { true } + + it "returns a new Answer" do + expect(subject).to be_an(Answer) + end + + it "doesn't persist the new Answer" do + expect(subject).to be_new_record + end + + end + + context "when create_if_missing is false and qid exists on the DB" do + + let!(:create_if_missing) { false } + + let!(:answer) { create(:answer, plan: plan, question: question) } + + it "returns the existing Answer" do + expect(subject).to eql(answer) + end + + end + + context "when create_if_missing is false and qid doesn't exist on the DB" do + + let!(:create_if_missing) { false } + + let!(:answer) { nil } + + it "returns nil" do + expect(subject).to be_nil + end + + end + end + + describe "#guidance_group_options" do + + let!(:plan) { create(:plan) } + + subject { plan.guidance_group_options } + + before do + @phase = create(:phase, template: plan.template) + @section = create(:section, phase: @phase) + @question = create(:question, section: @section) + @theme = create(:theme) + @guidance = create(:guidance) + @guidance_group = @guidance.guidance_group + @question.themes << @theme + @theme.guidances << @guidance + end + + context "when guidance groups are unpublished" do + + before do + @guidance_group.update(published: false) + end + + it "excludes the guidance group from options" do + expect(subject).not_to include(@guidance_group) + end + + end + + context "when guidance groups are published" do + + it "includes the guidance group in options" do + expect(subject).to include(@guidance_group) + end + + end + + end + + describe "#request_feedback" do + + subject { plan.request_feedback(user) } + + let!(:org) { create(:org) } + + let!(:user) { create(:user, org: org) } + + let!(:plan) { create(:plan) } + + before do + # Create 2 Org admins for this Org. + create_list(:user, 2, org: org).each do |user| + user.perms << Perm.where(name: 'modify_guidance').first_or_create + end + end + + it "changes plan's feedback_requested value to true" do + expect { subject }.to change { + plan.reload.feedback_requested + }.from(false).to(true) + end + + it "doesn't send any emails" do + expect { subject }.not_to change { ActionMailer::Base.deliveries.size } + end + + context "when org contact_email present" do + + before do + org.update!(contact_email: Faker::Internet.safe_email) + end + + it "emails the admins" do + expect { subject }.to change { + ActionMailer::Base.deliveries.size + }.by(1) + end + + end + + end + + describe "#complete_feedback" do + + subject { plan.complete_feedback(user) } + + let!(:org) { create(:org) } + + let!(:user) { create(:user, org: org) } + + let!(:admin) { create(:user) } + + let!(:template) { create(:template, phases: 2) } + + let!(:plan) { create(:plan, feedback_requested: true, template: template) } + + before do + create(:role, :creator, plan: plan, user: user) + # This person gets the email notification + create(:role, :administrator, plan: plan, user: admin) + create_list(:role, 2, :reviewer, plan: plan) + end + + it "changes plan's feedback_requested value to false" do + expect { subject }.to change { + plan.reload.feedback_requested + }.from(true).to(false) + end + + it "destroys the reviewer Roles" do + expect { subject }.to change { plan.roles.count }.by(-2) + end + + it "doesn't send any emails" do + User.any_instance.stubs(:get_preferences) + .returns(:users => { :feedback_provided => false }) + expect { subject }.not_to change { ActionMailer::Base.deliveries.size } + end + + context "when user feedback provided pref is true" do + + before do + User.any_instance.stubs(:get_preferences) + .returns(:users => { :feedback_provided => true }) + end + + it "emails the owners" do + expect { subject }.to change { + ActionMailer::Base.deliveries.size + }.by(1) + end + + end + + end + + describe "#guidance_by_question_as_hash" do + + + + end + + describe "#editable_by?" do + + let!(:user) { create(:user) } + + let!(:plan) { create(:plan) } + + subject { plan } + + context "when User has no Role for this Plan" do + + it { is_expected.not_to be_editable_by(user.id) } + + end + + context "when User is passed instead of User ID" do + + before do + create(:role, :editor, plan: plan, user: user) + end + + it { is_expected.to be_editable_by(user) } + + end + + context "when user Role :creator" do + + before do + create(:role, :creator, plan: plan, user: user) + end + + it { is_expected.not_to be_editable_by(user.id) } + + end + + context "when user Role :administrator" do + + before do + create(:role, :administrator, plan: plan, user: user) + end + + it { is_expected.not_to be_editable_by(user.id) } + + end + + context "when user Role :editor" do + + before do + create(:role, :editor, plan: plan, user: user) + end + + it { is_expected.to be_editable_by(user.id) } + + end + + context "when user Role :commenter" do + + before do + create(:role, :commenter, plan: plan, user: user) + end + + it { is_expected.not_to be_editable_by(user.id) } + + end + + context "when user Role :reviewer" do + + before do + create(:role, :reviewer, plan: plan, user: user) + end + + it { is_expected.not_to be_editable_by(user.id) } + + end + + end + + describe "#readable_by?" do + + let!(:plan) { create(:plan) } + + let!(:creator) do + create(:user).tap { |u| create(:role, :creator, user: u, plan: plan) } + end + + let!(:org) { creator.org } + + let!(:user) { create(:user, org: org) } + + subject { plan } + + context "when User has super admin permission" do + + before do + user.perms << create(:perm, name: "add_organisations") + end + + it { is_expected.to be_readable_by(user) } + + end + + context "when User is Org admin & user is Org owner & system permission" do + + before do + user.perms << create(:perm, name: "grant_permissions") + Branding.expects(:fetch) + .with(:service_configuration, :plans, :org_admins_read_all) + .returns(true) + end + + it { is_expected.to be_readable_by(user) } + + end + + context "when User is Org admin & user is Org owner & not system permission" do + + before do + user.perms << create(:perm, name: "grant_permissions") + Branding.expects(:fetch) + .with(:service_configuration, :plans, :org_admins_read_all) + .returns(false) + end + + it { is_expected.not_to be_readable_by(user) } + + end + + context "when User is Org admin & user not Org owner & system permission" do + + before do + user.update(org: create(:org)) + + user.perms << create(:perm, name: "grant_permissions") + Branding.expects(:fetch) + .with(:service_configuration, :plans, :org_admins_read_all) + .returns(true) + end + + it { is_expected.not_to be_readable_by(user) } + + end + + context "when User is Org admin & user not Org owner & not system permission" do + + before do + user.update(org: create(:org)) + + user.perms << create(:perm, name: "grant_permissions") + Branding.expects(:fetch) + .with(:service_configuration, :plans, :org_admins_read_all) + .returns(false) + end + + it { is_expected.not_to be_readable_by(user) } + + end + + context "when User not Org admin & user not Org owner & system permission" do + + before do + user.update(org: create(:org)) + Branding.expects(:fetch) + .with(:service_configuration, :plans, :org_admins_read_all) + .returns(true) + end + + it { is_expected.not_to be_readable_by(user) } + + end + + context "when User not Org admin & user not Org owner & not system permission" do + + before do + user.update(org: create(:org)) + Branding.expects(:fetch) + .with(:service_configuration, :plans, :org_admins_read_all) + .returns(false) + end + + it { is_expected.not_to be_readable_by(user) } + + end + + context "when User has commenter role" do + + before do + create(:role, :commenter, user: user, plan: plan) + end + + it { is_expected.to be_readable_by(user) } + + end + + context "when User doesn't have commenter Role" do + + it { is_expected.not_to be_readable_by(user) } + + end + + end + + describe "#commentable_by?" do + + let!(:user) { create(:user) } + + let!(:plan) { create(:plan) } + + subject { plan } + + context "when User has no Role for this Plan" do + + it { is_expected.not_to be_commentable_by(user.id) } + + end + + context "when User is passed instead of User ID" do + + before do + create(:role, :commenter, plan: plan, user: user) + end + + it { is_expected.to be_commentable_by(user) } + + end + + context "when user Role :creator" do + + before do + create(:role, :creator, plan: plan, user: user) + end + + it { is_expected.not_to be_commentable_by(user.id) } + + end + + context "when user Role :administrator" do + + before do + create(:role, :administrator, plan: plan, user: user) + end + + it { is_expected.not_to be_commentable_by(user.id) } + + end + + context "when user Role :editor" do + + before do + create(:role, :editor, plan: plan, user: user) + end + + it { is_expected.not_to be_commentable_by(user.id) } + + end + + context "when user Role :commenter" do + + before do + create(:role, :commenter, plan: plan, user: user) + end + + it { is_expected.to be_commentable_by(user.id) } + + end + + context "when user Role :reviewer" do + + before do + create(:role, :reviewer, plan: plan, user: user) + end + + it { is_expected.not_to be_commentable_by(user.id) } + + end + + end + + describe "#administerable_by?" do + + let!(:user) { create(:user) } + + let!(:plan) { create(:plan) } + + subject { plan } + + context "when User has no Role for this Plan" do + + it { is_expected.not_to be_administerable_by(user.id) } + + end + + context "when User is passed instead of User ID" do + + before do + create(:role, :administrator, plan: plan, user: user) + end + + it { is_expected.to be_administerable_by(user) } + + end + + context "when user Role :creator" do + + before do + create(:role, :creator, plan: plan, user: user) + end + + it { is_expected.not_to be_administerable_by(user.id) } + + end + + context "when user Role :administrator" do + + before do + create(:role, :administrator, plan: plan, user: user) + end + + it { is_expected.to be_administerable_by(user.id) } + + end + + context "when user Role :editor" do + + before do + create(:role, :editor, plan: plan, user: user) + end + + it { is_expected.not_to be_administerable_by(user.id) } + + end + + context "when user Role :commenter" do + + before do + create(:role, :commenter, plan: plan, user: user) + end + + it { is_expected.not_to be_administerable_by(user.id) } + + end + + context "when user Role :reviewer" do + + before do + create(:role, :reviewer, plan: plan, user: user) + end + + it { is_expected.not_to be_administerable_by(user.id) } + + end + + end + + describe "#reviewable_by?" do + + let!(:user) { create(:user) } + + let!(:plan) { create(:plan) } + + subject { plan } + + context "when User has no Role for this Plan" do + + it { is_expected.not_to be_reviewable_by(user.id) } + + end + + context "when User is passed instead of User ID" do + + before do + create(:role, :reviewer, plan: plan, user: user) + end + + it { is_expected.to be_reviewable_by(user) } + + end + + context "when user Role :creator" do + + before do + create(:role, :creator, plan: plan, user: user) + end + + it { is_expected.not_to be_reviewable_by(user.id) } + + end + + context "when user Role :administrator" do + + before do + create(:role, :administrator, plan: plan, user: user) + end + + it { is_expected.not_to be_reviewable_by(user.id) } + + end + + context "when user Role :editor" do + + before do + create(:role, :editor, plan: plan, user: user) + end + + it { is_expected.not_to be_reviewable_by(user.id) } + + end + + context "when user Role :commenter" do + + before do + create(:role, :commenter, plan: plan, user: user) + end + + it { is_expected.not_to be_reviewable_by(user.id) } + + end + + context "when user Role :reviewer" do + + before do + create(:role, :reviewer, plan: plan, user: user) + end + + it { is_expected.to be_reviewable_by(user.id) } + + end + + end + + describe "#assign_creator" do + + let!(:plan) { create(:plan) } + + let!(:user) { create(:user) } + + subject { plan.assign_creator(user.id) } + + it "creates a role for the user and plan" do + expect { subject }.to change { user.roles.count }.by(1) + end + + end + + describe "#latest_update" do + + let!(:plan) { create(:plan, updated_at: 5.minutes.ago) } + + subject { plan.latest_update.to_i } + + context "when plan updated_at is latest" do + + before do + create_list(:phase, 2, template: plan.template, + updated_at: 6.minutes.ago) + end + + it "returns the plan's updated_at value" do + is_expected.to be_within(5.seconds).of(5.minutes.ago.to_i) + end + + end + + context "when plan has phases updated_at latest" do + + before do + create_list(:phase, 2, template: plan.template) + end + + it "returns the plan's updated_at value" do + is_expected.to be_within(5.seconds).of(Time.current.to_i) + end + + end + + end + + describe "#name" do + + let!(:plan) { build(:plan, title: "Foo bar") } + + it "returns the title" do + expect(plan.name).to eql("Foo bar") + end + + end + + describe "#owner" do + + subject { plan.owner } + + let!(:plan) { create(:plan) } + + let!(:user) { create(:user) } + + context "when user Role is :creator" do + + before do + create(:role, :creator, plan: plan, user: user) + end + + it { is_expected.to eql(user) } + + end + + context "when user Role is :administrator" do + + before do + create(:role, :administrator, plan: plan, user: user) + end + + it { is_expected.to be_nil } + + end + + context "when user Role is :editor" do + + before do + create(:role, :editor, plan: plan, user: user) + end + + it { is_expected.to be_nil } + + end + + context "when user Role is :commenter" do + + before do + create(:role, :commenter, plan: plan, user: user) + end + + it { is_expected.to be_nil } + + end + + context "when user Role is :reviewer" do + + before do + create(:role, :reviewer, plan: plan, user: user) + end + + it { is_expected.to be_nil } + + end + + end + + describe "#shared?" do + + subject { plan.shared? } + + let!(:plan) { create(:plan) } + + context "when roles are: creator" do + + before do + create(:role, :creator, plan: plan) + end + + it { is_expected.to eql(false) } + + end + + context "when roles are: administrator" do + + before do + create(:role, :administrator, plan: plan) + end + + it { is_expected.to eql(true) } + + end + + context "when roles are: creator, administrator" do + + before do + create(:role, :creator, :administrator, plan: plan) + end + + it { is_expected.to eql(false) } + + end + + context "when roles are: editor" do + + before do + create(:role, :editor, plan: plan) + end + + it { is_expected.to eql(true) } + + end + + context "when roles are: creator, editor" do + + before do + create(:role, :creator, :editor, plan: plan) + end + + it { is_expected.to eql(false) } + + end + + context "when roles are: administrator, editor" do + + before do + create(:role, :administrator, :editor, plan: plan) + end + + it { is_expected.to eql(true) } + + end + + context "when roles are: creator, editor, administrator" do + + before do + create(:role, :creator, :editor, :administrator, plan: plan) + end + + it { is_expected.to eql(false) } + + end + + context "when roles are: commenter" do + + before do + create(:role, :commenter, plan: plan) + end + + it { is_expected.to eql(true) } + + end + + context "when roles are: creator, commenter" do + + before do + create(:role, :creator, :commenter, plan: plan) + end + + it { is_expected.to eql(false) } + + end + + context "when roles are: administrator, commenter" do + + before do + create(:role, :administrator, :commenter, plan: plan) + end + + it { is_expected.to eql(true) } + + end + + context "when roles are: creator, administrator, commenter" do + + before do + create(:role, :creator, :administrator, :commenter, plan: plan) + end + + it { is_expected.to eql(false) } + + end + + context "when roles are: editor, commenter" do + + before do + create(:role, :editor, :commenter, plan: plan) + end + + it { is_expected.to eql(true) } + + end + + context "when roles are: creator, editor, commenter" do + + before do + create(:role, :creator, :editor, :commenter, plan: plan) + end + + it { is_expected.to eql(false) } + + end + + context "when roles are: administrator, editor, commenter" do + + before do + create(:role, :administrator, :editor, :commenter, plan: plan) + end + + it { is_expected.to eql(true) } + + end + + context "when roles are: creator, administrator, editor, commenter" do + + before do + create(:role, :creator, :administrator, :editor, :commenter, plan: plan) + end + + it { is_expected.to eql(false) } + + end + + context "when roles are: reviewer" do + + before do + create(:role, :reviewer, plan: plan) + end + + it { is_expected.to eql(true) } + + end + + context "when roles are: creator, reviewer" do + + before do + create(:role, :creator, :reviewer, plan: plan) + end + + it { is_expected.to eql(false) } + + end + + context "when roles are: administrator, reviewer" do + + before do + create(:role, :administrator, :reviewer, plan: plan) + end + + it { is_expected.to eql(true) } + + end + + context "when roles are: creator, administrator, reviewer" do + + before do + create(:role, :creator, :administrator, :reviewer, plan: plan) + end + + it { is_expected.to eql(false) } + + end + + context "when roles are: editor, reviewer" do + + before do + create(:role, :editor, :reviewer, plan: plan) + end + + it { is_expected.to eql(true) } + + end + + context "when roles are: creator, editor, reviewer" do + + before do + create(:role, :creator, :editor, :reviewer, plan: plan) + end + + it { is_expected.to eql(false) } + + end + + context "when roles are: administrator, editor, reviewer" do + + before do + create(:role, :administrator, :editor, :reviewer, plan: plan) + end + + it { is_expected.to eql(true) } + + end + + context "when roles are: creator, editor, administrator, reviewer" do + + before do + create(:role, :creator, :editor, :administrator, :reviewer, plan: plan) + end + + it { is_expected.to eql(false) } + + end + + context "when roles are: commenter, reviewer" do + + before do + create(:role, :commenter, :reviewer, plan: plan) + end + + it { is_expected.to eql(true) } + + end + + context "when roles are: creator, commenter, reviewer" do + + before do + create(:role, :creator, :commenter, :reviewer, plan: plan) + end + + it { is_expected.to eql(false) } + + end + + context "when roles are: administrator, commenter, reviewer" do + + before do + create(:role, :administrator, :commenter, :reviewer, plan: plan) + end + + it { is_expected.to eql(true) } + + end + + context "when roles are: creator, administrator, commenter, reviewer" do + + before do + create(:role, :creator, :administrator, :commenter, :reviewer, plan: plan) + end + + it { is_expected.to eql(false) } + + end + + context "when roles are: editor, commenter, reviewer" do + + before do + create(:role, :editor, :commenter, :reviewer, plan: plan) + end + + it { is_expected.to eql(true) } + + end + + context "when roles are: creator, editor, commenter, reviewer" do + + before do + create(:role, :creator, :editor, :commenter, :reviewer, plan: plan) + end + + it { is_expected.to eql(false) } + + end + + context "when roles are: administrator, editor, commenter, reviewer" do + + before do + create(:role, :administrator, :editor, :commenter, :reviewer, plan: plan) + end + + it { is_expected.to eql(true) } + + end + + context "when roles are: creator, administrator, editor, commenter, reviewer" do + + before do + create(:role, :creator, :administrator, :editor, :commenter, :reviewer, plan: plan) + end + + it { is_expected.to eql(false) } + + end + + end + + describe "#owner_and_coowners" do + + let!(:user) { create(:user) } + + let!(:plan) { create(:plan) } + + subject { plan.owner_and_coowners } + + context "when role is creator" do + + before do + create(:role, :creator, user: user, plan: plan) + end + + it { is_expected.to include(user) } + + end + + context "when role is administrator" do + + before do + create(:role, :administrator, user: user, plan: plan) + end + + it { is_expected.to include(user) } + + end + + context "when role is editor" do + + before do + create(:role, :editor, user: user, plan: plan) + end + + it { is_expected.not_to include(user) } + + end + + context "when role is commenter" do + + before do + create(:role, :commenter, user: user, plan: plan) + end + + it { is_expected.not_to include(user) } + + end + + context "when role is reviewer" do + + before do + create(:role, :reviewer, user: user, plan: plan) + end + + it { is_expected.not_to include(user) } + + end + + end + + describe "#num_answered_questions" do + + let!(:template) { create(:template) } + + let!(:plan) { create(:plan, template: template) } + + subject { plan.num_answered_questions } + + before do + @phase = create(:phase, template: template) + @section = create(:section, phase: @phase) + @questions = create_list(:question, 3, :textarea, section: @section) + # 2 valid answers + @questions.first(2).each do |question| + create(:answer, question: question, plan: plan) + end + # 1 valid answers + @questions.last(1).each do |question| + create(:answer, question: question, plan: plan, text: nil) + end + end + + it "returns the number of questions with valid answers" do + expect(subject).to eql(2) + end + + end + + describe "#num_questions" do + + let!(:template) { create(:template) } + + let!(:plan) { create(:plan, template: template) } + + subject { plan.num_questions } + + before do + create_list(:phase, 2, template: template) do |phase| + create_list(:section, 2, phase: phase) do |section| + create_list(:question, 3, section: section) + end + end + end + + it "returns the number of questions belonging to this plan's sections" do + expect(subject).to eql(12) + end + + end + + describe "#visibility_allowed?" do + + let!(:template) { create(:template) } + + let!(:plan) { create(:plan, template: template) } + + subject { plan.visibility_allowed? } + + before do + @phase = create(:phase, template: template) + @section = create(:section, phase: @phase) + @questions = create_list(:question, 4, :textarea, section: @section) + @questions.take(3).each do |question| + create(:answer, question: question, plan: plan) + end + end + + context "when requisite number of questions answered" do + + before do + Rails.application.config.default_plan_percentage_answered = 75 + end + + it { is_expected.to eql(true) } + + end + + context "when requisite number of questions not answered" do + + before do + Rails.application.config.default_plan_percentage_answered = 76 + end + + it { is_expected.to eql(false) } + end + + end + + describe "#question_exists?" do + + subject { plan.question_exists?(question.id) } + + context "when Question with ID and Plan exists" do + + let!(:question) { create(:question) } + + let!(:plan) { create(:plan, template: question.section.phase.template) } + + it { is_expected.to eql(true) } + + end + + context "when Question with ID and Plan don't exist" do + + let!(:question) { create(:question) } + + let!(:plan) { create(:plan) } + + it { is_expected.to eql(false) } + + end + + end + + describe "#no_questions_matches_no_answers?" do + + let!(:plan) { create(:plan) } + + subject { plan.no_questions_matches_no_answers? } + + context "when has no answers" do + + it { is_expected.to eql(true) } + + end + + context "when has answers that are not valid" do + + let!(:question) { create(:question, :textarea) } + + before do + create_list(:answer, 1, text: "", plan: plan, question: question) + end + + it { is_expected.to eql(true) } + + end + + context "when has answers that are valid" do + + let!(:question) { create(:question, :textarea) } + + before do + create_list(:answer, 1, plan: plan, question: question) + end + + it { is_expected.to eql(false) } + + end + end + +end diff --git a/spec/models/pref_spec.rb b/spec/models/pref_spec.rb new file mode 100644 index 0000000..c2ff583 --- /dev/null +++ b/spec/models/pref_spec.rb @@ -0,0 +1,31 @@ +require 'rails_helper' + +RSpec.describe Pref, type: :model do + + context "attributes" do + + it { is_expected.to serialize(:settings) } + + end + + context "associations" do + + it { is_expected.to belong_to(:user) } + + end + + context "validations" do + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:settings) } + end + + describe ".default_settings" do + + it "returns Rails configuration for preferences" do + expect(Pref.default_settings).to eql(Rails.configuration.branding[:preferences]) + expect(Pref.default_settings).not_to be_nil + end + + end + +end diff --git a/spec/models/question_format_spec.rb b/spec/models/question_format_spec.rb new file mode 100644 index 0000000..b28edde --- /dev/null +++ b/spec/models/question_format_spec.rb @@ -0,0 +1,101 @@ +require 'rails_helper' + +RSpec.describe QuestionFormat, type: :model do + + context "validations" do + + it { is_expected.to validate_presence_of(:title) } + + it { is_expected.to validate_uniqueness_of(:title) + .with_message("must be unique") } + + it { is_expected.to validate_presence_of(:description) } + + it { is_expected.to allow_values(true, false).for(:option_based) } + + it { is_expected.not_to allow_value(nil).for(:option_based) } + + it { is_expected.to allow_values(:textarea, :textfield, :radiobuttons, + :checkbox, :dropdown, :multiselectbox, + :date, :rda_metadata) + .for(:formattype) } + + end + + context "associations" do + + it { is_expected.to have_many(:questions) } + + end + + describe ".id_for" do + + let!(:format_type) { 'textarea' } + + subject { QuestionFormat.id_for(format_type) } + + context "when record doesn't exist" do + + xit "it returns nil" do + # TODO: This behaviour is fixed in the refactors branch + expect(subject).to be_nil + end + + end + + context "when record exists" do + + before do + @question_format = create(:question_format, formattype: "textarea") + end + + it "returns the ID for that record" do + expect(subject).to eql(@question_format.id) + end + + end + + end + + describe "#to_s" do + + let!(:question_format) { create(:question_format) } + + it "returns the title" do + expect(question_format.to_s).to eql(question_format.title) + end + + end + + describe "#option_based?" do + + subject { question_format.option_based? } + + context "when question_format option_based is true" do + + let!(:question_format) { create(:question_format, option_based: true) } + + it { is_expected.to eql(true) } + + end + + + context "when question_format option_based is true" do + + let!(:question_format) { create(:question_format, option_based: false) } + + it { is_expected.to eql(false) } + + end + + end + + describe "#formattype" do + + it "raises an exception when value not recognised" do + expect { subject.formattype = :foo }.to raise_error(ArgumentError) + end + + end + +end diff --git a/spec/models/question_option_spec.rb b/spec/models/question_option_spec.rb new file mode 100644 index 0000000..7cddd93 --- /dev/null +++ b/spec/models/question_option_spec.rb @@ -0,0 +1,89 @@ +require 'rails_helper' + +RSpec.describe QuestionOption, type: :model do + + context "validations" do + + it { is_expected.to validate_presence_of(:question) } + + it { is_expected.to validate_presence_of(:text) } + + it { is_expected.to validate_presence_of(:number) } + + it { is_expected.to allow_values(true, false).for(:is_default) } + + it { is_expected.not_to allow_value(nil).for(:is_default) } + + end + + context "associations" do + + it { is_expected.to belong_to(:question) } + + it { is_expected.to have_and_belong_to_many(:answers) + .join_table("answers_question_options") } + end + + describe ".by_number" do + + subject { QuestionOption.by_number } + + before do + @a = create(:question_option, number: 1) + @b = create(:question_option, number: 3) + @c = create(:question_option, number: 2) + end + + it "orders records by the number attribute" do + expect(subject.first).to eql(@a) + expect(subject.last).to eql(@b) + end + + end + + describe "#deep_copy" do + + let!(:options) { Hash.new } + + let!(:question_option) { create(:question_option, is_default: true) } + + subject { question_option.deep_copy(options) } + + context "when no options provided" do + + it "builds a new record" do + expect(subject).to be_new_record + end + + it "copies is_default from original" do + expect(subject.is_default).to eql(question_option.is_default) + end + + it "copies number from original" do + expect(subject.number).to eql(question_option.number) + end + + it "copies text from original" do + expect(subject.text).to eql(question_option.text) + end + + it "sets question_id to nil" do + expect(subject.question_id).to be_nil + end + + end + + context "when question_id option is present" do + + let!(:question) { create(:question) } + + let!(:options) { { question_id: question.id } } + + it "sets question_id to given option" do + expect(subject.question_id).to eql(question.id) + end + + end + end + +end diff --git a/spec/models/question_spec.rb b/spec/models/question_spec.rb new file mode 100644 index 0000000..eefd514 --- /dev/null +++ b/spec/models/question_spec.rb @@ -0,0 +1,355 @@ +require 'rails_helper' + +RSpec.describe Question, type: :model do + + context "validations" do + + it { is_expected.to validate_presence_of(:text) } + + it { is_expected.to validate_presence_of(:number) } + + it { is_expected.to validate_uniqueness_of(:number) + .scoped_to(:section_id) + .with_message("must be unique") } + + it { is_expected.to validate_presence_of(:section) } + + + it { is_expected.to validate_presence_of(:question_format) } + + it { is_expected.to allow_values(true, false).for(:option_comment_display) } + + it { is_expected.to allow_value(nil).for(:option_comment_display) } + + it { is_expected.to allow_values(true, false).for(:modifiable) } + + it { is_expected.to allow_value(nil).for(:modifiable) } + + end + + context "associations" do + + it { is_expected.to belong_to :section } + + it { is_expected.to belong_to :question_format } + + it { is_expected.to have_one :phase } + + it { is_expected.to have_one :template } + + it { is_expected.to have_many :answers } + + it { is_expected.to have_many :question_options } + + it { is_expected.to have_many :annotations } + + it { is_expected.to have_and_belong_to_many(:themes) + .join_table("questions_themes") } + + end + + + describe "#to_s" do + + before do + subject.text = 'foo bar' + end + + it "returns the Question's text" do + expect(subject.to_s).to eql("foo bar") + end + + end + + describe "#option_based?" do + + let!(:question) { create(:question, question_format: question_format) } + + subject { question_format.option_based? } + + context "when QuestionFormat is option_based" do + + let!(:question_format) { create(:question_format, option_based: true) } + + it { is_expected.to eql(true) } + + end + + context "when QuestionFormat is not option_based" do + + let!(:question_format) { create(:question_format, option_based: false) } + + it { is_expected.to eql(false) } + + end + end + + describe "#deep_copy" do + + let!(:question) { create(:question, { + default_value: "foo bar", + modifiable: true, + number: 12, + option_comment_display: false, + text: "How many foos can bar?", + }) } + + let!(:options) { Hash.new } + + subject { question.deep_copy(options) } + + context "when no options are provided" do + + it "doesn't persist the record" do + expect(subject).to be_new_record + end + + it "copies default_value from original Question" do + expect(subject.default_value).to eql("foo bar") + end + + it "copies modifiable from original Question" do + expect(subject.modifiable).to eql(true) + question.modifiable = false + expect(question.deep_copy.modifiable).to eql(false) + end + + it "copies number from original Question" do + expect(subject.number).to eql(12) + end + + it "copies option_comment_display from original Question" do + expect(subject.option_comment_display).to eql(false) + end + + it "copies text from original Question" do + expect(subject.text).to eql("How many foos can bar?") + end + + it "copies question_format_id from original Question" do + expect(subject.question_format_id).to eql(question.question_format_id) + end + + it "sets section_id to nil" do + expect(subject.section_id).to be_nil + end + + end + + context "when modifiable option provided" do + + let!(:options) { { modifiable: true } } + + it "copies modifiable from option" do + expect(subject.modifiable).to eql(true) + question.modifiable = false + expect(question.deep_copy.modifiable).to eql(false) + end + + it "ignores the original record's value" do + question.modifiable = false + expect(question.deep_copy(options).modifiable).to eql(true) + end + + end + + context "when section_id option provided" do + + let!(:section) { create(:section) } + + let!(:options) { { section_id: section.id } } + + it "sets the section_id attribute" do + expect(subject.section_id).to eql(section.id) + end + + end + + context "when save option provided" do + + let!(:options) { { save: true } } + + it "persists the record to the database" do + expect(subject).to be_persisted + end + end + end + + describe '#guidance_for_org' do + + pending "Ignore this for now. Should move method to view helpers" + + end + + describe "#example_answers" do + + subject { question.example_answers([org.id]) } + + let!(:question) { create(:question) } + + let!(:org) { create(:org) } + + context "when belongs to Org and type 'Example answer'" do + + let!(:annotation) do + create(:annotation, question: question, org: org, + type: Annotation.types[:example_answer]) + end + + it { is_expected.to include(annotation) } + + end + + context "when belongs to Org and type 'Guidance'" do + + let!(:annotation) do + create(:annotation, question: question, org: org, + type: Annotation.types[:guidance]) + end + + it { is_expected.not_to include(annotation) } + + end + + context "when belongs to other Org and type 'Example answer'" do + + let!(:annotation) do + create(:annotation, question: question, + type: Annotation.types[:guidance]) + end + + it { is_expected.not_to include(annotation) } + + end + + end + + describe "#guidance_annotation" do + + subject { question.guidance_annotation(org.id) } + + let!(:question) { create(:question) } + + let!(:org) { create(:org) } + + context "when Annotation type is 'guidance' and belongs to Org" do + + let!(:annotation) do + create(:annotation, + org: org, + question: question, + type: Annotation.types[:guidance]) + end + + it { is_expected.to eql(annotation) } + + end + + context "when Annotation type is 'Example Answer' and belongs to Org" do + + let!(:annotation) do + create(:annotation, + org: org, + question: question, + type: Annotation.types[:example_answer]) + end + + it { is_expected.to be_nil } + + end + + context "when Annotation type is 'guidance' and doesn't belong to Org" do + + let!(:annotation) do + create(:annotation, + question: question, + type: Annotation.types[:guidance]) + end + + it { is_expected.to be_nil } + + end + + context "when Annotation type 'Example Answer' and doesn't belong to Org" do + + let!(:annotation) do + create(:annotation, + question: question, + type: Annotation.types[:example_answer]) + end + + it { is_expected.to be_nil } + + end + + end + + describe "#annotations_per_org" do + + subject { question.annotations_per_org(org.id) } + + let!(:org) { create(:org) } + + let!(:question) { create(:question) } + + context "when example answer already present" do + + before do + create(:annotation, type: Annotation.types[:example_answer], + org: org, question: question) + end + + it "returns the existing annotation" do + expect(subject.first).to be_persisted + end + + it "returns example answer" do + expect(subject.first).to be_example_answer + end + + end + + context "when example answer not present" do + + it "returns the existing annotation" do + expect(subject.first).to be_new_record + end + + it "returns example answer" do + expect(subject.first).to be_example_answer + end + + end + + context "when guidance already present" do + + before do + create(:annotation, type: Annotation.types[:guidance], + org: org, question: question) + end + + it "returns the existing annotation" do + expect(subject.last).to be_persisted + end + + it "returns example answer" do + expect(subject.last).to be_guidance + end + + end + + context "when guidance not present" do + + it "returns the existing annotation" do + expect(subject.last).to be_new_record + end + + it "returns example answer" do + expect(subject.last).to be_guidance + end + + end + + end + +end diff --git a/spec/models/region_spec.rb b/spec/models/region_spec.rb new file mode 100644 index 0000000..b859782 --- /dev/null +++ b/spec/models/region_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +RSpec.describe Region, type: :model do + + context "validations" do + it { is_expected.to validate_presence_of(:abbreviation) } + + it { is_expected.to validate_uniqueness_of(:abbreviation) + .with_message("must be unique") } + + it { is_expected.to validate_presence_of(:description) } + + it { is_expected.to validate_presence_of(:name) } + end + + context "associations" do + + it { is_expected.to belong_to :super_region } + + it { is_expected.to have_many :sub_regions } + + end + +end diff --git a/spec/models/role_spec.rb b/spec/models/role_spec.rb new file mode 100644 index 0000000..05d5b07 --- /dev/null +++ b/spec/models/role_spec.rb @@ -0,0 +1,175 @@ +require 'rails_helper' + +RSpec.describe Role, type: :model do + + context "validations" do + it { is_expected.to validate_presence_of(:user) } + + it { is_expected.to validate_presence_of(:plan) } + + it { is_expected.to allow_values(true, false).for(:active) } + + it { is_expected.not_to allow_value(nil).for(:active) } + + it { is_expected.to validate_numericality_of(:access) + .only_integer + .is_greater_than(0) + .with_message("can't be less than zero") } + + end + + context "associations" do + + it { is_expected.to belong_to :user } + + it { is_expected.to belong_to :plan } + + end + + describe "#access_level" do + + subject { role.access_level } + + context "when Role is reviewer" do + + let!(:role) { build(:role, :reviewer) } + + it { is_expected.to eql(5) } + + end + + context "when Role is administrator" do + + let!(:role) { build(:role, :administrator) } + + it { is_expected.to eql(3) } + + end + + context "when Role is editor" do + + let!(:role) { build(:role, :editor) } + + it { is_expected.to eql(2) } + + end + + context "when Role is commenter" do + + let!(:role) { build(:role, :commenter) } + + it { is_expected.to eql(1) } + + end + + end + + describe "#access_level=" do + + let!(:role) { build(:role).tap { |r| r.access_level = 0} } + + subject { role } + + context "when value is 0" do + + before do + role.access_level = 0 + end + + it "sets commenter to false" do + expect(subject).not_to be_commenter + end + + it "sets editor to false" do + expect(subject).not_to be_editor + end + + it "sets administrator to false" do + expect(subject).not_to be_administrator + end + + end + + context "when value is 1" do + + before do + role.access_level = 1 + end + + it "sets commenter to true" do + expect(subject).to be_commenter + end + + it "sets editor to false" do + expect(subject).not_to be_editor + end + + it "sets administrator to false" do + expect(subject).not_to be_administrator + end + + end + + context "when value is 2" do + + before do + role.access_level = 2 + end + + it "sets commenter to true" do + expect(subject).to be_commenter + end + + it "sets editor to true" do + expect(subject).to be_editor + end + + it "sets administrator to false" do + expect(subject).not_to be_administrator + end + + end + + context "when value is 3" do + + before do + role.access_level = 3 + end + + it "sets commenter to true" do + expect(subject).to be_commenter + end + + it "sets editor to true" do + expect(subject).to be_editor + end + + it "sets administrator to true" do + expect(subject).to be_administrator + end + + end + + context "when value is 4" do + + before do + role.access_level = 4 + end + + it "sets commenter to true" do + expect(subject).to be_commenter + end + + it "sets editor to true" do + expect(subject).to be_editor + end + + it "sets administrator to true" do + expect(subject).to be_administrator + end + + end + + end + +end diff --git a/spec/models/section_spec.rb b/spec/models/section_spec.rb new file mode 100644 index 0000000..03b2655 --- /dev/null +++ b/spec/models/section_spec.rb @@ -0,0 +1,75 @@ +require 'rails_helper' + +RSpec.describe Section, type: :model do + + context "validations" do + it { is_expected.to validate_presence_of(:title) } + + it { is_expected.to validate_presence_of(:number) } + + it { is_expected.to validate_presence_of(:phase) } + + it { is_expected.to validate_uniqueness_of(:number) + .scoped_to(:phase_id) + .with_message("must be unique") } + + it { is_expected.to allow_values(true, false).for(:modifiable) } + + end + + context "associations" do + + it { is_expected.to belong_to :phase } + + it { is_expected.not_to belong_to :organisation } + + it { is_expected.to have_one :template } + + it { is_expected.to have_many :questions } + + end + + describe "#num_answered_questions" do + + let!(:phase) { create(:phase, template: template) } + + let!(:section) { create(:section, phase: phase) } + + subject { section.num_answered_questions(plan) } + + context "when plan is nil" do + + let!(:plan) { nil } + + let!(:template) { create(:template) } + + it { is_expected.to be_zero } + + end + + context "when plan is present" do + + let!(:plan) { create(:plan) } + + let!(:template) { plan.template } + + before do + question = create(:question, section: section) + create(:answer, question: question, plan: plan, text: '') + + question = create(:question, section: section) + create(:answer, question: question, plan: plan) + + question = create(:question, section: section) + create(:answer, question: question, plan: plan) + end + + it "is expected to return the number of valid answered questions" do + expect(subject).to eql(2) + end + + end + + end + +end diff --git a/spec/models/template_spec.rb b/spec/models/template_spec.rb new file mode 100644 index 0000000..59315bf --- /dev/null +++ b/spec/models/template_spec.rb @@ -0,0 +1,1398 @@ +require 'rails_helper' + +RSpec.describe Template, type: :model do + + context "validations" do + it { is_expected.to validate_presence_of(:title) } + + it { is_expected.to allow_values(true, false).for(:published) } + + # This is currently being set in the defaults before validation + # it { is_expected.not_to allow_value(nil).for(:published) } + + it { is_expected.to validate_presence_of(:org) } + + it { is_expected.to validate_presence_of(:locale) } + + it { is_expected.to allow_values(true, false).for(:is_default) } + + # This is currently being set in the defaults before validation + # it { is_expected.not_to allow_value(nil).for(:is_default) } + + # This is currently being set in the defaults before validation + # it { is_expected.to validate_presence_of(:version) } + + # This is currently being set in the defaults before validation + # it { is_expected.to validate_presence_of(:visibility) } + + # This is currently being set in the defaults before validation + # it { is_expected.to validate_presence_of(:family_id) } + + it { is_expected.to allow_values(true, false).for(:archived) } + + # This is currently being set in the defaults before validation + # it { is_expected.not_to allow_value(nil).for(:archived) } + end + + context "associations" do + + it { is_expected.to belong_to :org } + + it { is_expected.to have_many :plans } + + it { is_expected.to have_many :phases } + + it { is_expected.to have_many :sections } + + it { is_expected.to have_many :questions } + + it { is_expected.to have_many :annotations } + + end + + describe '.archived' do + + subject { Template.archived } + + context "when template is archived" do + + let!(:template) { create(:template, archived: true) } + + it { is_expected.to include(template) } + + end + + context "when template is archived" do + + let!(:template) { create(:template, archived: false) } + + it { is_expected.not_to include(template) } + + end + end + + describe '.unarchived' do + + subject { Template.unarchived } + + context "when template is archived" do + + let!(:template) { create(:template, archived: true) } + + it { is_expected.not_to include(template) } + + end + + context "when template is archived" do + + let!(:template) { create(:template, archived: false) } + + it { is_expected.to include(template) } + + end + end + + describe '.default' do + + subject { Template.default } + + context "when default published template exists" do + + + before do + @a = create(:template, :default, :published) + @b = create(:template, :default, :published) + end + + it "returns the latest record" do + expect(subject).to eql(@b) + end + + end + + context "when default template is not published" do + + before do + create(:template, :default, :unpublished) + end + + xit "returns nil" do + # TODO: This is not behaving as expected. Fixed in another branch... + is_expected.to be_nil + end + + end + + context "when none of the published templates are default" do + + before do + create(:template, :published, is_default: false) + end + + xit "returns nil" do + # TODO: This is not behaving as expected. Fixed in another branch... + is_expected.to be_nil + end + + end + + end + + describe ".latest_version" do + + let!(:family_id) { nil } + + subject { Template.latest_version(family_id) } + + it "returns an ActiveRecord::Relation" do + expect(subject).to be_a(ActiveRecord::Relation) + end + + context "when family_id is present" do + + let!(:family_id) { "1235" } + + let!(:template) do + create(:template, :unpublished, family_id: "1235", version: 12) + end + + before do + create(:template, :unpublished, family_id: "1235", version: 11) + create(:template, :unpublished, family_id: "9999", version: 13) + end + + it "filters results by family_id" do + expect(subject).to include(template) + end + + end + + context "when family_id is absent" do + + let!(:family_id) { nil } + + let!(:template) { create(:template, :unpublished, version: 12) } + + before do + create(:template, :unpublished, version: 11) + create(:template, :unpublished, version: 10) + end + + it "returns the " do + expect(subject).to include(template) + end + + end + + context "when template is archived" do + + let!(:family_id) { nil } + + let!(:template) { create(:template, :archived, :unpublished, version: 12) } + + before do + @a = create(:template, :unpublished, version: 11) + @b = create(:template, :unpublished, version: 10) + end + + it "excludes from the results" do + expect(subject).not_to include(template) + end + + end + + end + + describe ".published" do + + + subject { Template.published(family_id) } + + before do + @a = create(:template, :published, family_id: family_id, version: 1) + @b = create(:template, :published, version: 3) + @c = create(:template, :unpublished, family_id: family_id, version: 2) + @d = create(:template, :unpublished, version: 5) + end + + context "when family_id is present" do + + let!(:family_id) { "1234" } + + it "includes records with family id" do + expect(subject).to include(@a) + end + + it "excludes records without family id" do + expect(subject).not_to include(@b) + end + + it "excludes unpublished records" do + expect(subject).not_to include(@c) + expect(subject).not_to include(@d) + end + + end + + context "when family_id is absent" do + + let!(:family_id) { nil } + + it "includes all published records" do + expect(subject).to include(@a) + expect(subject).to include(@b) + end + + it "excludes all published records" do + expect(subject).not_to include(@c) + expect(subject).not_to include(@d) + end + end + end + + describe ".latest_version_per_family" do + xit "This should be moved to a private method" + end + + describe ".latest_customized_version_per_customised_of" do + xit "This should be moved to a private method" + end + + describe ".latest_customized_version" do + + let!(:original_template) { create(:template) } + + let!(:family_id) { original_template.family_id } + + let!(:org) { create(:org) } + + subject { Template.latest_customized_version(family_id, org.id) } + + context "when latest version present" do + + before do + create(:template, customization_of: family_id, org: org, version: 5) + create(:template, customization_of: family_id, org: org, version: 6) + end + + it "returns a single record" do + expect(subject).to be_one + end + + it "returns the correct version" do + expect(subject.last.version).to eql(6) + end + + end + + context "when latest version absent" do + + before do + create(:template, customization_of: "1234", org: org, version: 5) + create(:template, customization_of: "1234", org: org, version: 6) + end + + it "returns an empty set" do + expect(subject).to be_empty + end + + end + + context "when latest version belongs to different Org" do + + before do + create(:template, customization_of: family_id, version: 5) + create(:template, customization_of: family_id, version: 6) + end + + it "returns an empty set" do + expect(subject).to be_empty + end + + end + + context "when latest version is archived" do + + before do + create(:template, customization_of: family_id, org: org, version: 5) + create(:template, :archived, customization_of: family_id, org: org, version: 6) + end + + # TODO: Confirm: Is this the desired behaviour? + it "returns an empty set" do + expect(subject).to be_empty + end + + end + end + + describe ".latest_version_per_org" do + + context "when org_id is an Integer" do + + let!(:org) { create(:org) } + + subject { Template.latest_version_per_org(org.id) } + + before do + @a = create(:template, org: org, version: 1, family_id: 123) + @b = create(:template, org: org, version: 2, family_id: 123) + @c = create(:template, org: org, version: 2, family_id: 456) + @d = create(:template, org: org, version: 1, family_id: 456) + end + + it { is_expected.not_to include(@a) } + + it { is_expected.to include(@b) } + + it { is_expected.to include(@c) } + + it { is_expected.not_to include(@d) } + + end + + context "when org_id is an Array" do + + let!(:org_a) { create(:org) } + + let!(:org_b) { create(:org) } + + subject { Template.latest_version_per_org([org_a.id, org_b.id]) } + + before do + @a = create(:template, org: org_a, version: 1, family_id: 123) + @b = create(:template, org: org_a, version: 2, family_id: 123) + @c = create(:template, org: org_b, version: 2, family_id: 456) + @d = create(:template, org: org_b, version: 1, family_id: 456) + end + + it { is_expected.not_to include(@a) } + + it { is_expected.to include(@b) } + + it { is_expected.to include(@c) } + + it { is_expected.not_to include(@d) } + + end + + end + + describe ".latest_customized_version_per_org" do + + let!(:original_template) { create(:template) } + + let!(:family_id) { original_template.family_id } + + let!(:org) { create(:org) } + + subject { Template.latest_customized_version_per_org(org.id) } + + context "when latest version present" do + + before do + create(:template, customization_of: family_id, org: org, version: 5) + create(:template, customization_of: family_id, org: org, version: 6) + end + + it "returns a single record" do + expect(subject).to be_one + end + + it "returns the correct version" do + expect(subject.last.version).to eql(6) + end + + end + + context "when latest version absent" do + + before do + create(:template, customization_of: "1234", org: org, version: 5) + create(:template, customization_of: "1234", org: org, version: 6) + end + + it "returns an empty set" do + expect(subject).to be_empty + end + + end + + context "when latest version belongs to different Org" do + + before do + create(:template, customization_of: family_id, version: 5) + create(:template, customization_of: family_id, version: 6) + end + + it "returns an empty set" do + expect(subject).to be_empty + end + + end + + context "when latest version is archived" do + + before do + create(:template, customization_of: family_id, org: org, version: 5) + create(:template, :archived, customization_of: family_id, org: org, version: 6) + end + + # TODO: Confirm: Is this the desired behaviour? + it "returns an empty set" do + expect(subject).to be_empty + end + + end + end + + describe ".families" do + + context "when org_id is given" do + + let!(:org) { create(:org) } + + subject { Template.families([org.id]) } + + before do + @a = create(:template, customization_of: nil) + @b = create(:template, customization_of: nil, org: org) + @c = create(:template, :archived, customization_of: nil, org: org) + end + + it "excludes Templates belonging to other Orgs" do + expect(subject).not_to include(@a) + end + + it "includes Templates that are not customisations" do + expect(subject).to include(@b) + end + + it "excludes archived Templates" do + expect(subject).not_to include(@c) + end + + end + + context "when org_id is nil" do + + subject { Template.families(nil) } + + before do + @a = create(:template, customization_of: nil) + @b = create(:template, customization_of: "123") + @c = create(:template, :archived, customization_of: nil) + end + + it "includes Templates belonging to all Orgs" do + expect(subject).to include(@a) + end + + it "excludes Templates that are customizations" do + expect(subject).not_to include(@b) + end + + it "excludes archived Templates" do + expect(subject).not_to include(@c) + end + + end + + end + + describe ".latest_customizable" do + + # TODO: These are breaking because Template.default is returning an + # ActiveRecord::Relation instead of a Template record. Investigate. + + subject { Template.latest_customizable } + + context "when Org is an institution" do + + let!(:org) { create(:org, :institution) } + + let!(:template) { create(:template, org: org) } + + xit { is_expected.not_to include(template) } + + end + + context "when Org is a funder" do + + let!(:org) { create(:org, :funder) } + + let!(:template) { create(:template, org: org) } + + xit { is_expected.to include(template) } + + end + + context "when Org is an organisation" do + + let!(:org) { create(:org, :organisation) } + + let!(:template) { create(:template, org: org) } + + xit { is_expected.not_to include(template) } + + end + + context "when Org is a research_institute" do + + let!(:org) { create(:org, :research_institute) } + + let!(:template) { create(:template, org: org) } + + xit { is_expected.not_to include(template) } + + end + + context "when Org is a project" do + + let!(:org) { create(:org, :project) } + + let!(:template) { create(:template, org: org) } + + xit { is_expected.not_to include(template) } + + end + + context "when Org is a school" do + + let!(:org) { create(:org, :school) } + + let!(:template) { create(:template, org: org) } + + xit { is_expected.not_to include(template) } + + end + + context "when template is default and published" do + + let!(:template) { create(:template, :default, :published) } + + xit { is_expected.to include(template) } + + end + + context "when template is default and unpublished" do + + let!(:template) { create(:template, :default, :unpublished) } + + xit { is_expected.not_to include(template) } + + end + + end + + describe ".publicly_visible" do + + subject { Template.publicly_visible } + + before do + @a = create(:template, :archived, :publicly_visible) + @b = create(:template, :publicly_visible) + @c = create(:template, :organisationally_visible) + end + + it "excludes archived Templates" do + pending "This test needs refactorings in the model before it will pass" + # The enum is currently overwriting this scope + expect(subject).not_to include(@a) + end + + it "includes publicly_visible Templates" do + expect(subject).to include(@b) + end + + it "excludes organisationally_visible Templates" do + expect(subject).not_to include(@c) + end + + end + + describe ".organisationally_visible" do + + subject { Template.organisationally_visible } + + before do + @a = create(:template, :archived, :organisationally_visible) + @b = create(:template, :publicly_visible) + @c = create(:template, :organisationally_visible) + end + + it "excludes archived Templates" do + pending "This test needs refactorings in the model before it will pass" + # The enum is currently overwriting this scope + expect(subject).not_to include(@a) + end + + it "excludes publicly_visible Templates" do + expect(subject).not_to include(@b) + end + + it "includes organisationally_visible Templates" do + expect(subject).to include(@c) + end + + end + + describe ".search" do + + subject { Template.search("foo") } + + before do + @a = create(:template, :archived, title: "foo bar") + @b = create(:template, title: "foo bar") + @c = create(:template, description: "

foo bar

") + @d = create(:template, org: create(:org, name: "foo org")) + end + + it "excludes archived Templates" do + pending "This is broken—Template needs refactored" + expect(subject).not_to include(@a) + end + + it "includes Templates with a matching title" do + pending "This is broken—Template needs refactored" + expect(subject).to include(@b) + end + + it "excludes Templates with a matching description" do + pending "This is broken—Template needs refactored" + expect(subject).not_to include(@c) + end + + it "includes Templates with a matching Org name" do + pending "This is broken—Template needs refactored" + expect(subject).to include(@d) + end + end + + describe ".current" do + + subject { Template.current("1234") } + + before do + @a = create(:template, :archived, family_id: "1234", version: 5) + @b = create(:template, family_id: "5555", version: 2) + @c = create(:template, family_id: "1234", version: 2) + end + + it "excludes archived Templates" do + expect(subject).not_to eql(@a) + end + + it "excludes Templates with a different family_id" do + expect(subject).not_to eql(@b) + end + + it "orders results by DESC version" do + expect(subject).to eql(@c) + end + + end + + describe ".live" do + + + context "when family ID is an Array" do + + subject { Template.live([1234, 1235]) } + + before do + @a = create(:template, :archived, :published, family_id: 1234) + @b = create(:template, :published, family_id: 1234) + @c = create(:template, :unpublished, family_id: 1234) + @d = create(:template, :published, family_id: 1235) + end + + it "returns an enumerable" do + expect(subject).to be_a(ActiveRecord::Relation) + end + + it "excludes archived Templates" do + expect(subject).not_to include(@a) + end + + it "includes published Templates" do + expect(subject).to include(@b) + end + + it "excludes unpublished Templates" do + expect(subject).not_to include(@c) + end + + it "includes published Templates" do + expect(subject).to include(@d) + end + + end + + context "when family ID is an Integer" do + + subject { Template.live(1234) } + + before do + @a = create(:template, :archived, :published, family_id: 1234) + @b = create(:template, :published, family_id: 1234) + @c = create(:template, :unpublished, family_id: 1234) + @d = create(:template, :published, family_id: 1235) + end + + it "excludes archived Templates" do + expect(subject).not_to eql(@a) + end + + it "includes published Templates" do + expect(subject).to eql(@b) + end + + it "excludes unpublished Templates" do + expect(subject).not_to eql(@c) + end + + it "excludes published Templates with other family_id" do + expect(subject).not_to eql(@d) + end + end + + end + + describe ".find_or_generate_version!" do + + subject { Template.find_or_generate_version!(template) } + + context "when template is not latest?" do + + let!(:template) { create(:template) } + + before do + template.expects(:latest?).at_least(1).returns(false) + end + + it "raises an error" do + expect { subject }.to raise_error(StandardError) + end + + end + + context "when template is latest and generate_version? is true" do + + let!(:template) { create(:template, :published) } + + before do + template.expects(:latest?).at_least(1).returns(true) + end + + it "returns a different Template" do + expect(subject).not_to eql(template) + end + + it "creates a persisted Template" do + expect(subject).to be_persisted + end + + end + + context "when template is latest and generate_version? is false" do + + let!(:template) { create(:template, :unpublished) } + + before do + template.expects(:latest?).at_least(1).returns(true) + end + + it "returns the same Template" do + expect(subject).to eql(template) + end + + end + + end + + describe "#deep_copy" do + + context "when attributes is provided" do + + let!(:template) { create(:template, :published, phases: 2) } + + subject do + template.deep_copy(attributes: { title: "foo", description: "bar" }) + end + + it "updates title with the provided value" do + expect(subject.title).to eql("foo") + end + + it "updates describe with the provided value" do + expect(subject.description).to eql("bar") + end + + end + + context "when options save is true" do + + let!(:template) { create(:template, :published, phases: 2) } + + subject { template.deep_copy(attributes: { family_id: 123 }, save: true) } + + it "returns a persisted record" do + expect(subject).to be_persisted + end + + it "creates phases" do + expect(subject.phases).to be_many + end + + it "sets template_id on phases to the new template" do + new_temp = subject + expect(new_temp.phases.map(&:template_id).uniq).to eql([new_temp.id]) + end + + end + + context "when options save is false" do + + let!(:template) { create(:template, :published, phases: 2) } + + subject { template.deep_copy(attributes: { family_id: 123 }, save: false) } + + it "returns a new record" do + expect(subject).to be_new_record + end + + it "builds phases" do + expect(subject.phases).to be_many + end + + it "doesn't set template_id on phases" do + expect(subject.phases.map(&:template_id).compact).to be_empty + end + + end + + end + + describe "#base_org" do + + subject { template.base_org } + + context "when customization_of is present" do + + let!(:source_template) { create(:template) } + + let!(:template) do + create(:template, customization_of: source_template.family_id) + end + + it "returns the source Template org" do + expect(subject).to eql(source_template.org) + end + + end + + context "when customization_of is not present" do + + let!(:template) { create(:template, customization_of: nil) } + + it "returns the base Template org" do + expect(subject).to eql(template.org) + end + + end + + end + + describe "#latest?" do + + context "when Template is the latest in its family" do + + let!(:template) do + create(:template, :published, version: 5, family_id: 123) + end + + it "returns true" do + expect(template).to be_latest + end + + end + + + context "when Template is not the latest in its family" do + + let!(:template) do + create(:template, :published, version: 5, family_id: 123) + end + + before do + create(:template, :published, version: 6, family_id: 123) + end + + it "returns false" do + expect(template).not_to be_latest + end + + end + + end + + describe "#generate_version?" do + + let!(:template) { build(:template) } + + subject { template.generate_version? } + + context "when published is true" do + + before do + template.published = true + end + + it { is_expected.to eql(true) } + + end + + context "when published is false" do + + before do + template.published = false + end + + it { is_expected.to eql(false) } + + end + + end + + describe "#customize?" do + + subject { template.customize?(org) } + + context "when param is Org, org is funder, customization doesn't exist" do + + let!(:org) { create(:org, :funder) } + + let!(:template) { create(:template, org: org) } + + it { is_expected.to eql(true) } + + end + + context "when param is Org, template default, customization doesn't exist" do + + let!(:org) { create(:org) } + + let!(:template) { create(:template, :default, org: org) } + + it { is_expected.to eql(true) } + + end + + context "when param is Org, org is funder, customization exists" do + + let!(:org) { create(:org, :funder) } + + let!(:template) { create(:template, org: org) } + + before do + create(:template, customization_of: template.family_id, org: org) + end + + it { is_expected.to eql(false) } + + end + + context "when param is Org, template default, customization exists" do + + let!(:org) { create(:org) } + + let!(:template) { create(:template, :default, org: org) } + + before do + create(:template, customization_of: template.family_id, org: org) + end + + it { is_expected.to eql(false) } + + end + + context "when param not Org" do + + let!(:org) { stub(:banana) } + + let!(:template) { create(:template, org: create(:org, :funder)) } + + it { is_expected.to eql(false) } + + end + + end + + describe "#upgrade_customization?" do + + let!(:org) { create(:org, :funder) } + + context "when not a customization of another template" do + + let!(:template) do + create(:template, :published, customization_of: nil, org: org) + end + + it { expect(template).not_to be_upgrade_customization } + + end + + context "when customization of another template and source is newer" do + + let!(:source) do + create(:template, :published, + family_id: 123, org: org, created_at: 1.minutes.from_now) + end + + let!(:template) do + create(:template, :published, customization_of: source.family_id) + end + + it { expect(template).to be_upgrade_customization } + + end + + context "when customization of another template and source is older" do + + let!(:source) do + create(:template, :published, family_id: 123, org: org, created_at: 1.minutes.ago) + end + + let!(:template) do + create(:template, :published, customization_of: source.family_id) + end + + it { expect(template).not_to be_upgrade_customization } + + end + end + + describe "#draft?" do + + subject { template.draft? } + + context "when published and family has published template" do + + let!(:template) { create(:template, :published) } + + before do + create(:template, :published, family_id: template.family_id) + end + + it { is_expected.to eql(false) } + + end + + context "when published and family has no published template" do + + let!(:template) { create(:template, :published) } + + before do + create(:template, :unpublished, family_id: template.family_id) + end + + + it { is_expected.to eql(false) } + + end + + context "when unpublished and family has published template" do + + let!(:template) { create(:template, :unpublished) } + + before do + create(:template, :published, family_id: template.family_id) + end + + + it { is_expected.to eql(true) } + + end + + context "when unpublished and family has no published template" do + + let!(:template) { create(:template, :unpublished) } + + before do + create(:template, :unpublished, family_id: template.family_id) + end + + it { is_expected.to eql(false) } + + end + end + + describe "#removable?" do + + let!(:template) { create(:template) } + + context "when there are no Plans using this Template" do + + it { expect(template).to be_removable } + + end + + context "when there Plans using this Template" do + + before do + create(:plan, template: template) + end + + it { expect(template).not_to be_removable } + + end + + context "when there are Plans, but using different Templates" do + + before do + create(:plan) + end + + it { expect(template).to be_removable } + + end + + end + + describe "#generate_copy!" do + + subject { template.generate_copy!(org) } + + let!(:template) { create(:template) } + + context "when org is not an Org" do + + let!(:org) { stub(:banana) } + + it "raises a StandardError" do + expect { subject }.to raise_error(StandardError) + end + + end + + context "when org is an Org record" do + + let!(:org) { create(:org) } + + it "sets the version to zero" do + expect(subject.version).to be_zero + end + + it "sets published to false" do + expect(subject.published).to eql(false) + end + + it "sets family_id to a new integer" do + expect(subject.family_id).to be_a(Integer) + end + + it "sets org to new org" do + expect(subject.org).to eql(org) + end + + it "sets is_default to false" do + expect(subject.is_default).to eql(false) + end + + it "sets title to include 'Copy of'" do + expect(subject.title).to include('Copy of') + end + + it "sets title to include original title" do + expect(subject.title).to include(template.title) + end + + end + + end + + describe "#generate_version!" do + + subject { template.generate_version! } + + context "when template is not published" do + + let!(:template) { create(:template, :unpublished) } + + it "raises a StandardError" do + expect { subject }.to raise_error(StandardError) + end + + end + + context "when Template is published" do + + let!(:template) do + create(:template, :published, version: 4) + end + + it "sets the version to current version plus one" do + expect(subject.version).to eql(5) + end + + it "sets published to false" do + expect(subject.published).to eql(false) + end + + it "sets org to new org" do + expect(subject.org).to eql(template.org) + end + + end + + end + + describe "#customize!" do + + subject { template.customize!(org) } + + # Added an additional type to Org so that funder_only? fails + let!(:org) { create(:org, :funder, :organisation) } + + let!(:template) { create(:template, :default, org: org) } + + it "sets the version to 0" do + expect(subject.version).to be_zero + end + + it "sets the published to false" do + expect(subject.published).to eql(false) + end + + it "sets the family_id to a new Integer" do + expect(subject.family_id).not_to eql(template.family_id) + end + + it "sets customization of to Template's family_id" do + expect(subject.customization_of).to eql(template.family_id) + end + + it "sets org to Template's org" do + expect(subject.org).to eql(template.org) + end + + it "sets visibility to Organisationally visible" do + expect(subject.visibility).to eql("organisationally_visible") + end + + it "sets is_default to false" do + expect(subject.is_default).to eql(false) + end + + context "when org is not an Org" do + + let!(:org) { stubs(:org) } + + let!(:template) { create(:template) } + + it "raises an exception" do + expect { subject }.to raise_error(StandardError) + end + + end + + context "when org is not funder only" do + + let!(:org) { create(:org, :school) } + + let!(:template) { create(:template, org: org) } + + it "raises an exception" do + expect { subject }.to raise_error(StandardError) + end + + end + + end + + describe "#upgrade_customization!" do + + subject { template.upgrade_customization! } + + let!(:source) do + create(:template, :published, phases: 3) + end + + let!(:template) do + create(:template, :published, + version: 5, customization_of: source.family_id) + end + + it "returns a Template" do + expect(subject).to be_a(Template) + end + + it "returns a persisted Template" do + expect(subject).to be_persisted + end + + it "sets the version to template version plus one" do + expect(subject.version).to eql(6) + end + + it "sets the published to false" do + expect(subject.published).to eql(false) + end + + it "sets the family_id to template's family ID" do + expect(subject.family_id).to eql(template.family_id) + end + + it "sets the customization_of to template's customization_of" do + expect(subject.customization_of).to eql(template.customization_of) + end + + it "sets the org to Template's org" do + expect(subject.org).to eql(template.org) + end + + it "sets the visibility to Organisationally visible" do + expect(subject.visibility).to eql("organisationally_visible") + end + + it "sets is_default to false" do + expect(subject.is_default).to eql(false) + end + + it "sets phases modifiable to false" do + subject.phases.each do |phase| + expect(phase.modifiable).to eql(false) + end + end + + it "does something with the Phase" do + pending "Not really sure what's *supposed* to happen with the rest" + expect(true).to eql(false) + end + + context "when customization_of is blank" do + + let!(:template) { create(:template, customization_of: nil) } + + it "raises an error" do + expect { subject }.to raise_error(StandardError) + end + + end + + context "when source Template is not present" do + + let!(:template) do + create(:template, :published, customization_of: 456) + end + + it "raises an error" do + expect { subject }.to raise_error(StandardError) + end + + end + + end + +end diff --git a/spec/models/theme_spec.rb b/spec/models/theme_spec.rb new file mode 100644 index 0000000..f650352 --- /dev/null +++ b/spec/models/theme_spec.rb @@ -0,0 +1,64 @@ +require 'rails_helper' + +RSpec.describe Theme, type: :model do + + context "validations" do + + it { is_expected.to validate_presence_of(:title) } + + end + + context "associations" do + + it { is_expected.to have_and_belong_to_many(:questions) + .join_table("questions_themes") } + + it { is_expected.to have_and_belong_to_many(:guidances) + .join_table("themes_in_guidance") } + + end + + describe ".search" do + + let!(:term) { "foo" } + + subject { Theme.search(term) } + + context "when neither title or description matches term" do + + let!(:theme) { create(:theme) } + + it { is_expected.not_to include(theme) } + + end + + context "when title is a match for term" do + + let!(:theme) { create(:theme, title: "The title is foo bar") } + + it { is_expected.to include(theme) } + + end + + context "when description is a match for term" do + + let!(:theme) { create(:theme, description: "The title is foo bar") } + + it { is_expected.to include(theme) } + + end + + end + + describe "#to_s" do + + let!(:theme) { create(:theme) } + + subject { theme.to_s } + + it "returns the title" do + expect(subject).to eql(theme.title) + end + end + +end diff --git a/spec/models/token_permission_type_spec.rb b/spec/models/token_permission_type_spec.rb new file mode 100644 index 0000000..f5ee866 --- /dev/null +++ b/spec/models/token_permission_type_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' + +RSpec.describe TokenPermissionType, type: :model do + + context "validations" do + + it { is_expected.to validate_presence_of(:token_type) } + + end + + describe '#to_s' do + + let!(:token_permission_type) do + build(:token_permission_type, token_type: 'templates') + end + + subject { token_permission_type.to_s } + + it "returns the token_type attribute" do + expect(subject).to eql('templates') + end + + end + +end diff --git a/spec/models/user_identifier_spec.rb b/spec/models/user_identifier_spec.rb new file mode 100644 index 0000000..d82de6b --- /dev/null +++ b/spec/models/user_identifier_spec.rb @@ -0,0 +1,23 @@ +require 'rails_helper' + +RSpec.describe UserIdentifier, type: :model do + + context "validations" do + + it { is_expected.to validate_presence_of(:identifier) } + + it { is_expected.to validate_presence_of(:user) } + + it { is_expected.to validate_presence_of(:identifier_scheme) } + + end + + context "associations" do + + it { is_expected.to belong_to :user } + + it { is_expected.to belong_to :identifier_scheme } + + end + +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb new file mode 100644 index 0000000..0701587 --- /dev/null +++ b/spec/models/user_spec.rb @@ -0,0 +1,640 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe User, type: :model do + + context "validations" do + it { is_expected.to validate_presence_of(:email) } + + it "should validate that email addres is unqique" do + subject.email = "text-email@example.com" + is_expected.to validate_uniqueness_of(:email) + .case_insensitive + .with_message("has already been taken") + end + + it { + is_expected.to allow_values("one@example.com", "foo-bar@ed.ac.uk") + .for(:email) + } + + it { + is_expected.not_to allow_values("example.com", "foo bar@ed.ac.uk") + .for(:email) + } + + it { is_expected.to allow_values(true, false).for(:active) } + + it { is_expected.not_to allow_value(nil).for(:active) } + end + + + + context "associations" do + + it { is_expected.to have_and_belong_to_many(:perms) } + + it { is_expected.to belong_to(:language) } + + it { is_expected.to belong_to(:org) } + + it { is_expected.to have_one(:pref) } + + it { is_expected.to have_many(:answers) } + + it { is_expected.to have_many(:notes) } + + it { is_expected.to have_many(:exported_plans) } + + it { is_expected.to have_many(:roles).dependent(:destroy) } + + it { is_expected.to have_many(:plans).through(:roles) } + + it { is_expected.to have_many(:user_identifiers) } + + it { + is_expected.to have_many(:identifier_schemes).through(:user_identifiers) + } + + it { + is_expected.to have_and_belong_to_many(:notifications).dependent(:destroy) + } + + it { + is_expected.to have_and_belong_to_many(:notifications) + .join_table("notification_acknowledgements") + } + end + + describe "#active_for_authentication?" do + + let!(:user) { build(:user) } + + subject { user.active_for_authentication? } + + context "when user is active" do + before do + user.active = true + end + + it { is_expected.to eql(true) } + + end + + context "when user is not active" do + before do + user.active = false + end + + it { is_expected.to eql(false) } + + end + end + + describe "#get_locale" do + + let!(:user) { build(:user) } + + subject { user.get_locale } + + context "when user language present" do + + before do + @abbreviation = user.language.abbreviation + end + + it { is_expected.to eql(@abbreviation) } + + end + + context "when user language and org absent" do + + before do + user.language = nil + user.org = nil + end + + it { is_expected.to be_nil } + + end + + context "when user language absent and org present" do + + before do + user.language = nil + @locale = user.org.get_locale + end + + it { is_expected.to eql(@locale) } + + end + end + + describe "#name" do + + let!(:user) { build(:user) } + + subject { user.name } + + context "when user firstname and surname not blank and + use_email set to false" do + + subject { user.name(false) } + + before do + @name = "#{user.firstname} #{user.surname}".strip + end + + it { is_expected.to eql(@name) } + + end + + context "when user firstname is blank and surname is not blank and + use_email set to false" do + + subject { user.name(false) } + + before do + user.firstname = "" + @name = user.surname.to_s.strip + end + + it { is_expected.to eql(@name) } + + end + + context "when user firstname is blank and surname is not blank and + use_email set to false" do + + subject { user.name(false) } + + before do + user.surname = "" + @name = user.firstname.to_s.strip + end + + it { is_expected.to eql(@name) } + + end + + context "when user firstname is blank and surname is not blank + use_email set to true (by default)" do + + before do + user.surname = "" + @email = user.email + end + + it { is_expected.to eql(@email) } + + end + + context "when user firstname is not blank and surname is blank + use_email set to true (by default)" do + before do + user.firstname = "" + @email = user.email + end + + it { is_expected.to eql(@email) } + + end + + context "when user firstname is not blank and surname is blank + use_email set to true (by default)" do + before do + user.firstname = "" + @email = user.email + end + + it { is_expected.to eql(@email) } + + end + end + + describe "#identifier_for" do + + let!(:user) { create(:user) } + + let!(:identifier_scheme) { create(:identifier_scheme) } + + subject { user.identifier_for(identifier_scheme) } + + context "when user has an user_identifier present" do + + let!(:user_identifier) do + create(:user_identifier, identifier_scheme: identifier_scheme, + user: user) + end + + it { is_expected.to eql(user_identifier) } + + end + + context "when user has no user_identifier present" do + + let!(:user_identifier) { create(:user_identifier, user: user) } + + it { is_expected.not_to eql(user_identifier) } + + end + end + + describe "#can_super_admin?" do + + subject { user.can_super_admin? } + + context "when user includes Perm with name 'add_organisations'" do + + let!(:perms) { create_list(:perm, 1, name: "add_organisations") } + + let!(:user) { create(:user, perms: perms) } + + it { is_expected.to eq(true) } + + end + + context "when user includes Perm with name 'grant_api_to_orgs'" do + + let!(:perms) { create_list(:perm, 1, name: "grant_api_to_orgs") } + + let!(:user) { create(:user, perms: perms) } + + it { is_expected.to eq(true) } + + end + + context "when user includes Perm with name 'change_org_affiliation'" do + + let!(:perms) { create_list(:perm, 1, name: "change_org_affiliation") } + + let!(:user) { create(:user, perms: perms) } + + it { is_expected.to eq(true) } + + end + + end + + describe "#can_org_admin?" do + + subject { user.can_org_admin? } + + context "when user includes Perm with name 'grant_permissions'" do + + let!(:perms) { create_list(:perm, 1, name: "grant_permissions") } + let!(:user) { create(:user, perms: perms) } + + it { is_expected.to eq(true) } + + end + + context "when user includes Perm with name 'modify_guidance'" do + + let!(:perms) { create_list(:perm, 1, name: "modify_guidance") } + let!(:user) { create(:user, perms: perms) } + + it { is_expected.to eq(true) } + + end + + context "when user includes Perm with name 'modify_templates'" do + + let!(:perms) { create_list(:perm, 1, name: "modify_templates") } + let!(:user) { create(:user, perms: perms) } + + it { is_expected.to eq(true) } + + end + + context "when user includes Perm with name 'change_org_details'" do + + let!(:perms) { create_list(:perm, 1, name: "change_org_details") } + let!(:user) { create(:user, perms: perms) } + + it { is_expected.to eq(true) } + + end + + end + + describe "#can_add_orgs?" do + + let!(:perms) { create_list(:perm, 1, name: "add_organisations") } + + let!(:user) { create(:user, perms: perms) } + + subject { user.can_add_orgs? } + + it { is_expected.to eq(true) } + + end + + describe "#can_change_org?" do + + let!(:perms) { create_list(:perm, 1, name: "change_org_affiliation") } + + let!(:user) { create(:user, perms: perms) } + + subject { user.can_change_org? } + + it { is_expected.to eq(true) } + + end + + describe "#can_grant_permissions?" do + + let!(:perms) { create_list(:perm, 1, name: "grant_permissions") } + + let!(:user) { create(:user, perms: perms) } + + subject { user.can_grant_permissions? } + + it { is_expected.to eq(true) } + + end + + describe "#can_modify_templates?" do + + let!(:perms) { create_list(:perm, 1, name: "modify_templates") } + + let!(:user) { create(:user, perms: perms) } + + subject { user.can_modify_templates? } + + it { is_expected.to eq(true) } + + end + + describe "#can_modify_guidance?" do + + let!(:perms) { create_list(:perm, 1, name: "modify_guidance") } + + let!(:user) { create(:user, perms: perms) } + + subject { user.can_modify_guidance? } + + it { is_expected.to eq(true) } + + end + + describe "#can_use_api?" do + + let!(:perms) { create_list(:perm, 1, name: "use_api") } + + let!(:user) { create(:user, perms: perms) } + + subject { user.can_use_api? } + + it { is_expected.to eq(true) } + + end + + describe "#can_modify_org_details?" do + + let!(:perms) { create_list(:perm, 1, name: "change_org_details") } + + let!(:user) { create(:user, perms: perms) } + + subject { user.can_modify_org_details? } + + it { is_expected.to eq(true) } + + end + + describe "#can_grant_api_to_orgs?" do + + let!(:perms) { create_list(:perm, 1, name: "grant_api_to_orgs") } + + let!(:user) { create(:user, perms: perms) } + + subject { user.can_grant_api_to_orgs? } + + it { is_expected.to eq(true) } + + end + + describe "#remove_token!" do + + subject { user.remove_token! } + + context "when user is not a new record and api_token is not blank" do + + let!(:user) { create(:user, api_token: "an token string") } + + it { expect { subject }.to change { user.api_token }.to "" } + + end + + context "when user is not a new record and api_token is nil" do + + let!(:user) { create(:user, api_token: nil) } + + it { expect { subject }.not_to change { user.api_token } } + + end + + context "when user is not a new record and api_token is an empty string" do + + let!(:user) { create(:user, api_token: "") } + + it { expect { subject }.not_to change { user.api_token } } + + end + + context "when user is a new record" do + + let!(:user) { build(:user, api_token: "an token string") } + + it { expect { subject }.not_to change { user.api_token } } + + end + end + + describe "#keep_or_generate_token!" do + + subject { user.keep_or_generate_token! } + + context "when user is not a new record and api_token is an empty string" do + + let!(:user) { create(:user, api_token: "") } + + it { expect { subject }.to change { user.api_token } } + + end + + context "when user is not a new record and api_token is nil" do + + let!(:user) { create(:user, api_token: "") } + + it { expect { subject }.to change { user.api_token } } + + end + + context "when user is a new record and api_token is an empty string" do + + let!(:user) { build(:user, api_token: "") } + + it { expect { subject }.not_to change { user.new_record? } } + + end + end + + describe ".from_omniauth" do + + let!(:user) { create(:user) } + + let!(:auth) { stub(provider: "auth-provider", uid: "1234abcd") } + + subject { User.from_omniauth(auth) } + + + context "when User has UserIdentifier, with different ID" do + + let!(:identifier_scheme) do + create(:identifier_scheme, name: "auth-provider") + end + + let!(:user_identifier) do + create(:user_identifier, user: user, + identifier_scheme: identifier_scheme, + identifier: "another-auth-uid") + end + + it { is_expected.to be_nil } + + end + + context "when user Identifier and auth Provider are the same string" do + + let!(:identifier_scheme) do + create(:identifier_scheme, name: "auth-provider") + end + + let!(:user_identifier) do + create(:user_identifier, user: user, + identifier_scheme: identifier_scheme, + identifier: "1234abcd") + end + + it { is_expected.to eql(user) } + + end + + end + + describe "#get_preferences" do + + let!(:user) { create(:user) } + + let!(:key) { :email } + + subject { user.get_preferences(key) } + + context "when the User doesn't have thier own Pref" do + + it "returns the default value" do + Pref.expects(:default_settings) + .returns(email: { foo: { "bar" => "baz" } }) + expect(subject).to eql({ foo: { "bar" => "baz" } }) + end + + end + + context "when the User has thier own Pref" do + + before do + create(:pref, user: user, + settings: { email: { foo: { bar: "bam" } } }) + end + + it "returns the User's value" do + Pref.expects(:default_settings) + .returns(email: { foo: { bar: "baz" } }) + expect(subject).to eql({ foo: { bar: "bam" } }) + end + + end + + context "when the User's own Pref doesn't contain a new default" do + + before do + create(:pref, user: user, + settings: { email: { foo: { bar: "bam" } } }) + end + + it "includes the default" do + Pref.expects(:default_settings) + .returns(email: { default: { val: true }, foo: { bar: "baz" } }) + expect(subject).to eql({ default: { val: true }, foo: { bar: "bam" } }) + end + + end + + end + + describe ".where_case_insensitive" do + + before do + @user = create(:user, firstname: "Test") + end + + subject { User.where_case_insensitive(:firstname, value) } + + context "when search value is capitalized" do + + let!(:value) { "TEST" } + + it { is_expected.to include(@user) } + + end + + context "when search value matches case" do + + let!(:value) { "Test" } + + it { is_expected.to include(@user) } + + end + + + context "when search value is lowercase" do + + let!(:value) { "test" } + + it { is_expected.to include(@user) } + + end + end + + describe "#acknowledge" do + + let!(:user) { create(:user) } + + subject { user.acknowledge(notification) } + + context "when notification is dismissable" do + + let!(:notification) { create(:notification, :dismissable) } + + it "adds the Notification to the User's notifications" do + subject + expect(user.notifications).to include(notification) + end + + end + + context "when notification is not dismissable" do + + let!(:notification) { create(:notification) } + + it "doesn't add the Notification to the User's notifications" do + subject + expect(user.notifications).not_to include(notification) + end + + end + end + +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 0000000..27d91f3 --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,55 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +require 'spec_helper' +ENV['RAILS_ENV'] ||= 'test' +require File.expand_path('../../config/environment', __FILE__) +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +require 'rspec/rails' +require 'capybara-screenshot/rspec' + +# Add additional requires below this line. Rails is not loaded until this point! +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } + +# Checks for pending migrations and applies them before tests are run. +# If you are not using ActiveRecord, you can remove this line. +ActiveRecord::Migration.maintain_test_schema! + +RSpec.configure do |config| + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = false + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, :type => :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://relishapp.com/rspec/rspec-rails/docs + config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..d6ac64d --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,99 @@ +require 'simplecov' +SimpleCov.start 'rails' + +require 'mocha' + +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + + config.example_status_persistence_file_path = "./tmp/rspec" + + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :mocha + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + config.disable_monkey_patching! + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb new file mode 100644 index 0000000..586e03c --- /dev/null +++ b/spec/support/capybara.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require_relative "helpers/capybara_helper" +require_relative "helpers/sessions_helper" +require_relative "helpers/tiny_mce_helper" + +SCREEN_SIZE = [2400, 1350] +DIMENSION = Selenium::WebDriver::Dimension.new(*SCREEN_SIZE) + +Capybara.default_driver = :rack_test + +# This is a customisation of the default :selenium_chrome_headless config in: +# https://github.com/teamcapybara/capybara/blob/master/lib/capybara.rb +# +# This adds the --no-sandbox flag to fix TravisCI as described here: +# https://docs.travis-ci.com/user/chrome#sandboxing +Capybara.register_driver :selenium_chrome_headless do |app| + Capybara::Selenium::Driver.load_selenium + browser_options = ::Selenium::WebDriver::Chrome::Options.new + browser_options.args << '--headless' + browser_options.args << '--no-sandbox' + browser_options.args << '--disable-gpu' if Gem.win_platform? + Capybara::Selenium::Driver.new(app, browser: :chrome, options: browser_options) +end + +RSpec.configure do |config| + + config.before(:each, type: :feature, js: false) do + Capybara.use_default_driver + end + + config.before(:each, type: :feature, js: true) do + Capybara.current_driver = :selenium_chrome_headless + Capybara.page.driver.browser.manage.window.size = DIMENSION + end + +end + +Capybara.configure do |config| + config.default_max_wait_time = 5 # seconds + config.server = :webrick + config.raise_server_errors = true +end + +RSpec.configure do |config| + config.include(CapybaraHelper, type: :feature) + config.include(SessionsHelper, type: :feature) + config.include(TinyMceHelper, type: :feature) +end diff --git a/spec/support/database_cleaner.rb b/spec/support/database_cleaner.rb new file mode 100644 index 0000000..32fa0f5 --- /dev/null +++ b/spec/support/database_cleaner.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require "database_cleaner" + +RSpec.configure do |config| + + config.before(:suite) do + if config.use_transactional_fixtures? + raise(<<~TEXT) + Delete line `config.use_transactional_fixtures = true` from rails_helper.rb + (or set it to false) to prevent uncommitted transactions being used in + JavaScript-dependent specs. + + During testing, the app-under-test that the browser driver connects to + uses a different database connection to the database connection used by + the spec. The app's database connection would not be able to access + uncommitted transaction data setup over the spec's database connection. + TEXT + end + DatabaseCleaner.clean_with(:truncation) + end + + config.before(:each) do + DatabaseCleaner.strategy = :transaction + end + + config.before(:each, type: :feature) do + # :rack_test driver's Rack app under test shares database connection + # with the specs, so continue to use transaction strategy for speed. + driver_shares_db_conn_with_specs = Capybara.current_driver == :rack_test + + unless driver_shares_db_conn_with_specs + # Driver is probably for an external browser with an app + # under test that does *not* share a database connection with the + # specs, so use truncation strategy. + DatabaseCleaner.strategy = :truncation + end + end + + config.before(:each) do + DatabaseCleaner.start + end + + config.append_after(:each) do + DatabaseCleaner.clean + end + +end diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb new file mode 100644 index 0000000..ce5b54a --- /dev/null +++ b/spec/support/factory_bot.rb @@ -0,0 +1,8 @@ +RSpec.configure do |config| + + config.include(FactoryBot::Syntax::Methods) + + config.before(:suite) do + # FactoryBot.lint + end +end diff --git a/spec/support/faker.rb b/spec/support/faker.rb new file mode 100644 index 0000000..65ff4f9 --- /dev/null +++ b/spec/support/faker.rb @@ -0,0 +1,11 @@ +require 'faker' + +LOCALE = 'en_GB' + +RSpec.configure do |config| + config.before(:each) do + I18n.locale = LOCALE + Faker::Config.locale = LOCALE + FastGettext.default_locale = LOCALE + end +end diff --git a/spec/support/helpers/capybara_helper.rb b/spec/support/helpers/capybara_helper.rb new file mode 100644 index 0000000..f4bf602 --- /dev/null +++ b/spec/support/helpers/capybara_helper.rb @@ -0,0 +1,14 @@ +module CapybaraHelper + def clear_cookies! + browser = Capybara.current_session.driver.browser + if browser.respond_to?(:clear_cookies) + # Rack::MockSession + browser.clear_cookies + elsif browser.respond_to?(:manage) and browser.manage.respond_to?(:delete_all_cookies) + # Selenium::WebDriver + browser.manage.delete_all_cookies + else + raise "Don't know how to clear cookies. Weird driver?" + end + end +end \ No newline at end of file diff --git a/spec/support/helpers/sessions_helper.rb b/spec/support/helpers/sessions_helper.rb new file mode 100644 index 0000000..69fa3b2 --- /dev/null +++ b/spec/support/helpers/sessions_helper.rb @@ -0,0 +1,24 @@ +module SessionsHelper + + def sign_in(user = :user) + case user + when User + sign_in_as_user(user) + when Symbol + sign_in_as_user(create(:user)) + else + raise ArgumentError, "Invalid argument user: #{user}" + end + end + + def sign_in_as_user(user) + clear_cookies! + visit root_path + within "#sign-in-form" do + fill_in "Email", with: user.email + fill_in "Password", with: user.password + click_button "Sign in" + end + end + +end diff --git a/spec/support/helpers/tiny_mce_helper.rb b/spec/support/helpers/tiny_mce_helper.rb new file mode 100644 index 0000000..f6988d3 --- /dev/null +++ b/spec/support/helpers/tiny_mce_helper.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# This module provides helper methods for testing TinyMCE within feature specs +module TinyMceHelper + + ## + # Fill in TinyMCE field with given text. + # + # id - String with the id attribute of the editor instance (without the #) + # val - String with the value to input to the text field + # + # Returns String + def tinymce_fill_in(id, val) + # wait until the TinyMCE editor instance is ready. + # This is required for cases where the editor is loaded via XHR. + sleep 0.2 until page.evaluate_script("tinyMCE.get('#{id}') !== null") + + page.execute_script <<~JS + tinyMCE.get('#{id}').setContent('#{val}') + JS + end + +end \ No newline at end of file diff --git a/spec/support/shoulda.rb b/spec/support/shoulda.rb new file mode 100644 index 0000000..afa6498 --- /dev/null +++ b/spec/support/shoulda.rb @@ -0,0 +1,6 @@ +require 'shoulda/matchers' + +RSpec.configure do |config| + config.include(Shoulda::Matchers::ActiveModel, type: :model) + config.include(Shoulda::Matchers::ActiveRecord, type: :model) +end diff --git a/spec/support/wait_for_ajax.rb b/spec/support/wait_for_ajax.rb new file mode 100644 index 0000000..45890fd --- /dev/null +++ b/spec/support/wait_for_ajax.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module WaitForAjax + def wait_for_ajax + Timeout.timeout(Capybara.default_max_wait_time) do + loop until finished_all_ajax_requests? + end + end + + def finished_all_ajax_requests? + page.evaluate_script('jQuery.active').zero? + end +end + +RSpec.configure do |config| + config.include(WaitForAjax, type: :system) +end diff --git a/test/api/guidance_group_api_test.rb b/test/api/guidance_group_api_test.rb deleted file mode 100644 index 0e37f15..0000000 --- a/test/api/guidance_group_api_test.rb +++ /dev/null @@ -1,73 +0,0 @@ -require "test_helper" - -# describe "GuidanceGroupsController" do -# describe "GET :index" do -# before do -# get :index -# end - -# it "renders items/index" do -# must_render_template "items/index" -# end - -# it "responds with success" do -# must_respond_with :success -# end -# end -# end -# - - -class GuidanceGroupsTest < ActionDispatch::IntegrationTest #ActiveSupport::TestCase - setup do - @guidance_group = guidance_groups(:one) - end - -=begin - test "should get index" do - get :index - assert_response :success - end - - test "should show a Guidance Group" do - get :show, id: @guidance_group - assert_response :success - end -=end - - # BASIC AUTH - # should not respond to incorrect api_tokens - # - # should not respond to correct api_tokens with incorrect permissions - # i.e. their permissions for token include "guidance" - # - # INDEX - # should not respond with non-viewable guidance groups for a user - # - # should respond with all viewable guidance groups for a user - # - # SHOW - # should not respond with non-viewable guidance group for a user - # - # should respond wiht viewable guidance_group for a user - # - # BASIC VIEWS - # should respond with json - # - # should respond with the correct template (index/show.jbuilder) - # - # should not respond to post - # - # should not respond to put - # - # should not respond to delete - # - # WHAT IT MEANS TO BE VIEABLE - # -belongs to the dcc - # -belongs to any funder - # -belongs to an organisation, of which the user is a member - # -TODO: strictly define classes of organisation types, what are proj/institution/reaserch? - # - - -end diff --git a/test/assets/logo.jpg b/test/assets/logo.jpg deleted file mode 100644 index b45a9a3..0000000 --- a/test/assets/logo.jpg +++ /dev/null Binary files differ diff --git a/test/assets/logo_100x100.jpg b/test/assets/logo_100x100.jpg deleted file mode 100644 index 71e3cda..0000000 --- a/test/assets/logo_100x100.jpg +++ /dev/null Binary files differ diff --git a/test/assets/logo_300x300.jpg b/test/assets/logo_300x300.jpg deleted file mode 100644 index 241fb0e..0000000 --- a/test/assets/logo_300x300.jpg +++ /dev/null Binary files differ diff --git a/test/configuration_test.rb b/test/configuration_test.rb deleted file mode 100644 index 05c9622..0000000 --- a/test/configuration_test.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'test_helper' - -class ConfigurationTest < ActionDispatch::IntegrationTest - - # -------------------------------------------------------------------- - test "Make sure that all of the example YAML files have been setup properly" do - # Check for YAML configs - ['database.yml', 'branding.yml', 'secrets.yml'].each do |yml| - assert File.exist?("./config/#{yml}"), "Was expecting to find ./config/#{yml}" - end - - # Check for initializers - ['contact_us.rb', 'devise.rb', 'recaptcha.rb', 'wicked_pdf.rb'].each do |rb| - assert File.exist?("./config/initializers/#{rb}"), "Was expecting to find ./config/initializers/#{rb}" - end - end - - # -------------------------------------------------------------------- - test "Make sure that the config/branding.yml contains the managing Org's info" do - abbr = Rails.configuration.branding[:organisation][:abbreviation] - assert_not abbr.nil?, "expected the config/branding.yml to define the managing Org's abbreviation in organisation.abbreviation!" - assert_not Org.find_by(abbreviation: abbr).nil?, "Was expecting the organisation.abbreviation listed in config/branding.yml, '#{abbr}', to match the one from db/seeds.rb, '#{Org.first.abbreviation}'" - end -end \ No newline at end of file diff --git a/test/functional/.gitkeep b/test/functional/.gitkeep deleted file mode 100644 index e69de29..0000000 --- a/test/functional/.gitkeep +++ /dev/null diff --git a/test/functional/answers_controller_test.rb b/test/functional/answers_controller_test.rb deleted file mode 100644 index 6b6a91f..0000000 --- a/test/functional/answers_controller_test.rb +++ /dev/null @@ -1,47 +0,0 @@ -require 'test_helper' - -class AnswersControllerTest < ActionDispatch::IntegrationTest - - include Devise::Test::IntegrationHelpers - - setup do - @user = User.last - scaffold_plan - end - - # POST /answers/create_or_update - test "should be able to create an answer" do - sign_in @user - - # Test an answer for each Querstion Format - QuestionFormat.all.each do |format| - question = Question.find_by(question_format: format) - template = question.section.phase.template - - plan = Plan.create(title: "Testing Answer For #{format.title}", - template: template, visibility: :is_test) - - Role.create!(user_id: @user.id, plan_id: plan.id, access: 4) - - form_attributes = { - answer: { - plan_id: plan.id, - question_id: question.id, - text: "Tested", - lock_version: 0 } - } - - post_create_or_update_answer(form_attributes) - answer = Answer.find_by(plan: plan, question: question) - assert_not answer.id.nil?, "expected the answer to have been updated and for an id to be present after creating a #{format.title} question!" - assert_equal "Tested", answer.text, "expected the text to have been updated for a #{format.title} question!" - end - end - - private - def post_create_or_update_answer(attributes) - post create_or_update_answers_path(params: attributes) - assert_response :success - assert_equal "application/json", @response.content_type - end -end diff --git a/test/functional/application_controller_test.rb b/test/functional/application_controller_test.rb deleted file mode 100644 index 5af6367..0000000 --- a/test/functional/application_controller_test.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'test_helper' - -class ApplicationControllerTest < ActionDispatch::IntegrationTest - include Devise::Test::IntegrationHelpers - - setup do - @user = User.first - - stub_blog_calls - - scaffold_plan - end - - # In order to test methods on the application controller, we must call routes - # on controllers that extend the ApplicationController class. - - # ---------------------------------------------------------------- - test "make sure unauthorized users are redirected to the root path" do - plan = Plan.first - get plan_path(plan) - - assert_redirected_to "#{root_path}" - end - - # ---------------------------------------------------------------- - test "a user's language specification gets picked up and set in the session" do - if LANGUAGES.count > 1 - @user.language = LANGUAGES.last - @user.save! - - sign_in @user - - get root_path - - # TODO: Setting the User's language doesn't seem to update the locale in this context but it probably should! - #assert_equal @user.language.abbreviation, FastGettext.locale, "Expected the locale to have been set to the user's chosen language" - end - end - - # ---------------------------------------------------------------- - test "a user's org language specification gets picked up and used if the user has no language setting" do - if LANGUAGES.count > 1 - @user.language = nil - - @user.org[:language_id] = LANGUAGES.last.id - @user.save! - - sign_in @user - - get root_path - org_lang = Language.find(@user.org[:language_id]) - # TODO: Setting the Org's language doesn't seem to update the locale in this context but it probably should! - #assert_equal org_lang.abbreviation, FastGettext.locale, "Expected the locale to have been set to the org's chosen language" - end - end - - # ---------------------------------------------------------------- - test "the last visited url is stored in the session" do - get root_path - assert_equal root_path, session[:previous_url] - - sign_in @user - get plans_path - assert_equal plans_path, session[:previous_url] - end - -end diff --git a/test/functional/guidance_groups_controller_test.rb b/test/functional/guidance_groups_controller_test.rb deleted file mode 100644 index fc49af0..0000000 --- a/test/functional/guidance_groups_controller_test.rb +++ /dev/null @@ -1,164 +0,0 @@ -require 'test_helper' - -class GuidanceGroupsControllerTest < ActionDispatch::IntegrationTest - - include Devise::Test::IntegrationHelpers - - # TODO: The following methods SHOULD replace the old 'admin_' prefixed methods. The routes file already has - # these defined. They are defined multiple times though and we need to clean this up! In particular - # look at the unnamed routes after 'new_plan_phase' below. They are not named because they are duplicates. - # We should just have: - # - # SHOULD BE: - # -------------------------------------------------- - # guidance_groups GET /guidance_groups guidance_groups#index - # POST /guidance_groups guidance_groups#create - # guidance_group GET /guidance_group/:id guidance_groups#show - # PATCH /guidance_groups/:id guidance_groups#update - # PUT /guidance_groups/:id guidance_groups#update - # DELETE /guidance_groups/:id guidance_groups#destroy - # - # CURRENT RESULTS OF `rake routes` - # -------------------------------------------------- - # admin_show_guidance_group GET /org/admin/guidancegroup/:id/admin_show guidance_groups#admin_show - # admin_new_guidance_group GET /org/admin/guidancegroup/:id/admin_new guidance_groups#admin_new - # admin_edit_guidance_group GET /org/admin/guidancegroup/:id/admin_edit guidance_groups#admin_edit - # admin_destroy_guidance_group DELETE /org/admin/guidancegroup/:id/admin_destroy guidance_groups#admin_destroy - # admin_create_guidance_group POST /org/admin/guidancegroup/:id/admin_create guidance_groups#admin_create - # admin_update_guidance_group PUT /org/admin/guidancegroup/:id/admin_update guidance_groups#admin_update - - setup do - scaffold_org_admin(GuidanceGroup.first.org) - end - - # GET /org/admin/guidancegroup/:id/admin_new (admin_new_guidance_group_path) - # ---------------------------------------------------------- - test 'load the new guidance_group page' do - # Should redirect user to the root path if they are not logged in! - # TODO: Why is there an id here!? its a new guidance_group! - get admin_new_guidance_group_path(@user.org) - assert_unauthorized_redirect_to_root_path - - sign_in @user - - get admin_new_guidance_group_path(@user.org) - assert_response :success - end - - # POST /org/admin/guidancegroup/:id/admin_create (admin_create_guidance_group_path) - # ---------------------------------------------------------- - test 'create a new guidance_group' do - params = {org_id: @user.org.id, published: false, name: 'Testing create'} - - # Should redirect user to the root path if they are not logged in! - post admin_create_guidance_group_path(@user.org), {guidance_group: params} - assert_unauthorized_redirect_to_root_path - - sign_in @user - - post admin_create_guidance_group_path(@user.org), {guidance_group: params} - assert_response :redirect - assert_redirected_to admin_index_guidance_path(@user.org) - assert flash[:notice].start_with?('Successfully') && flash[:notice].include?('created') - assert assigns(:guidance_group) - assert_equal 'Testing create', GuidanceGroup.last.name, "expected the record to have been created!" - - # Invalid object - post admin_create_guidance_group_path(@user.org), {guidance_group: {name: nil}} - assert flash[:alert].start_with?(_('Could not create your')) - assert_response :success - assert assigns(:guidance_group) - end - - # GET /org/admin/guidancegroup/:id/admin_edit (admin_edit_guidance_group_path) - # ---------------------------------------------------------- - test 'load the edit guidance_group page' do - # Should redirect user to the root path if they are not logged in! - get admin_edit_guidance_group_path(GuidanceGroup.find_by(org: @user.org)) - assert_unauthorized_redirect_to_root_path - - sign_in @user - - get admin_edit_guidance_group_path(GuidanceGroup.find_by(org: @user.org)) - assert_response :success - end - - # PUT /org/admin/templates/:id/admin_template (admin_update_guidance_group_path) - # ---------------------------------------------------------- - test 'update the guidance_group' do - params = {name: 'Testing UPDATE'} - - # Should redirect user to the root path if they are not logged in! - put admin_update_guidance_group_path(GuidanceGroup.first), {guidance_group: params} - assert_unauthorized_redirect_to_root_path - - sign_in @user - - put admin_update_guidance_group_path(GuidanceGroup.first), {guidance_group: params} - assert flash[:notice].start_with?('Successfully') && flash[:notice].include?('saved') - assert_response :redirect - assert_redirected_to "#{admin_index_guidance_path(@user.org)}?name=Testing+UPDATE" - assert assigns(:guidance_group) - assert_equal 'Testing UPDATE', GuidanceGroup.first.name, "expected the record to have been updated" - - # Invalid object - put admin_update_guidance_group_path(GuidanceGroup.first), {guidance_group: {name: nil}} - assert flash[:alert].starts_with?(_('Could not update your')) - assert_response :success - assert assigns(:guidance_group) - end - - # PUT /org/admin/guidancegroup/:id/admin_update_publish (admin_update_publish_guidance_group) - test 'publish the guidance' do - @guidance_group = GuidanceGroup.first - - # Should redirect user to the root path if they are not logged in! - put admin_update_publish_guidance_group_path(@guidance_group) - assert_unauthorized_redirect_to_root_path - - sign_in @user - - put admin_update_publish_guidance_group_path(@guidance_group) - assert_response :redirect - assert flash[:notice].include?('published') - assert_redirected_to "#{admin_index_guidance_path}" - assert assigns(:guidance_group) - end - - # PUT /org/admin/guidancegroup/:id/admin_update_unpublish (admin_update_unpublish_guidance_group) - test 'unpublish the guidance' do - @guidance_group = GuidanceGroup.first - - # Should redirect user to the root path if they are not logged in! - put admin_update_unpublish_guidance_group_path(@guidance_group) - assert_unauthorized_redirect_to_root_path - - sign_in @user - - put admin_update_unpublish_guidance_group_path(@guidance_group) - assert_response :redirect - assert flash[:notice].include?('no longer published') - assert_redirected_to "#{admin_index_guidance_path}" - assert assigns(:guidance_group) - end - - # DELETE /org/admin/guidancegroup/:id/admin_destroy (admin_destroy_guidance_group_path) - # ---------------------------------------------------------- - test 'delete the guidance_group' do - id = GuidanceGroup.first.id - # Should redirect user to the root path if they are not logged in! - delete admin_destroy_guidance_group_path(GuidanceGroup.first) - assert_unauthorized_redirect_to_root_path - - sign_in @user - - delete admin_destroy_guidance_group_path(GuidanceGroup.first) - assert_response :redirect - assert_redirected_to admin_index_guidance_path - assert flash[:notice].start_with?('Successfully') && flash[:notice].include?('deleted') - assert_raise ActiveRecord::RecordNotFound do - GuidanceGroup.find(id).nil? - end - end - -end \ No newline at end of file diff --git a/test/functional/guidances_controller_test.rb b/test/functional/guidances_controller_test.rb deleted file mode 100644 index da6fd0b..0000000 --- a/test/functional/guidances_controller_test.rb +++ /dev/null @@ -1,180 +0,0 @@ -require 'test_helper' - -class GuidancesControllerTest < ActionDispatch::IntegrationTest - - include Devise::Test::IntegrationHelpers - - # TODO: The following methods SHOULD replace the old 'admin_' prefixed methods. The routes file already has - # these defined. They are defined multiple times though and we need to clean this up: - # - # SHOULD BE: - # -------------------------------------------------- - # guidances GET /guidances guidances#index - # POST /guidances guidances#create - # guidance GET /guidance/:id guidances#show - # PATCH /guidance/:id guidances#update - # PUT /guidance/:id guidances#update - # DELETE /guidance/:id guidances#destroy - # - # CURRENT RESULTS OF `rake routes` - # -------------------------------------------------- - # admin_show_guidance GET /org/admin/guidance/:id/admin_show guidances#admin_show - # admin_index_guidance GET /org/admin/guidance/:id/admin_index guidances#admin_index - # admin_edit_guidance GET /org/admin/guidance/:id/admin_edit guidances#admin_edit - # admin_new_guidance GET /org/admin/guidance/:id/admin_new guidances#admin_new - # admin_destroy_guidance DELETE /org/admin/guidance/:id/admin_destroy guidances#admin_destroy - # admin_create_guidance POST /org/admin/guidance/:id/admin_create guidances#admin_create - # admin_update_guidance PUT /org/admin/guidance/:id/admin_update guidances#admin_update - # update_phases_guidance GET /org/admin/guidance/:id/update_phases guidances#update_phases - # update_versions_guidance GET /org/admin/guidance/:id/update_versions guidances#update_versions - # update_sections_guidance GET /org/admin/guidance/:id/update_sections guidances#update_sections - # update_questions_guidance GET /org/admin/guidance/:id/update_questions guidances#update_questions - - setup do - scaffold_org_admin(GuidanceGroup.first.org) - @guidance_group = GuidanceGroup.first - end - - # GET /org/admin/guidance/:id/admin_index (admin_index_guidance_path) - # ---------------------------------------------------------- - test 'load the list of guidances page' do - # Should redirect user to the root path if they are not logged in! - get admin_index_guidance_path(@guidance_group) - assert_unauthorized_redirect_to_root_path - - sign_in @user - - get admin_index_guidance_path(@guidance_group) - assert_response :success - assert assigns(:guidances) - assert assigns(:guidance_groups) - end - - # /org/admin/guidance/:id/admin_new (admin_new_guidance_path) - # ---------------------------------------------------------- - test 'load the new guidance page' do - # Should redirect user to the root path if they are not logged in! - get admin_new_guidance_path(@guidance_group) - assert_unauthorized_redirect_to_root_path - - sign_in @user - - get admin_new_guidance_path(@guidance_group) - assert_response :success - end - - # /org/admin/guidance/:id/admin_edit (admin_edit_guidance_path) - # ---------------------------------------------------------- - test 'load the edit guidance page' do - # Should redirect user to the root path if they are not logged in! - get admin_edit_guidance_path(@guidance_group) - assert_unauthorized_redirect_to_root_path - - sign_in @user - - get admin_edit_guidance_path(@guidance_group) - assert_response :success - end - - # POST /org/admin/guidance/:id/admin_create (admin_create_guidance_path) - # ---------------------------------------------------------- - test 'create a new guidance' do - params = {'guidance-text': 'Testing create', guidance: {guidance_group_id: GuidanceGroup.first.id, published: true}} - - # Should redirect user to the root path if they are not logged in! - post admin_create_guidance_path(@user.org), params - assert_unauthorized_redirect_to_root_path - - sign_in @user - - post admin_create_guidance_path(@user.org), params - assert_response :redirect - assert_redirected_to admin_index_guidance_path - assert flash[:notice].start_with?('Successfully') && flash[:notice].include?('created') - assert_equal 'Testing create', Guidance.last.text, "expected the record to have been created!" - - # Invalid object - post admin_create_guidance_path(@user.org), {'guidance-text': nil, guidance: {published: false}} - assert flash[:alert].starts_with?(_('Could not create your')) - assert_response :redirect - assert_redirected_to admin_index_guidance_path - end - - # PUT /org/admin/guidance/:id/admin_update (admin_update_guidance_path) - # ---------------------------------------------------------- - test 'update the guidance' do - params = {'guidance-text': 'Testing UPDATE', guidance: {guidance_group_id: GuidanceGroup.first.id}} - - # Should redirect user to the root path if they are not logged in! - put admin_update_guidance_path(Guidance.first), params - assert_unauthorized_redirect_to_root_path - - sign_in @user - - put admin_update_guidance_path(Guidance.first), params - assert_response :redirect - assert flash[:notice].start_with?('Successfully') && flash[:notice].include?('saved') - assert_redirected_to admin_index_guidance_path - assert_equal 'Testing UPDATE', Guidance.first.text, "expected the record to have been updated" - - # Invalid object - put admin_update_guidance_path(Guidance.first), {'guidance-text': nil, guidance: {guidance_group_id: GuidanceGroup.first.id}} - assert flash[:alert].starts_with?(_('Could not update your')) - assert_response :redirect - assert_redirected_to admin_edit_guidance_path(Guidance.first) - end - - # PUT /org/admin/guidance/:id/admin_publish (admin_publish_guidance) - test 'publish the guidance' do - @guidance = Guidance.first - @guidance_group = @guidance.guidance_group - - # Should redirect user to the root path if they are not logged in! - put admin_publish_guidance_path(@guidance) - assert_unauthorized_redirect_to_root_path - - sign_in @user - - put admin_publish_guidance_path(@guidance) - assert_response :redirect - assert flash[:notice].include?('published') - assert_redirected_to "#{admin_index_guidance_path}" - end - - # PUT /org/admin/guidance/:id/admin_unpublish (admin_unpublish_guidance) - test 'unpublish the guidance' do - @guidance = Guidance.first - @guidance_group = @guidance.guidance_group - - # Should redirect user to the root path if they are not logged in! - put admin_unpublish_guidance_path(@guidance) - assert_unauthorized_redirect_to_root_path - - sign_in @user - - put admin_unpublish_guidance_path(@guidance) - assert_response :redirect - assert flash[:notice].include?('no longer published') - assert_redirected_to "#{admin_index_guidance_path}" - end - - # DELETE /org/admin/guidance/:id/admin_destroy (admin_destroy_guidance_path) - # ---------------------------------------------------------- - test 'delete the guidance' do - id = Guidance.first.id - # Should redirect user to the root path if they are not logged in! - delete admin_destroy_guidance_path(Guidance.first) - assert_unauthorized_redirect_to_root_path - - sign_in @user - - delete admin_destroy_guidance_path(Guidance.first) - assert_response :redirect - assert_redirected_to admin_index_guidance_path - assert flash[:notice].start_with?('Successfully') && flash[:notice].include?('deleted') - assert_raise ActiveRecord::RecordNotFound do - Guidance.find(id).nil? - end - end - -end \ No newline at end of file diff --git a/test/functional/home_controller_test.rb b/test/functional/home_controller_test.rb deleted file mode 100644 index a585c99..0000000 --- a/test/functional/home_controller_test.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'test_helper' - -class HomeControllerTest < ActionDispatch::IntegrationTest - - include Devise::Test::IntegrationHelpers - - setup do - @user = User.create(email: 'testing.another@user.account.org', password: 'password123', - password_confirmation: 'password123', accept_terms: true, - confirmed_at: Time.zone.now) - end - - # ---------------------------------------------------------- - test 'redirects logged in user to plans page' do - @user.firstname = 'Testing' - @user.surname = 'Another' - @user.save! - - sign_in @user - - get root_path - assert_response :redirect - assert_redirected_to plans_url - end - - # ---------------------------------------------------------- - test 'redirects logged in user to profile page if they have not added their name' do - sign_in @user - - get root_path - assert_response :redirect - -# TODO: This should be redirecting to the profile page so that the user can provide their name but the logic -# in the User model will always return the email address as the name so the check in the controller -# is always true and sends the user through to the plans page - #assert_redirected_to edit_user_registration_path - assert_redirected_to plans_url - end - -end \ No newline at end of file diff --git a/test/functional/notes_controller_test.rb b/test/functional/notes_controller_test.rb deleted file mode 100644 index da3cb39..0000000 --- a/test/functional/notes_controller_test.rb +++ /dev/null @@ -1,131 +0,0 @@ -require 'test_helper' - -class NotesControllerTest < ActionDispatch::IntegrationTest - - include Devise::Test::IntegrationHelpers - - setup do - @user = User.last - - scaffold_plan - # Assign the user to the plan as a commenter/reader - @plan.assign_reader(@user.id) - @plan.save! - - @question = Question.create(text: 'Answer Testing', number: 9, - section: @plan.template.phases.first.sections.first, - question_format: QuestionFormat.find_by(option_based: false)) - - @answer = Answer.create(user: @user, plan: @plan, question: @question, text: 'Testing') - - @note = Note.create(user: @user, plan: @plan, answer: @answer, question: @question, archived: false, - text: 'Test Note') - end - -# TODO: The following methods SHOULD probably be restful -# -# SHOULD BE: -# -------------------------------------------------- -# notes GET /answers/:answer_id/notes notes#index -# POST /answers/:answer_id/notes notes#create -# note GET /answers/:answer_id/notes/:id notes#show -# PATCH /answers/:answer_id/notes/:id notes#update -# PUT /answers/:answer_id/notes/:id notes#update -# DELETE /answers/:answer_id/notes/:id notes#destroy -# -# CURRENT RESULTS OF `rake routes` -# -------------------------------------------------- -# archive_note PATCH /notes/:id/archive notes#archive -# notes POST /notes notes#create -# note PATCH /notes/:id notes#update -# PUT /notes/:id notes#update - - - - # POST /notes (notes_path) - # ---------------------------------------------------------- - test "create a new note" do - params = {user_id: @user.id, answer_id: @answer.id, plan_id: @plan.id, question_id: @question.id, text: 'Test Note'} - - # Should redirect user to the root path if they are not logged in! - post notes_path, {note: params} - assert_unauthorized_redirect_to_root_path - - sign_in @user - - post notes_path, {note: params}, {'ACCEPT': 'application/json'} - assert_response :success - assert assigns(:note) - assert assigns(:plan) - assert assigns(:answer) - assert assigns(:question) - assert assigns(:notice) - #assert_select '.welcome-message h2', _('Comment was successfully created.') - assert_equal 'Test Note', Note.last.text, 'Expected the note to have been created' - - # No Answer - post notes_path, {note: {user_id: @user.id, plan_id: @plan.id, question_id: @question.id}}, {'ACCEPT': 'application/json'} - assert_response :bad_request - # TODO: expected the new note to have been added :/ - #assert_equal 'Test Note no Answer', Note.last.text, 'Expected the note to have been created even if there was no answer' - - # Invalid object - post notes_path, {note: {user_id: @user.id, answer_id: @answer.id, plan_id: @plan.id, - question_id: @question.id}}, {'ACCEPT': 'application/json'} - assert_response :bad_request - assert assigns(:note) - assert assigns(:plan) - assert assigns(:answer) - assert assigns(:question) - assert assigns(:notice) - end - - # PUT /notes/:id (note_path) - # ---------------------------------------------------------- - test "update the note" do - # Should redirect user to the root path if they are not logged in! - put note_path(@note), { note: { text: 'Test Note' }, id: @note.id }, {'ACCEPT': 'application/json'} - assert_unauthorized_redirect_to_root_path - - sign_in @user - - # Valid save - put note_path(@note), { note: {text: 'Test Note' }, id: @note.id }, {'ACCEPT': 'application/json'} - assert_response :success - assert assigns(:note) - assert assigns(:plan) - assert assigns(:answer) - assert assigns(:question) - assert assigns(:notice) - @note.reload - assert_equal 'Test Note', @note.text, "expected the note's text to be 'Test Note'" - - # Invalid save - put note_path(@note), { note: { text: nil }, id: @note.id }, {'ACCEPT': 'application/json'} - assert_response :bad_request - assert assigns(:notice) - assert_equal 'Test Note', @note.text, "expected the note's text to Still be 'Test Note'" - end - - # PATCH /notes/:id/archive (archive_note_path) - # ---------------------------------------------------------- - test "delete the note" do - # Should redirect user to the root path if they are not logged in! - patch archive_note_path(@note), { note: { archived_by: @user.id }, id: @note.id }, {'ACCEPT': 'application/json'} - assert_unauthorized_redirect_to_root_path - - sign_in @user - - patch archive_note_path(@note), { note: { archived_by: @user.id }, id: @note.id }, {'ACCEPT': 'application/json'} - assert_response :success - assert assigns(:note) - assert assigns(:plan) - assert assigns(:answer) - assert assigns(:question) - assert assigns(:notice) - - @note.reload - assert @note.archived, 'expected the archived flag to be true' - assert_equal @user.id, @note.archived_by, 'expected the archived_by to be set to @user' - end -end diff --git a/test/functional/notifications_controller_test.rb b/test/functional/notifications_controller_test.rb deleted file mode 100644 index 8616c5d..0000000 --- a/test/functional/notifications_controller_test.rb +++ /dev/null @@ -1,83 +0,0 @@ -require 'test_helper' -module SuperAdmin - class NotificationsControllerTest < ActionController::TestCase - include Devise::Test::ControllerHelpers - - setup do - @super_admin = User.find_by(email: 'super_admin@example.com') - scaffold_org_admin(Org.last) - - @notification_attributes = { - notification_type: Notification.notification_types[:global], - title: 'notification_1', - level: Notification.levels[:info], - body: 'notification 1', - dismissable: true, - starts_at: Date.today, - expires_at: Date.tomorrow - } - @notification = Notification.create!(@notification_attributes) - end - - test 'should get index' do - sign_in @super_admin - get :index - assert_response :success - assert_not_nil assigns(:notifications) - end - - test 'should get new' do - sign_in @super_admin - get :new - assert_response :success - end - - test 'should create notification' do - sign_in @super_admin - assert_difference('Notification.count') do - @notification_attributes[:level] = :info #controller is expecting the symbol instead of the numerical value - post :create, notification: @notification_attributes - end - assert_redirected_to super_admin_notifications_url - end - - test 'should get edit' do - sign_in @super_admin - get :edit, id: @notification - assert_response :success - assert_not_nil assigns(:notification) - end - - test 'should update notification' do - sign_in @super_admin - @notification_attributes[:title] = 'notification_2' - @notification_attributes[:level] = :info #controller is expecting the symbol instead of the numerical value - patch :update, id: @notification, notification: @notification_attributes - assert_redirected_to super_admin_notifications_url - end - - test 'should destroy notification' do - sign_in @super_admin - assert_difference('Notification.count', -1) do - delete :destroy, id: @notification - end - assert_redirected_to super_admin_notifications_url - end - - test 'unauthorized redirections' do - sign_in @user - get :index - assert_redirected_to(plans_url) - get :new - assert_redirected_to(plans_url) - post :create, notification: @notification_attributes - assert_redirected_to(plans_url) - get :edit, id: @notification - assert_redirected_to(plans_url) - patch :update, id: @notification, notification: @notification_attributes - assert_redirected_to(plans_url) - delete :destroy, id: @notification - assert_redirected_to(plans_url) - end - end -end diff --git a/test/functional/org_admin/phases_controller_test.rb b/test/functional/org_admin/phases_controller_test.rb deleted file mode 100644 index 211959d..0000000 --- a/test/functional/org_admin/phases_controller_test.rb +++ /dev/null @@ -1,154 +0,0 @@ -require 'test_helper' - -class PhasesControllerTest < ActionDispatch::IntegrationTest - - include Devise::Test::IntegrationHelpers - - setup do - @institution = init_institution - @researcher = init_researcher(@institution) - @org_admin = init_org_admin(@institution) - @template = init_template(@institution, { - title: 'Test Template', - published: true, - visibility: Template.visibilities[:publicly_visible] - }) - @phase = init_phase(@template) - end - - test "unauthorized user cannot access the show/edit page" do - get org_admin_template_phase_path(@template, @phase) - assert_unauthorized_redirect_to_root_path - sign_in @researcher - get org_admin_template_phase_path(@template, @phase) - assert_authorized_redirect_to_plans_page - end - - test 'authorized user can access the show/edit page' do - sign_in @org_admin - get org_admin_template_phase_path(@template, @phase) - assert_response :success - assert_nil flash[:notice] - assert_nil flash[:alert] - end - - test 'get phases#edit redirects to #show when template is not latest' do - new_version = @template.generate_version! - sign_in @org_admin - get(edit_org_admin_template_phase_path(@template.id, @template.phases.first.id)) - assert_response :redirect - assert_redirected_to org_admin_template_phase_path(@template.id, @template.phases.first.id) - end - - test "unauthorized user cannot access the preview phase page" do - get preview_org_admin_template_phase_path(@template, @phase) - assert_unauthorized_redirect_to_root_path - sign_in @researcher - get preview_org_admin_template_phase_path(@template, @phase) - assert_authorized_redirect_to_plans_page - end - - test 'authorized user can access the preview phase page' do - sign_in @org_admin - get preview_org_admin_template_phase_path(@template, @phase) - assert_response :success - assert_nil flash[:notice] - assert_nil flash[:alert] - end - - test "unauthorized user cannot access the new phase page" do - get new_org_admin_template_phase_path(@template) - assert_unauthorized_redirect_to_root_path - sign_in @researcher - get new_org_admin_template_phase_path(@template) - assert_authorized_redirect_to_plans_page - end - - test 'authorized user can access the new phase page' do - sign_in @org_admin - get new_org_admin_template_phase_path(@template) - assert_response :success - assert_nil flash[:notice] - assert_nil flash[:alert] - end - - test 'unauthorized user cannot create a phase' do - params = { phase: { title: 'New phase', number: 2 } } - post org_admin_template_phases_path(@template), params - assert_unauthorized_redirect_to_root_path - sign_in @researcher - post org_admin_template_phases_path(@template), params - assert_authorized_redirect_to_plans_page - end - - test 'authorized user can create a phase for an unpublished template' do - @template.update!(published: false) - params = { phase: { title: 'New phase', number: 2 } } - sign_in @org_admin - post org_admin_template_phases_path(@template), params - assert_response :redirect - assert_redirected_to edit_org_admin_template_phase_path(template_id: @template.id, id: @template.phases.last.id) - end - - test 'authorized user can create a phase for a published template' do - params = { phase: { title: 'New phase', number: 2 } } - sign_in @org_admin - post org_admin_template_phases_path(@template), params - assert_response :redirect - template = Template.latest_version(@template.family_id).first - assert_redirected_to edit_org_admin_template_phase_path(template_id: template.id, id: template.phases.last.id) - end - - test 'unauthorized user cannot edit a phase' do - params = { phase: { title: 'New phase' } } - put org_admin_template_phase_path(@template, @phase), params - assert_unauthorized_redirect_to_root_path - sign_in @researcher - put org_admin_template_phase_path(@template, @phase), params - assert_authorized_redirect_to_plans_page - end - - test 'authorized user can edit a phase for an unpublished template' do - @template.update!(published: false) - params = { phase: { title: 'New phase' } } - sign_in @org_admin - put org_admin_template_phase_path(@template, @phase), params - assert_response :redirect - assert_redirected_to edit_org_admin_template_phase_path(template_id: @template.id, id: @template.phases.last.id) - end - - test 'authorized user can edit a phase for a published template' do - params = { phase: { title: 'New phase' } } - sign_in @org_admin - put org_admin_template_phase_path(@template, @phase), params - assert_response :redirect - template = Template.latest_version(@template.family_id).first - assert_redirected_to edit_org_admin_template_phase_path(template_id: template.id, id: template.phases.last.id) - end - - test 'unauthorized user cannot delete a phase' do - delete org_admin_template_phase_path(@template, @phase) - assert_unauthorized_redirect_to_root_path - sign_in @researcher - delete org_admin_template_phase_path(@template, @phase) - assert_authorized_redirect_to_plans_page - end - - test 'authorized user can delete a phase from an unpublished template' do - @template.update!(published: false) - params = { phase: { title: 'New phase' } } - sign_in @org_admin - delete org_admin_template_phase_path(@template, @phase) - assert_response :redirect - assert_redirected_to edit_org_admin_template_path(@template.id) - end - - test 'authorized user can delete a phase from a published template' do - params = { phase: { title: 'New phase' } } - sign_in @org_admin - delete org_admin_template_phase_path(@template, @phase) - assert_response :redirect - template = Template.latest_version(@template.family_id).first - assert_redirected_to edit_org_admin_template_path(template.id) - end -end diff --git a/test/functional/org_admin/plans_controller_test.rb b/test/functional/org_admin/plans_controller_test.rb deleted file mode 100644 index cb3d50e..0000000 --- a/test/functional/org_admin/plans_controller_test.rb +++ /dev/null @@ -1,84 +0,0 @@ -require 'test_helper' - -class PlansControllerTest < ActionDispatch::IntegrationTest - - include Devise::Test::IntegrationHelpers - - setup do - # Get the first Org Admin - @org = Org.funder.first - @admin = User.create!(email: "org-admin-plans-tester@example.com", - firstname: "Org", surname: "Admin", - password: "password123", password_confirmation: "password123", - org: @org, accept_terms: true, confirmed_at: Time.zone.now) - - # Make sure the user is an org admin - @admin.perms << Perm.where(name: ['grant_permissions', 'modify_guidance', - 'modify_templates', 'change_org_details']) - @admin.save! - - @regular_user = User.create!(email: 'org_admin_plans_tester@example.com', firstname: "Tester", surname: "Testing", - password: "password123", password_confirmation: "password123", - org: @org, accept_terms: true, confirmed_at: Time.zone.now, ) - @plan = Plan.create!(template: Template.first, title: 'Test Plan', visibility: :privately_visible, feedback_requested: true, - roles: [Role.new(user: @regular_user, creator: true)]) - Role.create!(user: @admin, plan: @plan, access: Role.access_values_for(:reviewer).min) - end - - test "unauthorized user cannot access the plans page" do - # Should redirect user to the root path if they are not logged in! - get org_admin_plans_path - assert_unauthorized_redirect_to_root_path - # Non Org-Admin cannot perform this action - sign_in @regular_user - get org_admin_plans_path - assert_authorized_redirect_to_plans_page - end - - test "org admin can access the plans page" do - sign_in @admin - get org_admin_plans_path - - assert_response :success - assert assigns(:plans) - assert assigns(:feedback_plans) - end - - test "unauthorized user cannot complete feedback" do - # Should redirect user to the root path if they are not logged in! - get org_admin_plans_path - get feedback_complete_org_admin_plan_path(@plan) - # Non Org-Admin cannot perform this action - sign_in @regular_user - get feedback_complete_org_admin_plan_path(@plan) - assert_authorized_redirect_to_plans_page - end - - test "org admin can complete feedback" do - sign_in @admin - get feedback_complete_org_admin_plan_path(@plan) - assert_response :redirect - - # TODO: This one is failing on Travis but not on any other machine - # seems to be due to the seeds.rb not loading properly on the - # latest instance of Travis - #assert_redirected_to org_admin_plans_path - end - - test "unauthorized user cannot download the plans as CSV" do - # Should redirect user to the root path if they are not logged in! - get org_admin_download_plans_path(format: :csv) - assert_unauthorized_redirect_to_root_path - # Non Org-Admin cannot perform this action - sign_in @regular_user - get org_admin_download_plans_path(format: :csv) - assert_authorized_redirect_to_plans_page - end - - test "org admin can download plans as CSV" do - sign_in @admin - get org_admin_download_plans_path(format: :csv) - assert_response :success - end - -end \ No newline at end of file diff --git a/test/functional/org_admin/questions_controller_test.rb b/test/functional/org_admin/questions_controller_test.rb deleted file mode 100644 index 1324e3f..0000000 --- a/test/functional/org_admin/questions_controller_test.rb +++ /dev/null @@ -1,133 +0,0 @@ -require 'test_helper' - -class QuestionsControllerTest < ActionDispatch::IntegrationTest - - include Devise::Test::IntegrationHelpers - - setup do - @institution = init_institution - @researcher = init_researcher(@institution) - @org_admin = init_org_admin(@institution) - @template = init_template(@institution, { - title: 'Test Template', - published: true, - visibility: Template.visibilities[:publicly_visible] - }) - @phase = init_phase(@template) - @section = init_section(@phase) - @text_area = init_question_format({ title: 'Test question format' }) - @question = init_question(@section) - end - - test 'unauthorized user cannot call question_controller#create' do - params = { question: { text: 'New question test', number: 2, question_format_id: @text_area.id } } - post org_admin_template_phase_section_questions_path(@template, @phase, @section), params - assert_unauthorized_redirect_to_root_path - sign_in @researcher - post org_admin_template_phase_section_questions_path(@template, @phase, @section), params - assert_authorized_redirect_to_plans_page - end - - test 'unauthorized user cannot call question_controller#create for another org\'s template' do - params = { question: { text: 'New question test', number: 2, question_format_id: @text_area.id } } - funder = init_funder - funder_template = init_template(funder) - funder_phase = init_phase(funder_template) - funder_section = init_section(funder_phase) - sign_in @org_admin - post org_admin_template_phase_section_questions_path(funder_template, funder_phase, funder_section), params - assert_authorized_redirect_to_plans_page - end - - test 'authorized user can call question_controller#create for an unpublished template' do - @template.update!(published: false) - params = { question: { text: 'New question test', number: 2, question_format_id: @text_area.id } } - sign_in @org_admin - post org_admin_template_phase_section_questions_path(@template, @phase, @section), params - assert_response :redirect - assert_redirected_to edit_org_admin_template_phase_path(template_id: @template.id, id: @phase.id, section: @section.id) - end - - test 'authorized user can call question_controller#create for a published template' do - params = { question: { text: 'New question test', number: 2, question_format_id: @text_area.id } } - sign_in @org_admin - post org_admin_template_phase_section_questions_path(@template, @phase, @section), params - assert_response :redirect - template = Template.latest_version(@template.family_id).first - assert_redirected_to edit_org_admin_template_phase_path(template_id: template.id, id: template.phases.first.id, section: template.phases.first.sections.first.id) - end - - test 'unauthorized user cannot call question_controller#edit' do - params = { section: { text: 'Edited question' } } - put org_admin_template_phase_section_question_path(@template, @phase, @section, @question), params - assert_unauthorized_redirect_to_root_path - sign_in @researcher - put org_admin_template_phase_section_question_path(@template, @phase, @section, @question), params - assert_authorized_redirect_to_plans_page - end - - test 'unauthorized user cannot call question_controller#edit for another org\'s template' do - params = { section: { text: 'Edited question' } } - funder = init_funder - funder_template = init_template(funder) - funder_phase = init_phase(funder_template) - funder_section = init_section(funder_phase) - funder_question = init_question(funder_section) - sign_in @org_admin - put org_admin_template_phase_section_question_path(funder_template, funder_phase, funder_section, funder_question), params - assert_authorized_redirect_to_plans_page - end - - test 'authorized user can call question_controller#edit for an unpublished template' do - @template.update!(published: false) - params = { section: { text: 'Edited question' } } - sign_in @org_admin - put org_admin_template_phase_section_question_path(@template, @phase, @section, @question), params - assert_response :redirect - assert_redirected_to edit_org_admin_template_phase_path(template_id: @template.id, id: @phase.id, section: @section.id) - end - - test 'authorized user can call question_controller#edit for a published template' do - params = { section: { text: 'Edited question' } } - sign_in @org_admin - put org_admin_template_phase_section_question_path(@template, @phase, @section, @question), params - assert_response :redirect - template = Template.latest_version(@template.family_id).first - assert_redirected_to edit_org_admin_template_phase_path(template_id: template.id, id: template.phases.first.id, section: template.phases.first.sections.first.id) - end - - test 'unauthorized user cannot call question_controller#destroy' do - delete org_admin_template_phase_section_question_path(@template, @phase, @section, @question) - assert_unauthorized_redirect_to_root_path - sign_in @researcher - delete org_admin_template_phase_section_question_path(@template, @phase, @section, @question) - assert_authorized_redirect_to_plans_page - end - - test 'unauthorized user cannot call question_controller#destroy for another org\'s template' do - funder = init_funder - funder_template = init_template(funder) - funder_phase = init_phase(funder_template) - funder_section = init_section(funder_phase) - funder_question = init_question(funder_section) - sign_in @org_admin - delete org_admin_template_phase_section_question_path(funder_template, funder_phase, funder_section, funder_question) - assert_authorized_redirect_to_plans_page - end - - test 'authorized user can call question_controller#destroy for an unpublished template' do - @template.update!(published: false) - sign_in @org_admin - delete org_admin_template_phase_section_question_path(@template, @phase, @section, @question) - assert_response :redirect - assert_redirected_to edit_org_admin_template_phase_path(template_id: @template.id, id: @phase.id, section: @section.id) - end - - test 'authorized user can call question_controller#destroy for a published template' do - sign_in @org_admin - delete org_admin_template_phase_section_question_path(@template, @phase, @section, @question) - assert_response :redirect - template = Template.latest_version(@template.family_id).first - assert_redirected_to edit_org_admin_template_phase_path(template_id: template.id, id: template.phases.first.id, section: template.phases.first.sections.first.id) - end -end \ No newline at end of file diff --git a/test/functional/org_admin/sections_controller_test.rb b/test/functional/org_admin/sections_controller_test.rb deleted file mode 100644 index 6bb2c4c..0000000 --- a/test/functional/org_admin/sections_controller_test.rb +++ /dev/null @@ -1,174 +0,0 @@ -require 'test_helper' - -class SectionsControllerTest < ActionDispatch::IntegrationTest - - include Devise::Test::IntegrationHelpers - - setup do - @institution = init_institution - @researcher = init_researcher(@institution) - @org_admin = init_org_admin(@institution) - @template = init_template(@institution, { - title: 'Test Template', - published: true, - visibility: Template.visibilities[:publicly_visible] - }) - @phase = init_phase(@template) - @section = init_section(@phase) - end - - test "unauthorized user cannot access the index page" do - get org_admin_template_phase_sections_path(@template, @phase) - assert_unauthorized_redirect_to_root_path - end - - test 'authorized user can access the index page' do - [@researcher, @org_admin].each do |user| - sign_in user - get org_admin_template_phase_sections_path(@template, @phase) - assert_response :success, "expected #{user.name(false)} to be able to access the section_controller#index page" - assert_nil flash[:notice] - assert_nil flash[:alert] - end - end - - test "unauthorized user cannot access the section_controller#show page" do - get org_admin_template_phase_section_path(@template, @phase, @section) - assert_unauthorized_redirect_to_root_path - end - - test 'authorized user can access the section_controller#show page' do - [@researcher, @org_admin].each do |user| - sign_in user - get org_admin_template_phase_section_path(@template, @phase, @section) - assert_response :success, "expected #{user.name(false)} to be able to access the section_controller#show page" - assert_nil flash[:notice] - assert_nil flash[:alert] - end - end - - test "unauthorized user cannot access the section_controller#edit page" do - get edit_org_admin_template_phase_section_path(@template, @phase, @section) - assert_unauthorized_redirect_to_root_path - sign_in @researcher - get edit_org_admin_template_phase_section_path(@template, @phase, @section) - assert_authorized_redirect_to_plans_page - end - - test 'authorized user can access the section_controller#edit page' do - sign_in @org_admin - get edit_org_admin_template_phase_section_path(@template, @phase, @section) - assert_response :success - assert_nil flash[:notice] - assert_nil flash[:alert] - end - - test 'unauthorized user cannot call section_controller#create' do - params = { section: { title: 'New section', number: 2 } } - post org_admin_template_phase_sections_path(@template, @phase), params - assert_unauthorized_redirect_to_root_path - sign_in @researcher - post org_admin_template_phase_sections_path(@template, @phase), params - assert_authorized_redirect_to_plans_page - end - - test 'unauthorized user cannot call section_controller#create for another org\'s template' do - params = { section: { title: 'New section', number: 2 } } - funder = init_funder - funder_template = init_template(funder) - funder_phase = init_phase(funder_template) - sign_in @org_admin - post org_admin_template_phase_sections_path(funder_template, funder_phase), params - assert_authorized_redirect_to_plans_page - end - - test 'authorized user can call section_controller#create for an unpublished template' do - @template.update!(published: false) - params = { section: { title: 'New section', number: 2 } } - sign_in @org_admin - post org_admin_template_phase_sections_path(@template, @phase), params - assert_response :redirect - assert_redirected_to edit_org_admin_template_phase_path(template_id: @template.id, id: @phase.id, section: @phase.sections.last.id) - end - - test 'authorized user can call section_controller#create for a published template' do - params = { section: { title: 'New section', number: 2 } } - sign_in @org_admin - post org_admin_template_phase_sections_path(@template, @phase), params - assert_response :redirect - template = Template.latest_version(@template.family_id).first - assert_redirected_to edit_org_admin_template_phase_path(template_id: template.id, id: template.phases.first.id, section: template.phases.first.sections.last.id) - end - - test 'unauthorized user cannot call section_controller#edit' do - params = { section: { title: 'Edited section' } } - put org_admin_template_phase_section_path(@template, @phase, @section), params - assert_unauthorized_redirect_to_root_path - sign_in @researcher - put org_admin_template_phase_section_path(@template, @phase, @section), params - assert_authorized_redirect_to_plans_page - end - - test 'unauthorized user cannot call section_controller#edit for another org\'s template' do - params = { section: { title: 'Edited section' } } - funder = init_funder - funder_template = init_template(funder) - funder_phase = init_phase(funder_template) - funder_section = init_section(funder_phase) - sign_in @org_admin - put org_admin_template_phase_section_path(funder_template, funder_phase, funder_section), params - assert_authorized_redirect_to_plans_page - end - - test 'authorized user can call section_controller#edit for an unpublished template' do - @template.update!(published: false) - params = { section: { title: 'Edited section' } } - sign_in @org_admin - put org_admin_template_phase_section_path(@template, @phase, @section), params - assert_response :redirect - assert_redirected_to edit_org_admin_template_phase_path(template_id: @template.id, id: @phase.id, section: @phase.sections.last.id) - end - - test 'authorized user can call section_controller#edit for a published template' do - params = { section: { title: 'Edited section' } } - sign_in @org_admin - put org_admin_template_phase_section_path(@template, @phase, @section), params - assert_response :redirect - template = Template.latest_version(@template.family_id).first - assert_redirected_to edit_org_admin_template_phase_path(template_id: template.id, id: template.phases.first.id, section: template.phases.first.sections.last.id) - end - - test 'unauthorized user cannot call section_controller#destroy' do - delete org_admin_template_phase_section_path(@template, @phase, @section) - assert_unauthorized_redirect_to_root_path - sign_in @researcher - delete org_admin_template_phase_section_path(@template, @phase, @section) - assert_authorized_redirect_to_plans_page - end - - test 'unauthorized user cannot call section_controller#destroy for another org\'s template' do - funder = init_funder - funder_template = init_template(funder) - funder_phase = init_phase(funder_template) - funder_section = init_section(funder_phase) - sign_in @org_admin - delete org_admin_template_phase_section_path(funder_template, funder_phase, funder_section) - assert_authorized_redirect_to_plans_page - end - - test 'authorized user can call section_controller#destroy for an unpublished template' do - @template.update!(published: false) - sign_in @org_admin - delete org_admin_template_phase_section_path(@template, @phase, @section) - assert_response :redirect - assert_redirected_to edit_org_admin_template_phase_path(template_id: @template.id, id: @phase.id) - end - - test 'authorized user can call section_controller#destroy for a published template' do - sign_in @org_admin - delete org_admin_template_phase_section_path(@template, @phase, @section) - assert_response :redirect - template = Template.latest_version(@template.family_id).first - assert_redirected_to edit_org_admin_template_phase_path(template_id: template.id, id: template.phases.first.id) - end -end \ No newline at end of file diff --git a/test/functional/org_admin/templates_controller_test.rb b/test/functional/org_admin/templates_controller_test.rb deleted file mode 100644 index d778889..0000000 --- a/test/functional/org_admin/templates_controller_test.rb +++ /dev/null @@ -1,309 +0,0 @@ -require 'test_helper' - -class TemplatesControllerTest < ActionDispatch::IntegrationTest - - include Devise::Test::IntegrationHelpers - - setup do - @funder = init_funder - @institution = init_institution - @organisation = init_organisation - - @researcher = init_researcher(@institution) - @org_admin = init_org_admin(@institution) - @super_admin = init_super_admin(@organisation) - - @funder_template = init_template(@funder, { - title: 'Test Funder Template', - published: true, - visibility: Template.visibilities[:publicly_visible] - }) - @org_template = init_template(@institution, { - title: 'Test Org Template', - published: true - }) - end - - test "unauthorized user cannot access the templates#index page" do - get org_admin_templates_path - assert_unauthorized_redirect_to_root_path - sign_in @researcher - get org_admin_templates_path - assert_authorized_redirect_to_plans_page - sign_in @org_admin - get org_admin_templates_path - assert_authorized_redirect_to_plans_page - end - - test "authorized user can access the templates#index page" do - sign_in @super_admin - get org_admin_templates_path - assert_response :success - end - - test "unauthorized user cannot access the templates#organisational page" do - get organisational_org_admin_templates_path - assert_unauthorized_redirect_to_root_path - sign_in @researcher - get organisational_org_admin_templates_path - assert_authorized_redirect_to_plans_page - end - - test "authorized user can access the templates#organisational page" do - sign_in @org_admin - get organisational_org_admin_templates_path - assert_response :success - end - - test "unauthorized user cannot access the templates#customisable page" do - get customisable_org_admin_templates_path - assert_unauthorized_redirect_to_root_path - sign_in @researcher - get customisable_org_admin_templates_path - assert_authorized_redirect_to_plans_page - end - - test "authorized user can access the templates#customisable page" do - sign_in @org_admin - get customisable_org_admin_templates_path - assert_response :success - end - - test "unauthorized user cannot access the template#edit page" do - get edit_org_admin_template_path(@org_template) - assert_unauthorized_redirect_to_root_path - sign_in @researcher - get edit_org_admin_template_path(@org_template) - assert_authorized_redirect_to_plans_page - end - - test "authorized user can access the template#edit page" do - sign_in @org_admin - get edit_org_admin_template_path(@org_template) - assert_response :success - end - - test "admin cannot access another org's template#edit page" do - sign_in @org_admin - get edit_org_admin_template_path(@funder_template) - assert_authorized_redirect_to_plans_page - end - - test "super admin can access any org's template#edit page" do - sign_in @super_admin - [@org_template, @funder_template].each do |template| - get edit_org_admin_template_path(template) - assert_response :success - end - end - - test 'get templates#edit returns ok when template is latest' do - sign_in @org_admin - get(edit_org_admin_template_path(@org_template)) - assert_response :success - assert_nil flash[:notice], 'expected no warning messages' - end - - test 'get templates#edit redirects to #show when template is not latest' do - new_version = @org_template.generate_version! - sign_in @org_admin - get(edit_org_admin_template_path(@org_template.id)) - assert_response :redirect - assert_redirected_to org_admin_template_path(@org_template.id) - end - - test "unauthorized user cannot access the template#new page" do - get new_org_admin_template_path - assert_unauthorized_redirect_to_root_path - sign_in @researcher - get new_org_admin_template_path - assert_authorized_redirect_to_plans_page - end - - test "authorized user can access the template#new page" do - sign_in @org_admin - get new_org_admin_template_path - assert_response :success - end - - test "unauthorized user cannot access the template#history page" do - get history_org_admin_template_path(@org_template) - assert_unauthorized_redirect_to_root_path - sign_in @researcher - get history_org_admin_template_path(@org_template) - assert_authorized_redirect_to_plans_page - end - - test "authorized user can access the template#history page" do - sign_in @org_admin - get history_org_admin_template_path(@org_template) - assert_response :success - end - - test "unauthorized user cannot access template#delete" do - delete org_admin_template_path(@org_template) - assert_unauthorized_redirect_to_root_path - sign_in @researcher - delete org_admin_template_path(@org_template) - assert_authorized_redirect_to_plans_page - end - - test "authorized user can access template#delete" do - sign_in @org_admin - delete org_admin_template_path(@org_template) - assert_response :redirect - assert_redirected_to org_admin_templates_path - assert_nil flash[:alert] - end - - test "unauthorized user cannot create a template#create" do - post org_admin_templates_path(@institution), {template: {title: ''}} - assert_unauthorized_redirect_to_root_path - sign_in @researcher - post org_admin_templates_path(@institution), {template: {title: ''}} - assert_authorized_redirect_to_plans_page - end - - test "authorized user can create a template#create" do - params = {title: 'Testing create route'} - sign_in @org_admin - - post org_admin_templates_path(@institution), {template: params} - assert flash[:notice].start_with?('Successfully') && flash[:notice].include?('created') - assert_response :redirect - assert_redirected_to edit_org_admin_template_url(Template.last.id) - end - - test "unauthorized user cannot update a template#update" do - put org_admin_template_path(@org_template), {template: {title: ''}} - assert_unauthorized_redirect_to_root_path - sign_in @researcher - put org_admin_template_path(@org_template), {template: {title: ''}} - assert_authorized_redirect_to_plans_page - end - - test "authorized user can update the template#update" do - params = {title: 'ABCD'} - sign_in @org_admin - put org_admin_template_path(@org_template), {template: params} - assert_response :ok - json_body = ActiveSupport::JSON.decode(response.body) - assert json_body["msg"].start_with?('Successfully') && json_body["msg"].include?('saved') - end - - test "unauthorized user cannot customize a template#customize" do - post customize_org_admin_template_path(@org_template) - assert_unauthorized_redirect_to_root_path - sign_in @researcher - post customize_org_admin_template_path(@org_template) - assert_authorized_redirect_to_plans_page - end - - test "authorized user can customize a funder template#customize" do - @funder_template.update!({ published: true }) - sign_in @org_admin - post customize_org_admin_template_path(@funder_template) - assert_response :redirect - assert_redirected_to org_admin_template_url(Template.latest_customized_version(@funder_template.family_id, @institution.id).first) - end - - test "unauthorized user cannot publish a template#publish" do - patch publish_org_admin_template_path(@org_template) - assert_unauthorized_redirect_to_root_path - sign_in @researcher - patch publish_org_admin_template_path(@org_template) - assert_authorized_redirect_to_plans_page - end - - test "authorized user cannot publish another org's template#publish" do - sign_in @org_admin - patch publish_org_admin_template_path(@funder_template) - assert_authorized_redirect_to_plans_page - end - - test "authorized user can publish a template#publish" do - sign_in @org_admin - patch publish_org_admin_template_path(@org_template) - assert_equal _('Your template has been published and is now available to users.'), flash[:notice] - assert_response :redirect - assert_redirected_to org_admin_templates_path - end - - test "unauthorized user cannot unpublish a template#unpublish" do - patch unpublish_org_admin_template_path(@org_template) - assert_unauthorized_redirect_to_root_path - sign_in @researcher - patch unpublish_org_admin_template_path(@org_template) - assert_authorized_redirect_to_plans_page - end - - test "authorized user can unpublish a template#unpublish" do - sign_in @org_admin - patch unpublish_org_admin_template_path(@org_template) - assert_response :redirect - assert_redirected_to org_admin_templates_path - end - - test "unauthorized user cannot copy a template#copy" do - post copy_org_admin_template_path(@org_template) - assert_unauthorized_redirect_to_root_path - sign_in @researcher - post copy_org_admin_template_path(@org_template) - assert_authorized_redirect_to_plans_page - end - - test "unauthorized user cannot copy another org's template template#copy" do - sign_in @researcher - post copy_org_admin_template_path(@funder_template) - assert_response :redirect - assert_authorized_redirect_to_plans_page - end - - test "authorized super admin can copy another org's template template#copy" do - sign_in @super_admin - post copy_org_admin_template_path(@funder_template) - assert_response :redirect - assert_redirected_to edit_org_admin_template_url(Template.where(org_id: @organisation.id).order(id: :desc).last) - end - - test "authorized user can copy a template#copy" do - sign_in @org_admin - post copy_org_admin_template_path(@org_template) - assert_response :redirect - assert_redirected_to edit_org_admin_template_url(Template.where(org_id: @institution.id).last) - end - - test "unauthorized user cannot transfer a template customization template#transfer_customization" do - post transfer_customization_org_admin_template_path(@org_template) - assert_unauthorized_redirect_to_root_path - sign_in @researcher - post transfer_customization_org_admin_template_path(@org_template) - assert_authorized_redirect_to_plans_page - end - - test "authorized user can transfer a template customization template#transfer_customization" do -# TODO: This will not work because Rails is persisting these transactions to the DB at the same time, so their created_at -# timestamps match even if we add a 'sleep' statement. The template.upgrade_customization? will fail because of this. -# sign_in @org_admin -# original = @funder_template.customize!(@organisation) -# # Add a phase to the funder template and republish it -# phase = init_phase(@funder_template, { title: 'testing transfer of customizations' }) -# phase.template.update!({ published: true, title: 'upgraded funder template' }) -# post transfer_customization_org_admin_template_path(original) -# assert_response :redirect -# assert_redirected_to edit_org_admin_template_url(Template.latest_customized_version(@funder_template.family_id, @organisation.id).first) - end - - test "unauthorized user cannot get template#template_options" do - get "#{org_admin_template_options_path}?plan[org_id]=#{@institution.id}&plan[funder_id]=#{@funder.id}" - assert_unauthorized_redirect_to_root_path - end - - test "authorized user can get template#template_options" do - sign_in @researcher - get "#{org_admin_template_options_path}?plan[org_id]=#{@institution.id}&plan[funder_id]=#{@funder.id}" - assert_response :success - json_body = JSON.parse(@response.body) - assert json_body["templates"].length > 0 - end -end diff --git a/test/functional/orgs_controller_test.rb b/test/functional/orgs_controller_test.rb deleted file mode 100644 index 53fab85..0000000 --- a/test/functional/orgs_controller_test.rb +++ /dev/null @@ -1,66 +0,0 @@ -require 'test_helper' - -class OrgsControllerTest < ActionDispatch::IntegrationTest - - include Devise::Test::IntegrationHelpers - - # TODO: The following methods SHOULD replace the old 'admin_' prefixed methods. The children_org and templates_org - # routes don't even have an endpoint defined in the controller! - # - # SHOULD BE: - # -------------------------------------------------- - # orgs GET /orgs orgs#index - # POST /orgs orgs#create - # org GET /orgs/:id orgs#show - # PATCH /orgs/:id orgs#update - # PUT /orgs/:id orgs#update - # DELETE /orgs/:id orgs#destroy - # - # CURRENT RESULTS OF `rake routes` - # -------------------------------------------------- - # children_org GET /org/admin/:id/children orgs#children - # templates_org GET /org/admin/:id/templates orgs#templates - # admin_show_org GET /org/admin/:id/admin_show orgs#admin_show - # admin_edit_org GET /org/admin/:id/admin_edit orgs#admin_edit - # admin_update_org PUT /org/admin/:id/admin_update orgs#admin_update - - setup do - @test_org = Org.create!(name: 'Testing', abbreviation: 'TST', links: {"org":[]}) - @admin = User.create!(email: "org-admin-controller-test@example.com", firstname: "Org", surname: "Admin", - password: "password123", password_confirmation: "password123", - org: @test_org, accept_terms: true, confirmed_at: Time.zone.now, - perms: Perm.where.not(name: ['admin', 'add_organisations', 'change_org_affiliation', 'grant_api_to_orgs', 'change_org_details'])) - @admin.perms << Perm.find_by(name: 'change_org_details') - @admin.perms << Perm.find_by(name: 'modify_templates') - @admin.perms << Perm.find_by(name: 'modify_guidance') - @admin.perms << Perm.find_by(name: 'grant_permissions') - @admin.save! - end - - # GET /org/admin/:id/admin_edit (admin_edit_org_path) - # ---------------------------------------------------------- - test 'load the edit org page' do - # Should redirect user to the root path if they are not logged in! - get admin_edit_org_path(@test_org) - assert_unauthorized_redirect_to_root_path - - sign_in @admin - get admin_edit_org_path(@test_org) - assert_response :success - end - - # PUT /org/admin/:id/admin_update (admin_update_org_path) - # ---------------------------------------------------------- - test 'update the org' do - params = {name: 'Testing UPDATE', links: {"org": []}} - - # Should redirect user to the root path if they are not logged in! - put admin_update_org_path(@test_org), {org: params} - assert_unauthorized_redirect_to_root_path - - sign_in @admin - put admin_update_org_path(@test_org), {org: params} - assert_response :redirect - assert_equal 'Testing UPDATE', @test_org.reload.name, "expected the record to have been updated" - end -end diff --git a/test/functional/paginable/plans_controller_test.rb b/test/functional/paginable/plans_controller_test.rb deleted file mode 100644 index e811322..0000000 --- a/test/functional/paginable/plans_controller_test.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'test_helper' - -class PlansControllerTest < ActionDispatch::IntegrationTest - include Devise::Test::IntegrationHelpers - setup do - @user = User.find_by(email: 'super_admin@example.com') - #@plans_total = Kaminari.config.default_per_page - #(1..@plans_total+1).each do - # Plan.create(title: 'Test Plan', template: @user.org.templates.first, grant_number: 'Plan12345', - # identifier: '000912', description: 'This is a test plan', - # principal_investigator: 'Foo Bar', principal_investigator_identifier: 'ABC', - # data_contact: 'foo.bar@example.com', visibility: :privately_visible).assign_creator(@user.id) - #end - end - test 'privately_visible action renders layout view for page param ALL' do - sign_in @user - get privately_visible_paginable_plans_path('ALL') - assert_response :success - # Checks the existence of a link with href equals to privately_visible_paginable_plans_path(1) - # assert_select('.paginable-layout .pull-left a[href=?]', privately_visible_paginable_plans_path(1)) - # Checks the existence of a link (e.g. View Less) with data-remote attribute as true (for AJAX requests) - # assert_select('.paginable-layout .pull-left a[data-remote=?]', 'true') - # Checks that does not exist any nav with class pagination in the view rendered (e.g. no pagination) - # assert_select('nav.pagination', { count: 0 }) - end - test 'privately_visible action renders layout view for page param 1' do - sign_in @user - get privately_visible_paginable_plans_path(1) - assert_response :success - #assert_select('.paginable-layout .pull-left a[href=?]', privately_visible_paginable_plans_path('ALL')) - #assert_select('.paginable-layout .pull-left a[data-remote=?]', 'true') - #assert_select('nav.pagination', { count: 1 }) - #assert_select('nav.pagination .page.current', { count: 1, text: '1' }) - end -end - -# assert_select reference at http://www.rubydoc.info/github/rails/rails-dom-testing/Rails/Dom/Testing/Assertions/SelectorAssertions \ No newline at end of file diff --git a/test/functional/plans_controller_test.rb b/test/functional/plans_controller_test.rb deleted file mode 100644 index 4d188b0..0000000 --- a/test/functional/plans_controller_test.rb +++ /dev/null @@ -1,314 +0,0 @@ -require 'test_helper' - -class PlansControllerTest < ActionDispatch::IntegrationTest - - include Devise::Test::IntegrationHelpers - - setup do - # First clear out any existing templates - GuidanceGroup.delete_all - GuidanceGroup.create!(name: "Generic Guidance (provided by the example curation...", org_id: 1, - created_at: "2018-01-03 21:02:14", updated_at: "2018-01-03 21:02:14", - optional_subset: true, published: true) - GuidanceGroup.create!(name: "Government Agency Advice (Funder specific guidance...", org_id: 2, - created_at: "2018-01-03 21:02:14", updated_at: "2018-01-03 21:02:14", - optional_subset: false, published: true) - @org = Org.first - scaffold_plan - @user = @plan.owner - - # This should NOT be unnecessary! Owner should have full access - role = Role.where(user: @user, plan: @plan).first - role.access = 15 - role.save! - end - - # GET /plans (plans_path) - # ---------------------------------------------------------- - test 'load the list of plans page' do - # Should redirect user to the root path if they are not logged in! - get plans_path - assert_unauthorized_redirect_to_root_path - - sign_in @user - - get plans_path - assert_response :success - assert assigns(:plans) - end - - # GET /plans/new (new_plan_path) - # ---------------------------------------------------------- - test 'load the new plan page' do - # Should redirect user to the root path if they are not logged in! - get new_plan_path - assert_unauthorized_redirect_to_root_path - - sign_in @user - - get new_plan_path - assert_response :success - assert assigns(:plan) - assert assigns(:orgs) - assert assigns(:funders) - end - - # POST /plans (plans_path) - # ---------------------------------------------------------- - test "create a new plan" do - params = {plan: {org_id: @template.org.id, template_id: @template.id, title: 'Testing Create'}} - # Should redirect user to the root path if they are not logged in! - post plans_path(format: :js), params - assert_unauthorized_redirect_to_root_path - - sign_in @user - - post plans_path(), params - assert flash[:notice].start_with?('Successfully') && flash[:notice].include?('created') - assert_response :redirect - - new_plan = Plan.last - assert_redirected_to plan_url(new_plan) - assert_equal "Testing Create", new_plan.title, "expected the record to have been created" - - # assert that the default visibility is used when none is specified - assert_equal Rails.application.config.default_plan_visibility, new_plan.visibility, "Expected the plan to have been assigned the default visibility" - end - - # GET /plan/:id (plan_path) - # ---------------------------------------------------------- - test 'show the plan page' do - # Should redirect user to the root path if they are not logged in! - get plan_path(@plan) - assert_unauthorized_redirect_to_root_path - - sign_in @user - get plan_path(@plan) - assert_response :success - assert assigns(:plan) - assert_not assigns(:editing) - assert assigns(:selected_guidance_groups) - end - - # PUT /plan/:id (plan_path) - # ---------------------------------------------------------- - test 'update the plan' do - params = {title: 'Testing UPDATE'} - # Should redirect user to the root path if they are not logged in! - put plan_path(@plan), {plan: params} - assert_unauthorized_redirect_to_root_path - - # User who is does not have access to the plan - sign_in User.first - put plan_path(@plan), {plan: params} - assert_equal _('You are not authorized to perform this action.'), flash[:alert] - assert_response :redirect - assert_redirected_to plans_url - - sign_in @user - - put plan_path(@plan), {plan: params} - assert flash[:notice].start_with?('Successfully') && flash[:notice].include?('saved') - assert_response :redirect - assert_redirected_to overview_plan_path(@plan) - assert assigns(:plan) - assert_equal 'Testing UPDATE', @plan.reload.title, "expected the record to have been updated" - -# TODO: Reactivate this once the validations on the model are in place! - # Invalid object -# put plan_path(@plan), {plan: {title: nil}} -# assert flash[:notice].starts_with?(_('Could not update your')) -# assert_response :success -# assert assigns(:plan) - end - - # POST/plan/:id - # ---------------------------------------------------------- - test 'duplicate a plan' do - # Should redirect user to the root path if they are not logged in! - post duplicate_plan_path(@plan) - assert_unauthorized_redirect_to_root_path - - # User who is does not have access to the plan - sign_in User.first - put plan_path(@plan) - assert_equal _('You are not authorized to perform this action.'), flash[:alert] - assert_response :redirect - assert_redirected_to plans_url - - sign_in @user - post duplicate_plan_path(@plan) - @duplicate_plan = Plan.last - assert flash[:notice].start_with?('Successfully') && flash[:notice].include?('copied') - assert_response :redirect - assert_redirected_to plan_url(@duplicate_plan) - end - - - # DELETE /plan/:id (plan_path) - # ---------------------------------------------------------- - test "delete the plan" do - id = @plan.id - # Should redirect user to the root path if they are not logged in! - delete plan_path(@plan) - assert_unauthorized_redirect_to_root_path - - # User who is does not have access to the plan - sign_in User.first - delete plan_path(@plan) - assert_equal _('You are not authorized to perform this action.'), flash[:alert] - assert_response :redirect - assert_redirected_to plans_url - - sign_in @user - delete plan_path(@plan) - assert flash[:notice].start_with?('Successfully') && flash[:notice].include?('deleted') - assert_response :redirect - assert assigns(:plan) - assert_redirected_to plans_path - assert_raise ActiveRecord::RecordNotFound do - Plan.find(id).nil? - end - end - - # PUT /plans/:id/update_guidance_choices (update_guidance_choices_plan_path) - # ---------------------------------------------------------- - test "update the selected guidance" do - ids = [GuidanceGroup.first.id, GuidanceGroup.last.id] - - # Make sure the guidance is attached to the template first so that its a valid selection! - q = @template.phases.first.sections.first.questions.first - - #this tricky bit is needed to set guidances to newly created Guidance Groups - Guidance.update_all( guidance_group_id: GuidanceGroup.first.id) - Guidance.last.update!(guidance_group_id: GuidanceGroup.last.id) - - q.themes << GuidanceGroup.first.guidances.first.themes.first - q.themes << GuidanceGroup.last.guidances.first.themes.first - q.save - - put plan_path(@plan), {plan: {}, guidance_group_ids: ids} - assert_unauthorized_redirect_to_root_path - - # User who does not have access to the plan - sign_in User.first - put plan_path(@plan), {plan: {}, guidance_group_ids: ids} - assert_equal _('You are not authorized to perform this action.'), flash[:alert] - assert_response :redirect - assert_redirected_to plans_url - - sign_in @user - put plan_path(@plan), {plan: {id: @plan.id}, guidance_group_ids: ids} - assert_response :redirect - assert_redirected_to overview_plan_path(@plan) - - @plan.reload - ggs = @plan.guidance_groups.ids - - assert ggs.include?(GuidanceGroup.first.id), "expected the plan to have the first GuidanceGroup selected" - assert ggs.include?(GuidanceGroup.last.id), "expected the plan to have the last GuidanceGroup selected" - end - - # GET /plans/:id/share (share_plan_path) - # ---------------------------------------------------------- - test "get the share plan page" do - # Should redirect user to the root path if they are not logged in! - get share_plan_path(@plan) - assert_unauthorized_redirect_to_root_path - - sign_in @user - get share_plan_path(@plan) - assert_response :success - assert assigns(:plan) - end - - # GET /plans/:id/status(format: :json) (status_plan_path) - # ---------------------------------------------------------- - test "get the plan status" do - # Should redirect user to the root path if they are not logged in! - get status_plan_path(@plan, format: :json) - assert_unauthorized_redirect_to_root_path - - sign_in @user - get status_plan_path(@plan, format: :json) - assert_response :success - assert assigns(:plan) - end - - # GET /plans/:id/answer(format: :json) (answer_plan_path) - # ---------------------------------------------------------- - test "get the answer to the specified question for the plan" do - # Should redirect user to the root path if they are not logged in! - get answer_plan_path(@plan, format: :json) - assert_unauthorized_redirect_to_root_path - - sign_in @user - get answer_plan_path(@plan, format: :json) - assert_response :success - assert assigns(:plan) - end - - # GET /plans/:id/export (export_plan_path) - # ---------------------------------------------------------- - test "export the plan" do - Answer.create!(plan: @plan, question: @plan.template.phases.first.sections.first.questions.last, text: 'Test Answer') - - # Should redirect user to the root path if they are not logged in! - get export_plan_path(@plan), {'format': 'pdf'} - assert_unauthorized_redirect_to_root_path - - export_params = {"utf8"=>"✓", - "phase_id"=>"5470", - "export"=>{"project_details"=>"true", - "question_headings"=>"true", - "unanswered_questions"=>"true", - "formatting"=>{"font_face"=>"Arial, - Helvetica, - Sans-Serif", - "font_size"=>"12", - "margin"=>{"top"=>"20", - "bottom"=>"20", - "left"=>"20", - "right"=>"20"}}}, - "format"=>"docx", - "commit"=>"Download Plan", - "id"=>"18009"} - sign_in @user - get export_plan_path(@plan), export_params - assert_response :success - assert assigns(:plan) - - # TODO: We need some better tests here to check the different formats! - end - - # GET /plans/:id/download (download_plan_path) - # ---------------------------------------------------------- - test "show the download plan page" do - # Should redirect user to the root path if they are not logged in! - get download_plan_path(@plan) - assert_unauthorized_redirect_to_root_path - - sign_in @user - get download_plan_path(@plan) - assert_response :success - assert assigns(:plan) - end - - test 'overview action responds redirect when plan does not exist' do - sign_in @user - get overview_plan_path(id: 'foo') - assert_response(:redirect) - assert_equal(_('There is no plan associated with id %{id}') %{ :id => 'foo' }, flash[:alert]) - end - - test 'overview action responds redirect when user does not have readable permissions on the plan' do - get overview_plan_path(@plan) - assert_response(:redirect) - end - - test 'overview actions responds success when user has readable permissions on the plan' do - sign_in @user - get overview_plan_path(@plan) - assert_response(:success) - end -end diff --git a/test/functional/public_pages_controller_test.rb b/test/functional/public_pages_controller_test.rb deleted file mode 100644 index 9531032..0000000 --- a/test/functional/public_pages_controller_test.rb +++ /dev/null @@ -1,109 +0,0 @@ -require 'test_helper' - -class PublicPagesControllerTest < ActionDispatch::IntegrationTest - - include Devise::Test::IntegrationHelpers - - setup do - @org = Org.first - scaffold_plan - - @plan.visibility = :publicly_visible - @plan.save - - @non_public_plans = [] - [:privately_visible, :organisationally_visible, :is_test].each do |vis| - @non_public_plans << Plan.create(template: @template, title: "#{vis} Plan", visibility: vis, - roles: [Role.new(user: User.last, creator: true)]) - end - - @inst_tmplt = Template.create!(title: 'Inst template', org: Org.institution.first, archived: false, published: true) - @dflt_tmplt = Template.create!(title: 'Dflt template', org: Org.managing_orgs.first, archived: false, published: true) - @fndr_tmplt = Template.create!(title: 'Fndr template', org: Org.funder.first, archived: false, published: true) - - [@inst_tmplt, @dflt_tmplt, @fndr_tmplt].each do |t| - t.published = true - t.is_default = true if t == @dflt_tmplt - t.visibility = t.title != 'Inst template' ? Template.visibilities[:publicly_visible] : Template.visibilities[:organisationally_visible] - t.save! - end - - @user = User.first - end - - # GET /public_plans (public_plans_path) - # ---------------------------------------------------------- - test 'load the list of public plans page' do - # Verify that public plans are visible when not logged in and that non-public plans are NOT in the list - get public_plans_path - assert_response :success - assert assigns(:plans) - assert @response.body.include?(plan_export_path(@plan)), "expected to see the plan download link when NOT logged in" - @non_public_plans.each do |plan| - assert_not @response.body.include?(plan_export_path(plan)), "expected to NOT see the on-public plan download link when NOT logged in" - end - - # Verify the same results are received when the user is logged in - sign_in @user - get public_plans_path - assert_response :success - assert assigns(:plans) - assert @response.body.include?(plan_export_path(@plan)), "expected to see the plan download link when NOT logged in" - @non_public_plans.each do |plan| - assert_not @response.body.include?(plan_export_path(plan)), "expected to NOT see the on-public plan download link when NOT logged in" - end - end - -# TODO: Need to install the wkhtmltopdf library on Travis for this to work! - # GET /plan_export/:id (plan_export_path) - # ---------------------------------------------------------- - test 'export a public plan' do -# get plan_export_path(@plan, format: :pdf) -# assert_response :success - -# @non_public_plans.each do |p| -# get plan_export_path(p, format: :pdf) -# assert_response :redirect -# assert_equal "You need to sign in or sign up before continuing.", flash[:alert] -# assert_redirected_to root_path -# end - end - - # GET /public_templates (public_templates_path) - # ---------------------------------------------------------- - test 'load the list of public templates page' do - # Verify that public templates are visible when not logged in and that non-funder and non-default - # templates are NOT in the list - get public_templates_path - assert_response :success - assert assigns(:templates) - assert @response.body.include?(template_export_path(@fndr_tmplt.family_id)), "expected to see the funder template download link when NOT logged in" - assert @response.body.include?(template_export_path(@dflt_tmplt.family_id)), "expected to see the default template download link when NOT logged in" - assert_not @response.body.include?(template_export_path(@inst_tmplt.family_id)), "expected to NOT see the institution template download link when NOT logged in" - - # Verify the same results are received when the user is logged in - sign_in @user - get public_templates_path - assert_response :success - assert assigns(:templates) - assert @response.body.include?(template_export_path(@fndr_tmplt.family_id)), "expected to see the funder template download link when NOT logged in" - assert @response.body.include?(template_export_path(@dflt_tmplt.family_id)), "expected to see the default template download link when NOT logged in" - assert_not @response.body.include?(template_export_path(@inst_tmplt.family_id)), "expected to NOT see the institution template download link when NOT logged in" - end - -# TODO: Need to install the wkhtmltopdf library on Travis for this to work! - # GET /template_export/:family_id (template_export_path) - # ---------------------------------------------------------- - test 'export a public template' do -# get template_export_path(@fndr_tmplt.family_id, format: :pdf) -# assert_response :success - -# get template_export_path(@dflt_tmplt.family_id, format: :pdf) -# assert_response :success - -# get template_export_path(@inst_tmplt.family_id, format: :pdf) -# assert_response :redirect -# assert_equal "You need to sign in or sign up before continuing.", flash[:alert] -# assert_redirected_to root_path - end -end \ No newline at end of file diff --git a/test/functional/registrations_controller_test.rb b/test/functional/registrations_controller_test.rb deleted file mode 100644 index 771f498..0000000 --- a/test/functional/registrations_controller_test.rb +++ /dev/null @@ -1,154 +0,0 @@ -require 'test_helper' - -class RegistrationsControllerTest < ActionDispatch::IntegrationTest - include Devise::Test::IntegrationHelpers - - setup do - @user = User.first - end - - # # ------------------------------------------------------------- - test "sign up form loads" do - get new_user_registration_path - - assert_response :success - assert_not '#new_user'.nil? - end - - # ------------------------------------------------------------- - test "user receives proper error messaging if they have not accepted terms" do - post user_registration_path, {user: {accept_terms: nil}} - - assert_response :redirect - follow_redirect! - - assert_response :success - assert_equal _('You must accept the terms and conditions to register.'), flash[:alert] - end - - test "user receives proper error messaging if they have not select an org from the list or entered their organisation name" do - post user_registration_path, {user: {accept_terms: "on"}} - assert_response :redirect - follow_redirect! - - assert_response :success - assert_equal _('Please select an organisation from the list, or enter your organisation\'s name.'), flash[:alert] - end - - # ------------------------------------------------------------- - test "user receives proper error messaging if they have not provided a valid email and/or password" do - org_id = Org.first.id - [ {}, - {email: 'foo.bar@test.org' }, # No Password or Confirmation - {password: 'test12345' }, # No Email - {password: 'test12345', password_confirmation: 'test12345'}, # No Email - {email: 'foo.bar@test.org', password: 'test' }, # Password is too short - {email: 'foo.bar$test.org', password: 'test12345' } # invalid email - ].each do |params| - post user_registration_path, {user: { accept_terms: "on", org_id: org_id }.merge(params)} - - assert_response :redirect - follow_redirect! - - assert_response :success - assert_equal _('Error processing registration. Please check that you have entered a valid email address and that your chosen password is at least 8 characters long.'), flash[:alert] - end - end - - # ------------------------------------------------------------- - test "user is able to register and is auto-logged in and brought to profile page" do - form = {accept_terms: "on", - email: 'foo.bar@test.org', - password: 'Test12345', - org_id: Org.first.id } - post user_registration_path, {user: form} - - assert_response :redirect - assert_redirected_to root_url - - follow_redirect! - assert_response :redirect - assert_redirected_to plans_path - end - - # ------------------------------------------------------------- - test "edit profile page loads when logged in" do - sign_in @user - - get edit_user_registration_path - - assert_response :success - assert_select 'main h1', _('Edit profile') - - end - - # ------------------------------------------------------------- - test "user is able to edit their profile" do - sign_in @user - - # Change name - put user_registration_path, {user: {email: @user.email, firstname: 'Testing', surname: 'UPDATE', org_id: Org.first.id}} - assert flash[:notice].start_with?('Successfully') - assert_response :redirect - assert_redirected_to "#{edit_user_registration_url}\#personal-details" - - # Change email but didn't provide password - put user_registration_path, {user: {email: 'something@else.org', firstname: @user.firstname, surname: @user.surname, org_id: Org.first.id}} - assert_response :success - assert_equal _('Please enter your password to change email address.'), flash[:alert] - -# TODO: These don't seem to be behaving as expected. There were several typos in the controller that have been fixed -# (succesfully_updated vs successfully_updated) -=begin - # Change email - put user_registration_path, {user: {email: 'something@else.org', current_password: 'password123', firstname: @user.firstname, surname: @user.surname}} - assert_equal _('Details successfully updated.'), flash[:notice] - assert_response :redirect - assert_redirected_to edit_user_registration_url - - # Change password but neglected to provide the password - put user_registration_path, {user: {password_confirmation: 'testing123', current_password: 'password123', firstname: @user.firstname, surname: @user.surname, email: @user.email}} - assert_response :success - assert flash[:notice].starts_with?(_('Unable to save your changes.')) - - # Change password but neglected to provide the password confirmation - put user_registration_path, {user: {password: 'testing123', current_password: 'password123', firstname: @user.firstname, surname: @user.surname, email: @user.email}} - assert_equal _('Please enter a password confirmation'), flash[:notice] - assert_response :success - - # Change password but the password and confirmation do not match - put user_registration_path, {user: {password: 'test123', password_confirmation: 'testing123', current_password: 'password123', firstname: @user.firstname, surname: @user.surname, email: @user.email}} - assert_equal _('Password and comfirmation must match'), flash[:notice] - assert_response :success - - # Change password - put user_registration_path, {user: {password: 'testing123', password_confirmation: 'testing123', current_password: 'password123', firstname: @user.firstname, surname: @user.surname, email: @user.email}} - assert flash[:notice].starts_with?(_('Could not update your')) - assert_response :success -=end - - end - -# INVALID AUTH REROUTING CHECKS - # ------------------------------------------------------------- - test "sign up form does NOT load if already logged in" do - sign_in @user - get new_user_registration_path - - assert_authorized_redirect_to_plans_page - end - - # ------------------------------------------------------------- - test "edit profile page does NOT load if not logged in" do - get edit_user_registration_path - - assert_unauthorized_redirect_to_root_path - end - - # ------------------------------------------------------------- - test "can NOT edit profile if not logged in" do - post user_registration_path, {user: {firstname: 'Foo', surname: 'Bar'}} - - assert_unauthorized_redirect_to_root_path - end -end \ No newline at end of file diff --git a/test/functional/roles_controller_test.rb b/test/functional/roles_controller_test.rb deleted file mode 100644 index 7d2f9f0..0000000 --- a/test/functional/roles_controller_test.rb +++ /dev/null @@ -1,126 +0,0 @@ -require 'test_helper' - -class RolesControllerTest < ActionDispatch::IntegrationTest - - include Devise::Test::IntegrationHelpers - - setup do - scaffold_plan - scaffold_org_admin(@plan.template.org) - - # This should NOT be unnecessary! Owner should have full access - @plan.roles << Role.create(user: @user, plan: @plan, access: 15) - - end - -# TODO: Cleanup routes for this one. The controller currently only responds to create, update, destroy - -# CURRENT RESULTS OF `rake routes` -# -------------------------------------------------- -# roles POST /roles roles#create -# role PATCH /roles/:id roles#update -# PUT /roles/:id roles#update -# DELETE /roles/:id roles#destroy - -# POST /roles (roles_path) - # ---------------------------------------------------------- - test "create a new role" do - - params = {plan_id: @plan.id, access_level: 4} - - # Should redirect user to the root path if they are not logged in! - post roles_path, {role: params} - assert_unauthorized_redirect_to_root_path - - sign_in @user - - # Known user - @invitee = User.where.not(id: [@plan.owner.id, @user.id]).first - post roles_path, {user: @invitee.email, role: params} - assert_equal _('Plan shared with %{email}.') % {email: @invitee.email}, flash[:notice] - assert_response :redirect - assert_redirected_to share_plan_path(@plan) - assert_equal @invitee.id, Role.last.user_id, "expected the record to have been created!" - assert assigns(:role) - - # Share to already invited user - post roles_path, {user: @invitee.email, role: params} - assert_equal _('Plan is already shared with %{email}.') % {email: @invitee.email}, flash[:notice] - assert_response :redirect - assert_redirected_to share_plan_path(@plan) - assert_equal @invitee.id, Role.last.user_id, "expected no record to have been created!" - assert assigns(:role) - - # Unknown user - post roles_path, {user: 'unknown_user@org.org', role: params} - assert_equal _('Invitation to unknown_user@org.org issued successfully. \nPlan shared with unknown_user@org.org.'), flash[:notice] - assert_response :redirect - assert_redirected_to share_plan_path(@plan) - assert_equal User.find_by(email:'unknown_user@org.org').id, Role.last.user_id, "expected the record to have been created!" - assert assigns(:role) - - # Invite owner - @invitee = User.find_by(id: @plan.owner.id) - post roles_path, {user: @invitee.email, role: params} - assert_equal _('Cannot share plan with %{email} since that email matches with the owner of the plan.') % {email: @invitee.email}, flash[:notice] - assert_response :redirect - assert_redirected_to share_plan_path(@plan) - assert_not_equal @invitee.id, Role.last.user_id, "expected no record to have been created!" - assert assigns(:role) - - # Missing email - post roles_path, {role: {plan_id: @plan.id, access_level: 4}} - assert_equal _('Please enter an email address'), flash[:notice] - assert_response :redirect - assert_redirected_to share_plan_path(@plan) - assert assigns(:role) - end - - # PUT /role/:id (role_path) - # ---------------------------------------------------------- - test "update the role" do - @invitee = User.last - role = Role.create(user: @invitee, plan: @plan, access: 1) - params = {access_level: 2} - - # Should redirect user to the root path if they are not logged in! - put role_path(role), {role: params} - assert_unauthorized_redirect_to_root_path - - sign_in @user - - # Valid save - put role_path(role, format: :json), {role: params} - assert_equal 13, role.reload.access, "expected the record to have been updated" - -# TODO: Role should require a user, plan and an access level :/ - # Invalid save -# put role_path(role), {role: {user: nil}} -# assert flash[:notice].starts_with?(_('Unable to save your changes.')) -# assert_response :redirect -# assert_redirected_to share_plan_path(@plan) -# assert assigns(:role) - end - - # DELETE /role/:id (role_path) - # ---------------------------------------------------------- - test "delete the section" do - @invitee = User.last - role = Role.create(user: @invitee, plan: @plan, access: 1) - - # Should redirect user to the root path if they are not logged in! - delete role_path(role) - assert_unauthorized_redirect_to_root_path - - sign_in @user - - delete role_path(role) - assert_equal _('Access removed'), flash[:notice] - assert_response :redirect - assert_redirected_to share_plan_path(@plan) - assert_raise ActiveRecord::RecordNotFound do - Role.find(role.id).nil? - end - end - -end diff --git a/test/functional/sessions_controller_test.rb b/test/functional/sessions_controller_test.rb deleted file mode 100644 index cc9e3dc..0000000 --- a/test/functional/sessions_controller_test.rb +++ /dev/null @@ -1,63 +0,0 @@ -require 'test_helper' - -class SessionsControllerTest < ActionDispatch::IntegrationTest - - include Warden::Test::Helpers - - # CURRENT RESULTS OF `rake routes` - # -------------------------------------------------- - # new_user_session GET /users/sign_in sessions#new - # user_session POST /users/sign_in sessions#create - # destroy_user_session DELETE /users/sign_out sessions#destroy - - setup do - @user = User.first - end - - # POST /users/sign_in (user_session_path) - # ---------------------------------------------------------- - test "existing user's language setting is stored in the session and FastGettext" do - @user.language = Language.find_by(abbreviation: 'de') - @user.save! - post user_session_path, {user: {email: @user.email}} - assert_equal 'de', session[:locale], "expected the existing user's locale to have been set in the session" - assert_response :redirect - assert_redirected_to root_path - end - - # POST /users/sign_in (user_session_path) - # ---------------------------------------------------------- - test "unknown user's session[:locale] set to FastGettext.default_locale" do - post user_session_path, {user: {email: 'testing.session@example.org'}} - assert_nil session[:locale], "expected the new user's locale to be empty" - assert_equal FastGettext.default_locale, FastGettext.locale, "expected the FastGettext to use the default locale" - assert_response :redirect - assert_redirected_to root_path - end - - # POST /users/sign_in (user_session_path) - # ---------------------------------------------------------- - test "existing user's Shibboleth id is captured" do - Warden.on_next_request do |proxy| - proxy.raw_session[:"devise.shibboleth_data"] = {uid: 'abcdefg'} - end - post user_session_path, {user: {email: @user.email}, shibboleth_data: {uid: 'abcdefg'}} - assert_response :redirect - assert_redirected_to root_path - assert_equal 'abcdefg', @user.reload.shibboleth_id, "expected the existing user's shib id to have been set" - end - - # DELETE /users/sign_in (destroy_user_session_path) - # ---------------------------------------------------------- - test "delete the user session" do - delete destroy_user_session_path - assert_nil session[:locale], "expected the locale to have been deleted from the session" - assert_response :redirect - if Rails.application.config.shibboleth_enabled - assert_redirected_to Rails.application.config.shibboleth_logout_url + root_url - else - assert_redirected_to root_path - end - end - -end diff --git a/test/functional/static_pages_controller_test.rb b/test/functional/static_pages_controller_test.rb deleted file mode 100644 index a9d6df7..0000000 --- a/test/functional/static_pages_controller_test.rb +++ /dev/null @@ -1,61 +0,0 @@ -require 'test_helper' - -class StaticPagesControllerTest < ActionDispatch::IntegrationTest - - include Devise::Test::IntegrationHelpers - - setup do - @public_plan = Plan.create!({title: 'Public Test Project', - template: Template.first, - visibility: :publicly_visible}) - end - - # ---------------------------------------------------------- - test "should only return plans with public visibility" do -# get public_plans_path(locale: I18n.locale) - -# assert_response :success -# assert_not_nil assigns(:plans) - -# all_public = true - -# assigns(:plans).each do |plan| -# all_public = false unless plan.publicly_visible? -# end - -# assert all_public, "expected all of the plans to have public visibility!" - end - - # ---------------------------------------------------------- - test "should export the publicly available plan" do - -# get public_export_path(locale: I18n.locale, id: @project) - - # Should be redirected to the plans controller's export function -# assert_redirected_to "#{export_project_plan_path(@project, @project.plans.first)}", "expected to be redirected to the exported plan" -# follow_redirect! - -# assert_redirected_to "blah" -# assert_response :success -# assert_equal Mime::PDF, response.content_type - end - - # ---------------------------------------------------------- - test "should NOT export a non-public plan to unauthorized users" do - # Set the is_public flag to false and try to access it when not logged in -# @public_plan.visibility = :privately_visible -# @public_plan.save! - -# get public_export_path(locale: I18n.locale, id: @public_plan) - -# assert_redirected_to "#{public_plans_path}", "expected to be redirected to the home page!" -# assert_equal _('This account does not have access to that plan.'), flash[:notice], "Expected an unauthorized message when trying to export a plan (via the public_export route) when the plan is not actually public" - -# sign_in User.first - -# get public_export_path(locale: I18n.locale, id: @public_plan) - -# assert_redirected_to "#{public_plans_path}", "expected to be redirected to the home page!" -# assert_equal _('This account does not have access to that plan.'), flash[:notice], "Expected an unauthorized message when trying to export a plan (via the public_export route) when the plan is not actually public" - end -end \ No newline at end of file diff --git a/test/functional/super_admin/orgs_controller_test.rb b/test/functional/super_admin/orgs_controller_test.rb deleted file mode 100644 index 0a8d623..0000000 --- a/test/functional/super_admin/orgs_controller_test.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'test_helper' - -class OrgsControllerTest < ActionDispatch::IntegrationTest - include Devise::Test::IntegrationHelpers - - setup do - @org = Org.first - @super_admin = User.find_by(email: 'super_admin@example.com') - end - - test 'unauthorized user cannot access index page' do - get super_admin_orgs_path - assert_unauthorized_redirect_to_root_path - - sign_in User.where.not(id: @super_admin.id).first - get super_admin_orgs_path - assert_authorized_redirect_to_plans_page - end - - test 'super admin can access index page' do - sign_in @super_admin - get super_admin_orgs_path - assert_response :success - end - - test 'unauthorized user cannot access new org page' do - get new_super_admin_org_path - assert_unauthorized_redirect_to_root_path - - sign_in User.where.not(id: @super_admin.id).first - get new_super_admin_org_path - assert_authorized_redirect_to_plans_page - end - - test 'super admin can access new org page' do - sign_in @super_admin - get new_super_admin_org_path - assert_response :success - end - - test 'unauthorized user cannot create an org' do - params = {name: 'Test Org', abbreviation: 'ABCD'} - post super_admin_orgs_path, {org: params} - assert_unauthorized_redirect_to_root_path - - sign_in User.where.not(id: @super_admin.id).first - post super_admin_orgs_path, {org: params} - assert_authorized_redirect_to_plans_page - end - - test 'super admin can create an org' do - params = {name: 'Test Org create', abbreviation: 'ABCD'} - sign_in @super_admin - - post super_admin_orgs_path, {org: params} - assert_response :redirect - end - - test 'unauthorized user cannot destroy an org' do - delete super_admin_org_path(@org) - assert_unauthorized_redirect_to_root_path - - sign_in User.where.not(id: @super_admin.id).first - delete super_admin_org_path(@org) - assert_authorized_redirect_to_plans_page - end - - test 'super admin can destroy an org' do - org = Org.create!(name: 'Testing destroy', abbreviation: 'TST', links: {"org":[]}) - sign_in @super_admin - - delete super_admin_org_path(org) - assert_response :redirect - assert_redirected_to super_admin_orgs_path - assert_not flash[:notice].nil? - end -end diff --git a/test/functional/super_admin/themes_controller.test.rb b/test/functional/super_admin/themes_controller.test.rb deleted file mode 100644 index fa646d5..0000000 --- a/test/functional/super_admin/themes_controller.test.rb +++ /dev/null @@ -1,117 +0,0 @@ -require 'test_helper' - -class ThemesControllerTest < ActionDispatch::IntegrationTest - include Devise::Test::IntegrationHelpers - - setup do - @user = User.find_by(email: 'super_admin@example.com') - end - - # index action tests - test 'index action responds redirect when user is not super_admin' do - get(super_admin_themes_path) - assert_response :redirect - end - test 'index action responds success when user is super_admin' do - sign_in @user - get(super_admin_themes_path) - assert_response :ok - end - - # new action tests - test 'new action responds redirect when user is not super_admin' do - get(new_super_admin_theme_path) - assert_response :redirect - end - test 'new action responds success when user is super_admin' do - sign_in @user - get(new_super_admin_theme_path) - assert_response :ok - end - - # create action tests - test 'create action responds redirect when user is not super_admin' do - post(super_admin_themes_path({ theme: { title: 'foo', description: 'bar' }})) - assert_response :redirect - end - test 'create action responds redirect with flash alert for ActionController::ParameterMissing' do - sign_in @user - post(super_admin_themes_path({ foo: 'bar' })) - assert_response :redirect - assert_equal(_('Unable to save since theme parameter is missing'), flash[:alert]) - end - test 'create action responds redirect with flash alert for ActiveRecord::RecordInvalid' do - sign_in @user - post(super_admin_themes_path({ theme: { description: 'bar' }})) - assert_response :redirect - assert_equal('unable to save your changes', flash[:alert]) - end - test 'create action responds redirect with flash notice' do - sign_in @user - post(super_admin_themes_path({ theme: { title: 'foo', description: 'bar' }})) - assert_response :redirect - assert_equal(_('Theme created successfully'), flash[:notice]) - end - - # edit action tests - test 'edit action responds redirect when user is not super_admin' do - get(edit_super_admin_theme_path(id: Theme.first.id)) - assert_response :redirect - end - test 'edit action responds redirect when theme id does not exist' do - sign_in @user - get(edit_super_admin_theme_path(id: 'foo')) - assert_response :redirect - assert_equal(_('There is no theme associated with id %{id}') % { :id => 'foo'}, flash[:alert]) - end - test 'edit action responds success when user is super_admin' do - sign_in @user - get(edit_super_admin_theme_path(id: Theme.first.id)) - assert_response :ok - end - # update action tests - test 'update action responds redirect when user is not super_admin' do - put(super_admin_theme_path({ id: Theme.first.id, theme: { title: 'foo', description: 'bar' }})) - assert_response :redirect - end - test 'update action responds redirect with flash alert for ActionController::ParameterMissing' do - sign_in @user - put(super_admin_theme_path({ id: Theme.first.id })) - assert_response :redirect - assert_equal(_('Unable to save since theme parameter is missing'), flash[:alert]) - end - test 'update action responds redirect with flash alert for ActiveRecord::RecordInvalid' do - sign_in @user - put(super_admin_theme_path({ id: Theme.first.id, theme: { title: '', description: 'bar' }})) - assert_response :redirect - assert_equal('unable to save your changes', flash[:alert]) - end - test 'update action responds redirect when theme id does not exist' do - sign_in @user - put(super_admin_theme_path({ id: 'foo', theme: { title: 'bar', description: 'foobar' }})) - assert_response :redirect - assert_equal(_('There is no theme associated with id %{id}') % { :id => 'foo'}, flash[:alert]) - end - test 'update action responds redirect with flash notice' do - sign_in @user - put(super_admin_theme_path({ id: Theme.first.id, theme: { title: 'foo', description: 'bar' }})) - assert_response :redirect - assert_equal(_('Theme updated successfully'), flash[:notice]) - end - test 'destroy action responds redirect when user is not super_admin' do - delete(super_admin_theme_path({ id: Theme.first.id })) - assert_response :redirect - end - test 'destroy action responds redirect when theme id does not exist' do - sign_in @user - delete(super_admin_theme_path({ id: 'foo' })) - assert_response :redirect - assert_equal(_('There is no theme associated with id %{id}') % { :id => 'foo' }, flash[:alert]) - end - test 'destroy action responds redirect with flash notice' do - sign_in @user - delete(super_admin_theme_path({ id: Theme.first.id })) - assert_response :redirect - assert_equal(_('Successfully deleted your theme'), flash[:notice]) - end -end \ No newline at end of file diff --git a/test/functional/super_admin/users_controller_test.rb b/test/functional/super_admin/users_controller_test.rb deleted file mode 100644 index 0aa21bc..0000000 --- a/test/functional/super_admin/users_controller_test.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'test_helper' - -class UsersControllerTest < ActionDispatch::IntegrationTest - include Devise::Test::IntegrationHelpers - - setup do - @user = User.create!(email: "super-admin-user-test@example.com", - firstname: "Testing", surname: "User", - password: "password123", password_confirmation: "password123", - org: Org.last, accept_terms: true, confirmed_at: Time.zone.now) - @super_admin = User.find_by(email: 'super_admin@example.com') - end - - test 'unauthorized user cannot access edit user page' do - get edit_super_admin_user_path(@user) - assert_unauthorized_redirect_to_root_path - - sign_in @user - get edit_super_admin_user_path(@user) - assert_authorized_redirect_to_plans_page - end - - test 'super admin can access edit user page' do - sign_in @super_admin - get edit_super_admin_user_path(@user) - assert_response :success - end - - test 'unauthorized user cannot edit a user' do - params = { firstname: 'Foo', surname: 'Bar' } - put super_admin_user_path(@user), { user: params } - assert_unauthorized_redirect_to_root_path - - sign_in @user - put super_admin_user_path(@user), { user: params } - assert_authorized_redirect_to_plans_page - end - - test 'super admin can edit a user' do - params = { firstname: 'Foo', surname: 'Bar' } - sign_in @super_admin - put super_admin_user_path(@user), { user: params } - assert_response :redirect - @user.reload - assert_equal 'Foo', @user.firstname, "expected the User's firstname to have been updated" - end - -end diff --git a/test/functional/user_identifiers_controller_test.rb b/test/functional/user_identifiers_controller_test.rb deleted file mode 100644 index 2488373..0000000 --- a/test/functional/user_identifiers_controller_test.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'test_helper' - -class UserIdentifiersControllerTest < ActionDispatch::IntegrationTest - - include Devise::Test::IntegrationHelpers - - setup do - @user = User.first - end - -# CURRENT RESULTS OF `rake routes` -# -------------------------------------------------- -# destroy_user_identifier DELETE /users/identifiers/:id user_identifiers#destroy - - - # DELETE /users/identifiers/:id (destroy_user_identifier_path) - # ---------------------------------------------------------- - test "delete the section" do - ui = UserIdentifier.create(user: @user, identifier_scheme: IdentifierScheme.first, identifier: 'TESTING') - - # Should redirect user to the root path if they are not logged in! - delete destroy_user_identifier_path(ui) - assert_unauthorized_redirect_to_root_path - - sign_in @user - - delete destroy_user_identifier_path(ui) - assert flash[:notice].start_with?(_('Successfully unlinked your account from')), "expected the success message" - assert_response :redirect - assert_redirected_to edit_user_registration_path - assert_raise ActiveRecord::RecordNotFound do - UserIdentifier.find(ui.id).nil? - end - end - -end \ No newline at end of file diff --git a/test/functional/users/omniauth_callbacks_controller_test.rb b/test/functional/users/omniauth_callbacks_controller_test.rb deleted file mode 100644 index fe6bce2..0000000 --- a/test/functional/users/omniauth_callbacks_controller_test.rb +++ /dev/null @@ -1,105 +0,0 @@ -class OmniauthCallbacksControllerTest < ActionDispatch::IntegrationTest - include Devise::Test::IntegrationHelpers - - setup do - @schemes = IdentifierScheme.all - @user = User.first - - @callback_uris = {} - - # Stub out omniauth provider responses - OmniAuth.config.test_mode = true - - @schemes.each do |scheme| - @callback_uris[scheme.name] = Rails.application.routes.url_helpers.send( - "user_#{scheme.name.downcase}_omniauth_callback_path") - - OmniAuth.config.mock_auth[:"#{scheme.name.downcase}"] = OmniAuth::AuthHash.new({ - :provider => "#{scheme.name.downcase}", - :uid => 'foo:bar' - }) - end - end - - # ------------------------------------------------------------- - test "User is not signed in and valid OAuth2 response does not match a User record in DB: should redirect to registration page" do - @schemes.each do |scheme| - post @callback_uris[scheme.name], locale: FastGettext.locale - - assert @response.redirect_url.include?(new_user_registration_url), "Expected a redirect to the registration page when the user is not logged in and we received a valid callback from #{scheme.name}" - - # make sure that the omniauth identifier is a hidden field on the registration page - assert_not "#user_identifiers[#{scheme.name}]".nil? - end - end - - # ------------------------------------------------------------- - test "User is not signed in and valid OAuth2 login matches a User record in the DB: should auto-signin and redirect to root page" do - - @schemes.each do |scheme| - @user.firstname = 'Tester' - @user.surname = 'MacTesting' - @user.user_identifiers << UserIdentifier.new(identifier_scheme: scheme, - identifier: "foo:bar") - @user.save! - post @callback_uris[scheme.name] - - ### Until ORCID login becomes supported. - if scheme.name == 'shibboleth' - assert [I18n.t('devise.omniauth_callbacks.user.success').gsub('%{kind}', scheme.description).downcase, - I18n.t('devise.omniauth_callbacks.success').gsub('%{kind}', scheme.description).downcase].include?(flash[:notice].downcase), "Expected a success message when simulating a valid callback from #{scheme.name}" - assert @response.redirect_url.include?(root_url), "Expected a redirect to the root page, #{root_url}, when omniauth returns with a valid identifier!" - else - assert_equal _('Successfully signed in'), flash[:notice], "Expected a success message when simulating a valid callback from #{scheme.name}" - assert @response.redirect_url.include?(new_user_registration_url), "Expected a redirect to the registration page when the user is not logged in and we received a valid callback from #{scheme.name}" - assert_not "#user_identifiers[#{scheme.name}]".nil? - end - end - end - - # ------------------------------------------------------------- - test "User is signed in and valid OAuth2 login does not match the current user's record in the DB: should attach the identifier to the user and redirect to the edit profile page" do - - @schemes.each do |scheme| - sign_in @user - - post @callback_uris[scheme.name] - - assert_equal _("Your account has been successfully linked to %{scheme}.") % { scheme: scheme.description }, flash[:notice], "Expected a success message when simulating a valid callback from #{scheme.name}" - - assert_redirected_to "#{edit_user_registration_path}", "Expected a redirect to the edit profile page, #{edit_user_registration_path}, when omniauth returns with a valid identifier for a user that is already signed in!" - - # reload the user record and make sure the omniauth value was attached to their record - usr = User.find(@user.id) - assert_equal usr.user_identifiers.find_by(identifier_scheme: scheme).identifier, 'foo:bar' - end - end - - # ------------------------------------------------------------- - test "An omniauth identifier cannot be tied to multiple accounts" do - new_account = User.create!(firstname: 'Tester', surname: 'Duplicate', - email: 'tester.duplicate@somewhereelse.org', - org: Org.first, accept_terms: true, - password: "password123", password_confirmation: "password123") - - @schemes.each do |scheme| - @user.user_identifiers << UserIdentifier.new(identifier_scheme: scheme, - identifier: "foo:bar") - sign_in new_account - - post @callback_uris[scheme.name] - - assert_equal _("The current #{scheme.description} iD has been already linked to a user with email #{@user.email}"), flash[:alert], "Expected that we could not attach #{scheme.name} to an account if it has already been associated with another account" - - assert_redirected_to "#{edit_user_registration_path}", "Expected a redirect to the edit profile page, #{edit_user_registration_path}, when omniauth returns with a valid identifier for a user that is already signed in!" - - # reload the user record and make sure the omniauth value NOT attached to their record - # and still associated with the original account - @user.reload - assert_equal @user.user_identifiers.find_by(identifier_scheme: scheme).identifier, 'foo:bar' - new_account.reload - assert new_account.user_identifiers.find_by(identifier_scheme: scheme).nil?, "Expected the new account to NOT be associated with the #{scheme.name}" - end - end - -end diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb deleted file mode 100644 index c6c7a98..0000000 --- a/test/functional/users_controller_test.rb +++ /dev/null @@ -1,70 +0,0 @@ -require 'test_helper' - -class UsersControllerTest < ActionDispatch::IntegrationTest - - include Devise::Test::IntegrationHelpers - - setup do - scaffold_org_admin(Org.last) - end - - # TODO: Reassess these routes. Devise handles the standard profile pages so defining a more RESTful setup - # wouldn't conflict with the update/create of the main user object. They should probably be something like: - # - # users GET /org/:org_id/users users#index - # user GET /user/:id users#show - # user PUT /user/:id users#update - - # CURRENT RESULTS OF `rake routes` - # -------------------------------------------------- - # admin_index_users GET /org/admin/users/admin_index users#admin_index - # admin_grant_permissions_user GET /org/admin/users/:id/admin_grant_permissions users#admin_grant_permissions - # admin_update_permissions_user PUT /org/admin/users/:id/admin_update_permissions users#admin_update_permissions - - - # GET /org/admin/users/admin_index (admin_index_users_path) - # ---------------------------------------------------------- - test "get the list of users" do - # Should redirect user to the root path if they are not logged in! - get admin_index_users_path - assert_unauthorized_redirect_to_root_path - - sign_in @user - - get admin_index_users_path - assert_response :success - assert assigns(:users) - end - - # GET /org/admin/users/:id/admin_grant_permissions (admin_grant_permissions_user_path) - # ---------------------------------------------------------- - test "grant the user's permissions" do - # Should redirect user to the root path if they are not logged in! - get admin_grant_permissions_user_path(@user.org.users.first) - assert_unauthorized_redirect_to_root_path - - sign_in @user - - get admin_grant_permissions_user_path(@user.org.users.first) - assert_response :success - end - - # PUT /org/admin/users/:id/admin_update_permissions (admin_update_permissions_user_path) - # ---------------------------------------------------------- - test "update the user's permissions" do - params = {perm_ids: [Perm.last.id, Perm.first.id]} - - # Should redirect user to the root path if they are not logged in! - put admin_update_permissions_user_path(@user.org.users.last), {user: params} - assert_unauthorized_redirect_to_root_path - - sign_in @user - - # Valid save - put admin_update_permissions_user_path(@user.org.users.last), {user: params} - assert_response :success - @user.org.users.last.perms.each do |perm| - assert params[:perm_ids].include?(perm.id), "did not expect to find the #{perm.name} attached to the user" - end - end -end diff --git a/test/helpers/application_helper_test.rb b/test/helpers/application_helper_test.rb deleted file mode 100644 index 0c5ca2b..0000000 --- a/test/helpers/application_helper_test.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'test_helper' - -class ApplicationHelperTest < ActionView::TestCase - - def setup - # initialize the ActionView Output so that we have access to its functions (e.g. content_for) - @view_flow = ActionView::OutputFlow.new - - content_for(:head) do - "Testing".html_safe - end - end - - # ----------------------------------------------------------------------- - test "resource_name should return :user" do - assert_equal :user, resource_name - end - - # ----------------------------------------------------------------------- - test "resource should return contents of instance variable @resource OR a new User" do - # If @resource is not set then we should receive a new User - assert resource.is_a?(User), "Expected resource() to return a new User" - assert_nil resource.id, "Expected resource() to return a User with an Id" - - # If @resource is set then we should receive that object - @resource = Org.first - assert resource.is_a?(Org), "Expected resource() to return @resource" - assert_equal @resource.id, resource.id, "Expected resource() to return the first Organisation" - end - - # ----------------------------------------------------------------------- - test "devise_mapping should return the mappings registered for Devise" do - # If @devise_mappings is not set we should get the mappings for :user - assert_equal Devise.mappings[:user], devise_mapping, "Expected devise_mapping() to return the correct default" - - # If @devise_mapping is set the we should receive it - @devise_mapping = {foo: 'bar'} - assert_equal @devise_mapping, devise_mapping, "Expected devise_mapping() to return @devise_mapping" - end - - # ----------------------------------------------------------------------- - test "hash_to_js_json_variable should return valid JSON markup for the specified Hash object" do - actual = hash_to_js_json_variable('hasher', {foo: 'bar', abc: '123'}) - - assert actual.include?('script'), "Expected the result to be contained within a script tag but got: #{actual}" - assert actual.include?('var hasher = '), "Expected the hash to appear as a variable but got: #{actual}" - assert actual.include?('{"foo":"bar","abc":"123"}'), "Expected the hash contents to appear but got: #{actual}" - end -end \ No newline at end of file diff --git a/test/helpers/mailer_helper_test.rb b/test/helpers/mailer_helper_test.rb deleted file mode 100644 index 617a7bc..0000000 --- a/test/helpers/mailer_helper_test.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'test_helper' - -class MailerHelperTest < ActionView::TestCase - setup do - @user = User.find_by(email: "super_admin@example.com") - @user.perms.destroy_all - end - test "returns nil when objects does not have a method perms" do - assert_nil privileges_list({}) - end - test "returns an empty ul list for an user without permissions" do - assert_equal("
    ", privileges_list(@user)) - end - test "return an ul list with the permission for an user" do - names = name_and_text # PermsHelper method included within MailerHelper - @user.perms << Perm.first - @user.perms << Perm.second - @user.save - expected="
    • #{names[Perm.first.name.to_sym]}
    • #{names[Perm.second.name.to_sym]}
    " - assert_equal(expected, privileges_list(@user)) - end -end \ No newline at end of file diff --git a/test/helpers/plans_helper_test.rb b/test/helpers/plans_helper_test.rb deleted file mode 100644 index f83d738..0000000 --- a/test/helpers/plans_helper_test.rb +++ /dev/null @@ -1,91 +0,0 @@ -require 'test_helper' - -class PlansHelperTest < ActionView::TestCase - - include Devise::Test::IntegrationHelpers - - UNKNOWN = I18n.t("helpers.project.columns.unknown") - - def setup - scaffold_plan - - @user = User.last - sign_in @user - end - -=begin - # ----------------------------------------------------------------------- - test "plan_list_column_heading should return the localized text for the column heading" do - cols = I18n.t("helpers.project.columns") - - cols.each do |k,v| - assert plan_list_column_heading(k.to_s).include?(">#{I18n.t("helpers.project.columns.#{k}")}<"), "expected #{k} to return a column heading labeled #{v}" - end - - assert plan_list_column_heading(["test1", 18, 'test3']).include?("Test1"), "expected the 1st item in the array if its a String" - assert plan_list_column_heading([18, 'test3']).include?(UNKNOWN), "expected 'Unknown' if the 1st item in the array if is NOT a String" - - assert plan_list_column_heading(18).include?(UNKNOWN), "expected 'Unknown' if the value passed is not a String or an Array" - end - - # ----------------------------------------------------------------------- - test "plan_list_column_body should return the localized text for the column heading" do - cols = I18n.t("helpers.project.columns") - - assert plan_list_column_body(["non_link_name", "owner"], @plan).include?(@plan.title), "expected the 1st column to be used if passing in an Array" - - cols.each do |k,v| - val = plan_list_column_body(k.to_s, @plan) - - if Plan.respond_to?(k) - assert plan_list_column_body(k.to_s, @plan).include?(">#{@plan.send(k)}<"), "expected #{k} to return a column containing the Plan's value for that column. Instead got: #{val}" - - else - if k == :owner - assert val.include?(">#{@plan.users.first.name}<") || val.include?(t("helpers.me")), "expected :owner to return a column containing the Plan's value for that column or #{I18n.t("helpers.me")}. Instead got: #{val}" - - elsif k == :shared? - assert val.include?(">Yes<") || val.include?(">No<"), "expected :shared? to return a column containing Yes/No. Instead got: #{val}" - else - - end - end - - end - - # Check different return options for the plan's owner - plan = Plan.create(template: @template, title: 'No owner test') - user = User.create(email: 'tester@example.com', firstname: 'Test', surname: 'Er', password: '123password') - - assert plan_list_column_body('owner', plan).include?(UNKNOWN), "expected Unknown if the column is 'owner' but the plan has no owner" - - plan.assign_creator(user.id) - plan.save! - plan.reload - - assert plan_list_column_body('owner', plan).include?(user.name), "expected the user's name if the column is 'owner' and the plan owner is not the current user" - end - - # TODO: 'custom_template' is part of the case logic in this method but it is unreachable - # because both the plan and template settings objects use "Settings::Template". We - # should remove it from logic - # ----------------------------------------------------------------------- - test "plan_settings_indicator should return the correct export formatting settings" do - assert plan_settings_indicator(@plan).include?(">#{I18n.t("helpers.settings.plans.default_formatting")}<"), "expected the default plan to use default export settings" - - @plan.settings(:export).formatting = {margin: {top: 5, bottom: 5, left: 5, right: 5}, - font_face: Settings::Template::VALID_FONT_FACES.last, - font_size: 12} - @plan.save! - - assert plan_settings_indicator(@plan).include?(">#{I18n.t("helpers.settings.plans.template_formatting")}<"), "expected the default plan to use default export settings" - - @plan.template.settings(:export).formatting = {margin: {top: 10, bottom: 10, left: 10, right: 10}, - font_face: Settings::Template::VALID_FONT_FACES.first, - font_size: 11} - @plan.save! - - assert plan_settings_indicator(@plan).include?(">#{I18n.t("helpers.settings.plans.template_formatting")}<"), "expected the default plan to use default export settings" - end -=end -end diff --git a/test/integration/answer_locking_test.rb b/test/integration/answer_locking_test.rb deleted file mode 100644 index a791eb9..0000000 --- a/test/integration/answer_locking_test.rb +++ /dev/null @@ -1,95 +0,0 @@ -require 'test_helper' - -class AnswerLockingTest < ActionDispatch::IntegrationTest - include Devise::Test::IntegrationHelpers - - setup do - scaffold_template - scaffold_plan - @question = Question.create(text: 'Test question', section: @plan.template.phases.first.sections.first, - question_format: QuestionFormat.where(option_based: false).first, number: 99) - - @owner = @plan.owner - users = User.all - @collaborator = users[users.find_index{ |u| u != @owner }] - - # Make the 2nd user an editor of the plan - Role.create!(user_id: @collaborator.id, plan_id: @plan.id, access: 4) - @plan.reload - end - - test 'answer#create_or_update responds not_found when a plan does not exist' do - userA = Answer.create!(user: @owner, question: @question, - text: "Initial answer - by UserA").attributes - sign_in @owner - params = obj_to_params(userA) - params[:answer][:plan_id] = 'foo' - post create_or_update_answers_path(params) - assert_response :not_found - assert_equal(_('There is no plan with id %{id} for which to create or update an answer') %{ :id => 'foo' }, ActiveSupport::JSON.decode(@response.body)['msg']) - end - - test 'answer#create_or_update responds not found when a question does not exist for a plan' do - userA = Answer.create!(user: @owner, plan: @plan, - text: "Initial answer - by UserA").attributes - sign_in @owner - params = obj_to_params(userA) - params[:answer][:question_id] = 'foo' - post create_or_update_answers_path(params) - assert_response :not_found - assert_equal( - _("There is no question with id %{question_id} associated to plan id %{plan_id} "\ - "for which to create or update an answer") %{ :question_id => 'foo', :plan_id => @plan.id }, ActiveSupport::JSON.decode(@response.body)['msg']) - end - - # ---------------------------------------------------------- - test 'user receives a lock notification if the answer was UPDATED while they were working' do - userA = Answer.create!(user: @owner, plan: @plan, question: @question, - text: "Initial answer - by UserA").attributes - userB = userA.clone - - # Signin as UserA and insert the new answer - sign_in @plan.owner - userA['text'] += " - Updated by userA" - - post create_or_update_answers_path(obj_to_params(userA)) - assert_response :success - assert_equal "application/json", @response.content_type - updated = Answer.find_by(plan: @plan, question: @question) - assert_equal "Initial answer - by UserA - Updated by userA", updated.text - assert_equal @plan.owner.id, updated.user_id - - # Make sure the answers/locking partial is NOT displayed - assert_not @response.body.include?(_('The following answer cannot be saved')), "expected there to be no lock error messaging" - assert @response.body.include?(_('Answered')) - assert @response.body.include?("#{_(' by')} #{@plan.owner.name}"), "expected the messaging to say the plan was updated by the plan owner" - - # Signin as UserB and try to insert the new answer but fail - sign_in @collaborator - userB['text'] += " - Updated by userB" - - post create_or_update_answers_path(obj_to_params(userB)) - assert_response :success - assert_equal "application/json", @response.content_type - updated = Answer.find_by(plan: @plan, question: @question) - assert_equal "Initial answer - by UserA - Updated by userA", updated.text - assert_equal @plan.owner.id, updated.user_id - - # Make sure the answer-notice IS displayed - assert @response.body.include?(_('The following answer cannot be saved')), "expected there to be lock error messaging" - assert @response.body.include?(_('since %{name} saved the answer below while you were editing. Please, combine your changes and then save the answer again.') % { name: @plan.owner.name}), "expected the messaging to STILL say the plan was updated by the plan owner" - assert @response.body.include?(_('Answered')), "expected the messaging to include the status" - end - -# ---------------------------------------------------------- - private - def obj_to_params(attributes) - { - answer: { - plan_id: attributes['plan_id'], - question_id: attributes['question_id'], - text: attributes['text'], - lock_version: attributes['lock_version']} - } - end -end diff --git a/test/integration/authentication_test.rb b/test/integration/authentication_test.rb deleted file mode 100644 index c0f41e5..0000000 --- a/test/integration/authentication_test.rb +++ /dev/null @@ -1,84 +0,0 @@ -require 'test_helper' - -class AuthenticationFlowTest < ActionDispatch::IntegrationTest - include Devise::Test::IntegrationHelpers - - setup do - @user = User.first - end - - # ---------------------------------------------------------- - test 'can sign in with valid email and password' do - sign_in @user - - get root_path - - assert_authorized_redirect_to_plans_page - end - - # ---------------------------------------------------------- - test 'can sign in with shibboleth' do - - end - - # ---------------------------------------------------------- - test 'can sign out' do - get root_path - assert_response :success - - sign_in @user - - delete destroy_user_session_path - - assert_response :redirect - if Rails.application.config.shibboleth_enabled - assert_redirected_to Rails.application.config.shibboleth_logout_url + root_url - else - assert_redirected_to root_path - end - get root_path - - # Make sure that the user is sent to the page that lists their plans - assert_select 'h1', _("Welcome to #{Rails.configuration.branding[:application][:name]}.") - end - - # ---------------------------------------------------------- - test 'can NOT sign in with an invalid email and/or password' do - get root_path - assert_response :success - - users = [{email: @user.email, password: 'bAd_pas$word1', remember_me: true}, - {email: 'unknown@institution.org', password: 'password123', remember_me: true}] - - users.each do |params| - post user_session_path, user: params - - assert_response :redirect - follow_redirect! - - # Make sure that the user is sent to the page that lists their plans - assert_response :success - assert_select 'h1', _("Welcome to #{Rails.configuration.branding[:application][:name]}.") - end - end - - - private - # ---------------------------------------------------------- -=begin - def sign_in - post user_session_path, user: { - email: @user.email, - password: 'password123', - remember_me: false - } - - # The Devise auth gem will end up performing 2 redirects while generating the user's - # session and sending them to the main landing page - 2.times do - assert_response :redirect - follow_redirect! - end - end -=end -end diff --git a/test/integration/download_plan_path_test.rb b/test/integration/download_plan_path_test.rb deleted file mode 100644 index cb6521e..0000000 --- a/test/integration/download_plan_path_test.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'test_helper' - -class DownloadPlanPathTest < ActionDispatch::IntegrationTest - include Devise::Test::IntegrationHelpers - - setup do - @org = init_organisation - template = init_template(@org) - phase = init_phase(template) - section = init_section(phase) - init_question(section) - @plan = init_plan(template) - @user = User.create(user_seed.merge({ org: @org })) - end - - def assert_download_link_present(plan, user) - sign_in user - get(download_plan_path(plan)) - links = css_select("a[href=\"#{download_plan_path(plan)}\"]") - refute_empty(links) - assert_equal(links[0].text, _('Download')) - end - - def refute_download_link_present(plan, user) - sign_in user - get(download_plan_path(plan)) - links = css_select("a[href=\"#{download_plan_path(plan)}\"]") - assert_empty(links) - end - - test 'download tab is visible when user has role creator, administrator, commenter, editor on the plan' do - assign_roles = [ - lambda{ |plan, user| plan.assign_creator(user.id) }, - lambda{ |plan, user| plan.assign_administrator(user.id) }, - lambda{ |plan, user| plan.assign_editor(user.id) }, - lambda{ |plan, user| plan.assign_reader(user.id) } - ] - assign_roles.each do |assign_role| - assign_role.call(@plan, @user) - assert_download_link_present(@plan, @user) - end - end - - test 'download tab is visible when user is super_admin' do - @user.perms = Perm.all - assert_download_link_present(@plan, @user) - end - - test 'download tab is visible when user is an org_admin from the same org that any owner\'s org' do - @plan.assign_creator(@user.id) - user2 = User.create(user_seed.merge({ org: @org, email: 'foo@bar.com' })) - user2.perms << Perm.grant_permissions - assert_download_link_present(@plan, user2) - end - - test 'download tab is NOT visible when user is an org_admin from an org different from every owner\'s org' do - @plan.assign_creator(@user.id) - user2 = User.create(user_seed.merge({ org: init_funder_organisation, email: 'foo@bar.com' })) - user2.perms << Perm.grant_permissions - refute_download_link_present(@plan, user2) - end - - test 'download tab is NOT visible when user is not super_admin nor org_admin nor has commenter role' do - @plan.roles << Role.new(user_id: @user.id, reviewer: true) - refute_download_link_present(@plan, @user) - end -end \ No newline at end of file diff --git a/test/integration/paginable_flows_test.rb b/test/integration/paginable_flows_test.rb deleted file mode 100644 index e1ce85e..0000000 --- a/test/integration/paginable_flows_test.rb +++ /dev/null @@ -1,124 +0,0 @@ -require 'test_helper' - -class PaginableFlowsTest < ActionDispatch::IntegrationTest - include Devise::Test::IntegrationHelpers - - setup do - @user = User.find_by(email: 'super_admin@example.com') - sign_in @user - end - - test 'when total greather than default_per_page pagination and searchable/paginable are enabled' do - create_users(Kaminari.config.default_per_page+1) - get(index_paginable_users_path(1)+"?search=User") - # Fails if search form does not exists under paginable-search - refute_empty(css_select('.paginable-search form')) - # Fails if sort link for email does not exist - refute_empty(css_select('a[href$="1?search=User&sort_field=email&sort_direction=ASC"]')) - # Fails if sort link for last_sign_in_at does not exist - refute_empty(css_select('a[href$="1?search=User&sort_field=last_sign_in_at&sort_direction=ASC"]')) - - link_view_all_search_results = css_select('a[href$="/ALL?search=User"]').first - refute_nil(link_view_all_search_results) - assert_equal(link_view_all_search_results.content, _('View all search results')) - - link_clear_search_results = css_select('a[href$="/1"]').first - refute_nil(link_clear_search_results) - assert_equal(link_clear_search_results.content, _('Clear search results')) - - # Fails if pagination nav is not found - refute_empty(css_select('nav.pagination')) - end - - test 'when total greather than default_per_page pagination and searchable/not paginable are enabled' do - create_users(Kaminari.config.default_per_page+1) - get(index_paginable_users_path('ALL')+"?search=User") - # Fails if search form does not exists under paginable-search - refute_empty(css_select('.paginable-search form')) - # Fails if sort link for email does not exist - refute_empty(css_select('a[href$="ALL?search=User&sort_field=email&sort_direction=ASC"]')) - # Fails if sort link for last_sign_in_at does not exist - refute_empty(css_select('a[href$="ALL?search=User&sort_field=last_sign_in_at&sort_direction=ASC"]')) - - link_view_less_search_results = css_select('a[href$="/1?search=User"]').first - refute_nil(link_view_less_search_results) - assert_equal(link_view_less_search_results.content, _('View less search results')) - - link_clear_search_results = css_select('a[href$="/1"]').first - refute_nil(link_clear_search_results) - assert_equal(link_clear_search_results.content, _('Clear search results')) - - # Fails if pagination nav is found - assert_empty(css_select('nav.pagination')) - end - - test 'when total greather than default_per_page pagination and not searchable/paginable are enabled' do - create_users(Kaminari.config.default_per_page) - get(index_paginable_users_path(1)) - # Fails if search form does not exists under paginable-search - refute_empty(css_select('.paginable-search form')) - # Fails if sort link for email does not exist - refute_empty(css_select('a[href$="1?sort_field=email&sort_direction=ASC"]')) - # Fails if sort link for last_sign_in_at does not exist - refute_empty(css_select('a[href$="1?sort_field=last_sign_in_at&sort_direction=ASC"]')) - # Super admins are not able to see View All link - link = css_select('a[href$="/ALL"]').first - assert_nil(link) - - # Fails if pagination nav is not found - refute_empty(css_select('nav.pagination')) - end - - test 'when total less than default_per_page pagination and searchable is enabled and no records found' do - get(index_paginable_users_path(1)+"?search=foo") - # Fails if search form does not exists under paginable-search - refute_empty(css_select('.paginable-search form')) - - message = css_select('p.bg-info').first - # Fails if there is not contextual background message - refute_nil(message) - assert_equal(message.content.strip, _('There are no records associated')) - - link = css_select('a[href$="/1"]').first - # Fails if link ending with /1 does not exist. Note, used to clear results - refute_nil(link) - assert_equal(link.content, _('Clear search results')) - end - - test 'when total less than default_per_page pagination and searchable is enabled' do - create_users(Kaminari.config.default_per_page) - get(index_paginable_users_path(1)+"?search=User") - # Fails if search form does not exists under paginable-search - refute_empty(css_select('.paginable-search form')) - - message = css_select('p.bg-info').first - assert_nil(message) - - link = css_select('a[href$="/1"]').first - # Fails if link ending with /1 does not exist. Note, used to clear results - refute_nil(link) - assert_equal(link.content, _('Clear search results')) - end - - test 'returns forbidden status when view_all option is false' do - create_users(Kaminari.config.default_per_page) - get(index_paginable_users_path('ALL')) - assert_response(:forbidden) - assert_equal(_('Restricted access to View All the records'), response.body) - end - - teardown do - User.where('email LIKE ?', "user%@example.com").destroy_all - end - - private - def create_users(number) - language = Language.find_by(abbreviation: FastGettext.locale) - (1..number).each do |i| - u = User.new({ - email: "user#{i}@example.com", firstname: "User", surname: "#{i}", - password: "password123", password_confirmation: "password123", org: @user.org, - language: language, accept_terms: true, confirmed_at: Time.zone.now }).save - end - end -end \ No newline at end of file diff --git a/test/integration/template_selection_test.rb b/test/integration/template_selection_test.rb deleted file mode 100644 index af69cd4..0000000 --- a/test/integration/template_selection_test.rb +++ /dev/null @@ -1,167 +0,0 @@ -require 'test_helper' - -class TemplateSelectionTest < ActionDispatch::IntegrationTest - include Devise::Test::IntegrationHelpers - - setup do - # Need to clear the tables until we get seed.rb out of test_helper.rb - Template.delete_all - - @funder = init_funder - @institution = init_institution - @organisation = init_organisation - @funder2 = init_funder({ name: 'Funder 2', abbreviation: 'F2' }) - - @researcher = init_researcher(@institution) - @org_admin = init_org_admin(@institution) - - @funder_published_public_template = init_template(@funder, { - title: 'Test Funder public Template', - published: true - }) - @funder_published_private_template = init_template(@funder, { - title: 'Test Funder private Template', - published: true - }) - # funder templates are public by default on creation so set it to organisationally_visible afterward - @funder_published_private_template.update!({ visibility: Template.visibilities[:organisationally_visible] }) - @funder_unpublished_template = init_template(@funder, { - title: 'Test Funder unpublished Template', - published: false - }) - @funder2_published_public_template = init_template(@funder2, { - title: 'Test Funder 2 Template', - published: true - }) - @org_published_private_template = init_template(@institution, { - title: 'Test Org Template', - published: true - }) - @default_published_private_template = init_template(@organisation, { - title: 'Default Template', - published: true, - is_default: true - }) - end - - # ---------------------------------------------------------- - test 'new plan gets published versions of templates not the latest version' do - version = @org_published_private_template.generate_version! - sign_in @researcher - get "#{org_admin_template_options_path}?plan[org_id]=#{@institution.id}" - json = JSON.parse(@response.body) - assert_equal 1, json['templates'].size, "expected 1 template but got: #{json['templates'].collect{|h| h['title'] }.join(', ')}" - assert_equal @org_published_private_template.id, json['templates'][0]['id'], 'expected the published version of the template' - end - - # ---------------------------------------------------------- - test 'new plan gets default template when no funder or research org is specified' do - sign_in @researcher - get "#{org_admin_template_options_path}?plan[org_id]=&plan[funder_id]=" - json = JSON.parse(@response.body) - assert_equal 1, json['templates'].size, "expected 1 template but got: #{json['templates'].collect{|h| h['title'] }.join(', ')}" - assert_equal @default_published_private_template.id, json['templates'][0]['id'], 'expected the default template' - end - - # ---------------------------------------------------------- - test 'new plan gets org template when a research org is specified but no funder is specified' do - sign_in @researcher - get "#{org_admin_template_options_path}?plan[org_id]=#{@institution.id}&plan[funder_id]=" - json = JSON.parse(@response.body) - assert_equal 1, json['templates'].size, "expected 1 template but got: #{json['templates'].collect{|h| h['title'] }.join(', ')}" - assert_equal @org_published_private_template.id, json['templates'][0]['id'], 'expected 1 org template' - end - - # ---------------------------------------------------------- - test 'new plan gets multiple org templates when a research org is specified but no funder is specified' do - template2 = init_template(@institution, { - title: 'Test Org Template 2', - published: true, - is_default: false, - }) - template2.update!(visibility: Template.visibilities[:organisationally_visible]) - sign_in @researcher - get "#{org_admin_template_options_path}?plan[org_id]=#{@institution.id}&plan[funder_id]=" - json = JSON.parse(@response.body) - assert_equal 2, json['templates'].size, "expected 2 templates but got: #{json['templates'].collect{|h| h['title'] }.join(', ')}" - json['templates'].each{ |h| assert [@org_published_private_template.id, template2.id].include?(h['id']), 'expected the json to include only the 2 org templates' } - end - - # ---------------------------------------------------------- - test 'new plan gets public funder template when no research org is specified' do - sign_in @researcher - get "#{org_admin_template_options_path}?plan[org_id]=&plan[funder_id]=#{@funder.id}" - json = JSON.parse(@response.body) - assert_equal 1, json['templates'].size, "expected 1 template but got: #{json['templates'].collect{|h| h['title'] }.join(', ')}" - assert_equal @funder_published_public_template.id, json['templates'][0]['id'], 'expected the funder template' - end - - # ---------------------------------------------------------- - test 'new plan gets multiple public funder templates when no research org is specified' do - template2 = init_template(@funder, { - title: 'Test Funder Template 2', - published: true, - is_default: false, - visibility: Template.visibilities[:publicly_visible] - }) - sign_in @researcher - get "#{org_admin_template_options_path}?plan[org_id]=&plan[funder_id]=#{@funder.id}" - json = JSON.parse(@response.body) - assert_equal 2, json['templates'].size, "expected 2 templates but got: #{json['templates'].collect{|h| h['title'] }.join(', ')}" - json['templates'].each{ |h| assert [@funder_published_public_template.id, template2.id].include?(h['id']), 'expected the json to include only the 2 funder templates' } - end - - # ---------------------------------------------------------- - test 'new plan gets both the public funder template when both research org and funder are specified' do - sign_in @researcher - get "#{org_admin_template_options_path}?plan[org_id]=#{@institution.id}&plan[funder_id]=#{@funder.id}" - json = JSON.parse(@response.body) - assert_equal 1, json['templates'].size, "expected 1 template but got: #{json['templates'].collect{|h| h['title'] }.join(', ')}" - assert_equal @funder_published_public_template.id, json['templates'][0]['id'], 'expected the funder template' - end - - # ---------------------------------------------------------- - test 'new plan gets the customized version of funder template when the specified research org has customized it' do - customization = @funder_published_public_template.customize!(@institution) - customization.update!(title: 'Customization test', published: true) - sign_in @researcher - get "#{org_admin_template_options_path}?plan[org_id]=#{@institution.id}&plan[funder_id]=#{@funder.id}" - json = JSON.parse(@response.body) - assert_equal 1, json['templates'].size, "expected 1 template but got: #{json['templates'].collect{|h| h['title'] }.join(', ')}" - assert_equal customization.id, json['templates'][0]['id'], 'expected the customization of the funder template' - end - - # ---------------------------------------------------------- - test 'plan gets choice between multiple funder templates when both research org and funder are specified and both the org and funder have multiple templates' do - funder_template2 = init_template(@funder, { title: 'Funder template 2', published: true, visibility: Template.visibilities[:publicly_visible] }) - org_template2 = init_template(@institution, { title: 'Org template 2', published: true, visibility: Template.visibilities[:organisationally_visible] }) - sign_in @researcher - get "#{org_admin_template_options_path}?plan[org_id]=#{@institution.id}&plan[funder_id]=#{@funder.id}" - json = JSON.parse(@response.body) - assert_equal 2, json['templates'].size, "expected 2 templates but got: #{json['templates'].collect{|h| h['title'] }.join(', ')}" - json['templates'].each{ |h| assert [@funder.id, funder_template2.id].include?(h['id']), 'expected the json to include only the funder templates' } - end - - # ---------------------------------------------------------- - test 'new plan gets default template when combination of specified funder and research org have no templates' do - @org_published_private_template.destroy! - @funder_published_public_template.destroy! - sign_in @researcher - get "#{org_admin_template_options_path}?plan[org_id]=#{@institution.id}&plan[funder_id]=#{@funder.id}" - json = JSON.parse(@response.body) - assert_equal 1, json['templates'].size, "expected 1 template but got: #{json['templates'].collect{|h| h['title'] }.join(', ')}" - assert_equal @default_published_private_template.id, json['templates'][0]['id'], 'expected the default template' - end - - # ---------------------------------------------------------- - test 'new plan gets customized version of the default template if the research org has no template of its own but has customized the default template' do - @org_published_private_template.destroy - customization = @default_published_private_template.customize!(@institution) - customization.update!(title: 'Default template customization test', published: true) - sign_in @researcher - get "#{org_admin_template_options_path}?plan[org_id]=#{@institution.id}&plan[funder_id]=" - json = JSON.parse(@response.body) - assert_equal 1, json['templates'].size, "expected 1 template but got: #{json['templates'].collect{|h| h['title'] }.join(', ')}" - assert_equal customization.id, json['templates'][0]['id'], "expected the customized version of the default template" - end -end diff --git a/test/integration/user_activation_test.rb b/test/integration/user_activation_test.rb deleted file mode 100644 index 89aaaf0..0000000 --- a/test/integration/user_activation_test.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'test_helper' - -class AnswerLockingTest < ActionDispatch::IntegrationTest - include Devise::Test::IntegrationHelpers - - setup do - @user = User.create!(email: "super-admin-user-test@example.com", - firstname: "Testing", surname: "User", - password: "password123", password_confirmation: "password123", - org: Org.last, accept_terms: true, confirmed_at: Time.zone.now) - end - - test 'user can login when their account is active' do - sign_in @user - get root_path - assert_authorized_redirect_to_plans_page - end - - test 'user cannot login when their account is inactive' do - @user.active = false - @user.save! - - sign_in @user - # Sign in throws an Exception when the user is inactive - assert_raise do - get root_path - end - end - - test 'logged in user is logged out when their account is deactivated' do - sign_in @user - get root_path - assert_authorized_redirect_to_plans_page - @user.active = false - @user.save! - get root_path - assert_unauthorized_redirect_to_root_path - end -end \ No newline at end of file diff --git a/test/lib/branding_test.rb b/test/lib/branding_test.rb deleted file mode 100644 index 029bb62..0000000 --- a/test/lib/branding_test.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'test_helper' -class BrandingTest < ActiveSupport::TestCase - - test "Returns nested value from hash" do - Rails.configuration.stub(:branding, { test: { value: "foo" }}) do - assert_equal Branding.fetch(:test, :value), "foo" - end - end - - test "It has indifferent access" do - Rails.configuration.stub(:branding, { test: { value: "foo" }}) do - assert_equal Branding.fetch(:test, 'value'), "foo" - end - end - - test "Returns nil if key is missing" do - Rails.configuration.stub(:branding, { test: nil }) do - assert_nil Branding.fetch(:test, 'value') - end - end -end diff --git a/test/mailers/previews/user_mailer_preview.rb b/test/mailers/previews/user_mailer_preview.rb deleted file mode 100644 index 04a07ed..0000000 --- a/test/mailers/previews/user_mailer_preview.rb +++ /dev/null @@ -1,54 +0,0 @@ -class UserMailerPreview < ActionMailer::Preview - def initialize - @org = Org.first - @recipient = User.new(email: "recipient@example.org", firstname: "Test", surname: "Recipient", - password: "password123", password_confirmation: "password123", org: @org, - accept_terms: true, confirmed_at: Time.zone.now) - @requestor = User.new(email: "requestor@example.org", firstname: "Test", surname: "Requestor", - password: "password123", password_confirmation: "password123", org: @org, - accept_terms: true, confirmed_at: Time.zone.now) - @template = Template.new(title: 'Test template', description: 'My test template', org: @org, - archived: false, family_id: "9999999") - @plan = Plan.new(template: @template, title: 'Test Plan', grant_number: 'Grant-123', - principal_investigator: 'Researcher', principal_investigator_identifier: 'researcher-1234', - description: "this is my plan's informative description", - identifier: '1234567890', data_contact: 'researcher@example.org', visibility: :privately_visible) - @role = Role.new(user: @requestor, plan: @plan, access: 14) - end - def welcome_notification - UserMailer.welcome_notification(@requestor) - end - def sharing_notification - UserMailer.sharing_notification(@role, @recipient, inviter: @requestor) - end - def permissions_change_notification - UserMailer.permissions_change_notification(@role, @recipient) - end - # relative_url at /rails/mailers/user_mailer/plan_access_removed - def plan_access_removed - UserMailer.plan_access_removed(@requestor, @plan, @recipient) - end - def api_token_granted_notification - UserMailer.api_token_granted_notification(@requestor) - end - def feedback_notification - UserMailer.feedback_notification(@requestor, @plan, @recipient) - end - def feedback_complete - UserMailer.feedback_complete(@requestor, @plan, @recipient) - end - def feedback_confirmation - UserMailer.feedback_confirmation(@requestor, @plan, @recipient) - end - def plan_visibility - UserMailer.plan_visibility(@requestor, @plan) - end - def new_comment - plan = Plan.joins(:roles).where(Role.creator_condition).first - UserMailer.new_comment(@requestor, plan) - end - # relative_url at /rails/mailers/user_mailer/admin_privileges - def admin_privileges - UserMailer.admin_privileges(@requestor) - end -end \ No newline at end of file diff --git a/test/missing_translation_test.rb b/test/missing_translation_test.rb deleted file mode 100644 index 6aa8926..0000000 --- a/test/missing_translation_test.rb +++ /dev/null @@ -1,69 +0,0 @@ -require 'test_helper' - -class MissingTranslationTest < ActionDispatch::IntegrationTest - - # -------------------------------------------------------------------- - test "Make sure that all FastGettext localisations are defined in the .pot/.po files" do - -# TODO: Do we even need this? We should be able to auto-run the Fastgettext scripts via hooks in rake tasks -=begin - missing = [] - - # Scan through the /app directory for localisations - getFiles(Rails.root.join("app")).each do |file| - contents = File.open(file, 'r').read - localisations = contents.scan(/_\(['"][^\)]+['"]\)/) - - localisations.each do |localisation| - translation = _(localisation) - missing << localisation if translation.include?('translation missing') - end - end - - assert missing.empty?, "Found some missing translations: #{missing.join("\n")}" - missing = [] - - # Loop through all of the models and force all validation errors to be translated - dir = Rails.root.join("app", "models").to_s - getFiles(dir).each do |model| - unless model.start_with?('.') - name = model.gsub('.rb', '').gsub("#{dir}/", '').split('_').collect{|p| p.capitalize }.join('') - name = name.split('/').collect{|p| p }.join('::').gsub(/::[a-z]{1}/, &:upcase) - - # Skip the Settings module classes since they throw errors when validating - unless name.include?('Settings::') - clazz = name.split('::').inject(Object){ |o,c| o.const_get(c) } - obj = clazz.new - - unless obj.valid? - obj.errors.each do |e,m| - missing << m if _(m).include?('translation missing') - end - end - end - end - end - - assert missing.empty?, "Found some missing translations for model errors:\n #{missing.join("\n")}" -=end - end - - - private - # Recursively collect the file names within the directory and its subdirectories - # -------------------------------------------------------------------- - def getFiles(dir) - files = [] - Dir.foreach(dir) do |f| - unless f.start_with?('.') - if File.directory?("#{dir}/#{f}") - files << getFiles("#{dir}/#{f}") - else - files << "#{dir}/#{f}" - end - end - end - files.flatten.uniq - end - -end diff --git a/test/performance/browsing_test.rb b/test/performance/browsing_test.rb deleted file mode 100644 index c1c6084..0000000 --- a/test/performance/browsing_test.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'test_helper' - -# The performance test helper is no longer a part of Rails 4.x. -# Just add the gem to the gemfile if you want use it in the future -=begin -require 'rails/performance_test_help' - -class BrowsingTest < ActionDispatch::PerformanceTest - # Refer to the documentation for all available options - # self.profile_options = { :runs => 5, :metrics => [:wall_time, :memory] - # :output => 'tmp/performance', :formats => [:flat] } - - def test_homepage - get '/' - end -end -=end \ No newline at end of file diff --git a/test/routing_test.rb b/test/routing_test.rb deleted file mode 100644 index 5ec880e..0000000 --- a/test/routing_test.rb +++ /dev/null @@ -1,98 +0,0 @@ -require 'test_helper' - -class RoutingTest < ActionDispatch::IntegrationTest - - include Devise::Test::IntegrationHelpers - - setup do - scaffold_plan - end - - # Routing for the home page - # ------------------------------------------------------------------- - test 'GET / should resolve to HomeController#index' do - assert_routing '/', controller: 'home', action: 'index' - end - - # Routing for Static Pages - # ------------------------------------------------------------------- - test 'GET /about_us should resolve to StaticPagesController#about_us' do - target = {controller: "static_pages", action: "about_us"} - assert_routing about_us_path, target - end - - test 'GET /help should resolve to StaticPagesController#help' do - target = {controller: "static_pages", action: "help"} - assert_routing help_path, target - end - test 'GET /roadmap should resolve to StaticPagesController#roadmap' do - target = {controller: "static_pages", action: "roadmap"} - assert_routing roadmap_path, target - end - test 'GET /terms should resolve to StaticPagesController#terms' do - target = {controller: "static_pages", action: "termsuse"} - assert_routing terms_path, target - end - test 'GET /public_plans should resolve to PublicPagesController#plan_index' do - target = {controller: "public_pages", action: "plan_index"} - assert_routing public_plans_path, target - end - test 'GET /public_export should resolve to PublicPagesController#plan_export' do - plan = Plan.first - target = {controller: "public_pages", action: "plan_export", id: plan.id.to_s} - - assert_routing plan_export_path(id: plan), target - end - - # OAuth - Based on providers identified in the en-UK locale file - # ------------------------------------------------------------------- - test "POST /auth/[:provider]/callback should resolve to OmniauthCallbackController#[:provider]" do - IdentifierScheme.where(active: true).all.each do |scheme| - target = {controller: "users/omniauth_callbacks", action: "#{scheme.name.downcase}"} - assert_routing "/users/auth/#{scheme.name.downcase}/callback", target - end - end - - - # Routing for Users (Some resolve to UsersController and others to Devise's - # RegistrationController) - # ------------------------------------------------------------------- -=begin - test "GET /users should resolve to UsersController#index" do - assert_routing "/users", controller: 'users', action: 'index' - end - - test "GET /users/new should resolve to UsersController#new" do - assert_routing "/users/new", controller: 'users', action: 'new' - end - - test "GET /users/1 should resolve to UsersController#show for user 1" do - assert_routing "/users/1", controller: 'users', action: 'show', id: '1' - end - - test "GET /users/edit should resolve to UsersController#edit" do - assert_routing "/users/1/edit", controller: 'users', action: 'edit', id: '1' - end - - test "POST /users should resolve to Devise's RegistrationsController#create" do - assert_routing({path: "/users", method: 'post'}, - {controller: 'registrations', action: 'create'}) - end - - test "PUT /users/1 should resolve to Devise's RegistrationsController#update" do - assert_routing({path: "/users", method: 'put'}, - {controller: 'registrations', action: 'update'}) - end - - test "PATCH /users/1 should resolve to Devise's RegistrationsController#update" do - assert_routing({path: "/users", method: 'patch'}, - {controller: 'registrations', action: 'update'}) - end - - test "DELETE /users/1 should resolve to Devise's RegistrationsController#update" do - assert_routing({path: "/users", method: 'delete'}, - {controller: 'registrations', action: 'destroy'}) - end -=end - -end \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb deleted file mode 100644 index 4127f42..0000000 --- a/test/test_helper.rb +++ /dev/null @@ -1,533 +0,0 @@ -ENV["RAILS_ENV"] = "test" - -# Startup the simple coverage gem so that our test results are captured -require 'simplecov' -SimpleCov.start 'rails' - -require File.expand_path('../../config/environment', __FILE__) -require 'rails/test_help' -require 'webmock/minitest' -require 'minitest/mock' -require 'active_support/inflector' # For pluralization utility - -class ActiveSupport::TestCase - include GlobalHelpers - - # Suppress noisy ActiveRecord logs because fixtures load for each test - ActiveRecord::Base.logger.level = Logger::INFO - - # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order. - # - # Note: You'll currently still have to declare fixtures explicitly in integration tests - # -- they do not yet inherit this setting - #fixtures :all - - # Use the seeds.rb file to seed the test database - require_relative '../db/seeds.rb' - - # Sometimes TravisCI fails when accessing the LANGUAGES array, so reload it here if necessary - LANGUAGES = Language.all if LANGUAGES.empty? - - # Default attributes for model initialization - def org_seed - { name: 'Test Institution', - abbreviation: 'TST', - org_type: Org.org_type_values_for(:institution).min, - target_url: 'http://test-funder.org', - language: LANGUAGES.first, - contact_email: 'help.desk@test-funder.org', - contact_name: 'Help Desk', - links: {"org":[{"link":"http://dmproadmap.org","text":"DMPRoadmap"}]}, - } - end - def user_seed - { - email: 'test-user@testing-roadmap.org', - firstname: 'Test', - surname: 'User', - language: Language.find_by(abbreviation: FastGettext.locale), - password: "password123", - password_confirmation: "password123", - accept_terms: true, - confirmed_at: Time.zone.now, - } - end - - def template_seed - { - title: 'Test template', - description: 'this is a test template', - org: Org.first, - } - end - def phase_seed - { - title: 'Test phase', - description: 'This is a phase used for testing', - number: 1, - modifiable: true, - } - end - def section_seed - { - title: 'Test section', - description: 'This is a section used for testing', - number: 1, - modifiable: true, - } - end - def question_format_seed - { - title: 'Text area', - option_based: false, - formattype: QuestionFormat.formattypes[:text_area] - } - end - def question_seed - { - text: 'how is our test coverage?', - default_value: 'Not as good as it could be.', - number: 1, - option_comment_display: true, - modifiable: true, - } - end - def annotation_seed - { - text: 'This is some test guidance for a customization', - type: Annotation.types[:guidance] - } - end - def question_option_seed - { - text: 'Option A', - number: 1, - is_default: true, - } - end - def plan_seed - { - title: 'Test plan', - funder_name: 'Organisation with a lot of funds', - grant_number: 'Grant123', - identifier: '123456789', - description: 'This is the project abstract.', - visibility: Plan.visibilities[:privately_visible], - principal_investigator: 'Jane Doe', - principal_investigator_identifier: 'ORCID123', - principal_investigator_email: 'jane.doe@pi.roadmap.org', - principal_investigator_phone: '1234', - data_contact: 'John Doe', - data_contact_email: 'john.doe@pi.roadmap.org', - data_contact_phone: '5678', - } - end - def theme_seed - { - title: 'Test theme', - description: 'This theme is used for testing', - locale: Language.find_by(abbreviation: FastGettext.locale), - } - end - def guidance_group_seed - { - name: 'Test guidance group', - optional_subset: false, - published: true, - } - end - def guidance_seed - { - text: 'This is thematic test guidance.', - published: true, - } - end - - def validate_and_create_obj(obj) - obj.validate - if obj.errors.present? - # Unable to save the object, so output an error rather than burying it - puts "Unable to save #{obj.class.name} because: #{obj.errors.collect{ |e,m| "#{e}: #{m}" }.join(', ')}" - else - obj.save! - end - assert obj.valid? - obj - end - - # Org initializers - def init_institution(**props) - validate_and_create_obj(Org.new(org_seed.merge(props))) - end - def init_funder(**props) - hash = { name: 'Test Funder', abbreviation: 'TSTFNDR', org_type: Org.org_type_values_for(:funder).min } - validate_and_create_obj(Org.new(org_seed.merge(hash.merge(props)))) - end - def init_organisation(**props) - hash = { name: 'Test Organisation', abbreviation: 'TSTORG', org_type: Org.org_type_values_for(:organisation).min } - validate_and_create_obj(Org.new(org_seed.merge(hash.merge(props)))) - end - def init_funder_organisation(**props) - hash = { name: 'Test Funder/Organisation', abbreviation: 'TSTFNDRORG', org_type: Org.org_type_values_for(:funder, :organisation).min } - validate_and_create_obj(Org.new(org_seed.merge(hash.merge(props)))) - end - - # User initializers - def init_researcher(org, **props) - validate_and_create_obj(User.new(user_seed.merge({ - org: org, - surname: 'Researcher', - email: 'researcher@testing-roadmap.org', - }.merge(props)))) - end - def init_org_admin(org, **props) - perms = Perm.where.not(name: ['admin', 'add_organisations', - 'change_org_affiliation', 'grant_api_to_orgs', - 'change_org_details']) - validate_and_create_obj(User.new(user_seed.merge({ - org: org, - surname: 'OrgAdmin', - email: 'org.admin@testing-roadmap.org', - perms: perms, - }.merge(props)))) - end - def init_super_admin(org, **props) - perms = Perm.all - validate_and_create_obj(User.new(user_seed.merge({ - org: org, - surname: 'SuperAdmin', - email: 'super.admin@testing-roadmap.org', - perms: perms - }.merge(props)))) - end - - # Template initializers - def init_template(org, **props) - if org.is_a? Org - validate_and_create_obj(Template.new(template_seed.merge({ org: org }.merge(props)))) - else - puts "You must supply an Org when creating a template! Got the following instead: #{org.inspect}" - nil - end - end - def init_phase(template, **props) - if template.is_a? Template - validate_and_create_obj(Phase.new(phase_seed.merge({ template: template }.merge(props)))) - else - puts "You must supply a Template when creating a phase! Got the following instead: #{template.inspect}" - nil - end - end - def init_section(phase, **props) - if phase.is_a? Phase - validate_and_create_obj(Section.new(section_seed.merge({ phase: phase }.merge(props)))) - else - puts "You must supply a Phase when creating a section! Got the following instead: #{phase.inspect}" - nil - end - end - def init_question_format(**props) - validate_and_create_obj(QuestionFormat.new(question_format_seed.merge(props))) - end - def init_question(section, **props) - if section.is_a? Section -# TODO call init_question_format instead once the seeds.rb has been removed - props[:question_format] = QuestionFormat.first unless props[:question_format].present? - validate_and_create_obj(Question.new(question_seed.merge({ section: section }.merge(props)))) - else - puts "You must supply a Section when creating a question! Got the following instead: #{section.inspect}" - nil - end - end - def init_annotation(org, question, **props) - if org.is_a?(Org) && question.is_a?(Question) - validate_and_create_obj(Annotation.new(annotation_seed.merge({ org: org, question: question }.merge(props)))) - else - puts "You must supply an Org and Question when creating an annotation! Got the following instead: ORG - #{org.inspect}, QUESTION - #{question.inspect}" - nil - end - end - def init_question_option(question, **props) - if question.is_a?(Question) - validate_and_create_obj(QuestionOption.new(question_option_seed.merge({ question: question }.merge(props)))) - else - puts "You must supply a Question when creating a question option! Got the following instead: QUESTION - #{question.inspect}" - nil - end - end - def init_theme(**props) - validate_and_create_obj(Theme.new(theme_seed.merge(props))) - end - def init_guidance_group(org, **props) - if org.is_a? Org - validate_and_create_obj(GuidanceGroup.new(guidance_group_seed.merge({ org: org }.merge(props)))) - else - puts "You must supply an Org when creating a GuidanceGroup! Got the following instead: ORG: #{org.inspect}" - end - end - def init_guidance(guidance_group, **props) - if guidance_group.is_a?(GuidanceGroup) - validate_and_create_obj(Guidance.new(guidance_seed.merge({ guidance_group: guidance_group }.merge(props)))) - else - puts "You must supply a GuidanceGroup when creating a Guidance! Got the following instead: GUIDANCE_GROUP: #{guidance_group.inspect}" - end - end - def init_plan(template, **props) - if template.is_a? Template - validate_and_create_obj(Plan.new(plan_seed.merge({ template: template }.merge(props)))) - else - puts "You must supply a Template when creating a plan! Got the following instead: #{template.inspect}" - nil - end - end - - # equality helpers for complex objects - def assert_annotations_equal(annotation1, annotation2) - assert_equal annotation1.text, annotation2.text, 'expected the annotations to have the same text' - assert_equal annotation1.type, annotation2.type, 'expected the annotations to be of the same type' - end - def assert_question_options_equal(option1, option2) - assert_equal option1.text, option2.text, 'expecetd the question options to have the same text' - assert_equal option1.number, option2.number, 'expecetd the question options to have the same number' - assert_equal option1.is_default, option2.is_default, 'expecetd the question options to have the same default flag value' - end - def assert_questions_equal(question1, question2) - assert_equal question1.number, question2.number, 'expected the question numbers to match' - assert_equal question1.text, question2.text, 'expected the question text to match' - assert_equal question1.question_format, question2.question_format, 'expected the question formats to match' - assert_equal question1.option_comment_display, question2.option_comment_display, 'expected the question optional comment display flags to match' - assert_equal question1.annotations.length, question2.annotations.length, 'expected the questions to have the same number of annotations' - assert_equal question1.question_options.length, question2.question_options.length, 'expected the questions to have the same number of options' - question1.annotations.each_with_index do |annotation, idx| - assert_annotations_equal(annotation, question2.annotations[idx]) - end - question1.question_options.each_with_index do |option, idx| - assert_question_options_equal(option, question2.question_options[idx]) - end - end - def assert_sections_equal(section1, section2) - assert_equal section1.number, section2.number, 'expected the section numbers to match' - assert_equal section1.title, section2.title, 'expected the section titles to match' - assert_equal section1.description, section2.description, 'expected the section descriptions to match' - assert_equal section1.questions.length, section2.questions.length, 'expected the sections to have the same number of questions' - section1.questions.each_with_index do |question, idx| - assert_questions_equal(question, section2.questions[idx]) - end - end - def assert_phases_equal(phase1, phase2) - assert_equal phase1.number, phase2.number, 'expected the phase numbers to match' - assert_equal phase1.title, phase2.title, 'expected the phase titles to match' - assert_equal phase1.description, phase2.description, 'expected the phase descriptions to match' - assert_equal phase1.sections.length, phase2.sections.length, 'expected the phase to have the same number of sections' - phase1.sections.each_with_index do |section, idx| - assert_sections_equal(section, phase2.sections[idx]) - end - end - - - # Get the organisational admin for the Org specified or create one - # ---------------------------------------------------------------------- - def scaffold_org_admin(org) - @user = User.create!(email: "admin-#{org.abbreviation.downcase}@example.com", firstname: "Org", surname: "Admin", - language: Language.find_by(abbreviation: FastGettext.locale), - password: "password123", password_confirmation: "password123", - org: org, accept_terms: true, confirmed_at: Time.zone.now, - perms: Perm.where.not(name: ['admin', 'add_organisations', 'change_org_affiliation', 'grant_api_to_orgs', 'change_org_details'])) - #perms: [Perm::GRANT_PERMISSIONS, Perm::MODIFY_TEMPLATES, Perm::MODIFY_GUIDANCE, Perm::CHANGE_ORG_DETAILS]) - end - - - # Convert Ruby Class Names into attribute names (e.g. MyClass --> my_class) - # ---------------------------------------------------------------------- - def class_name_to_attribute_name(name) - name.gsub(/([a-z]+)([A-Z])/, '\1_\2').gsub('-', '_').downcase - end - - # Scaffold a new Template with one Phase, one Section, and a Question for - # each of the possible Question Formats. - # ---------------------------------------------------------------------- - def scaffold_template - template = Template.new(title: 'Test template', - description: 'My test template', - links: {"funder":[],"sample_plan":[]}, - org: Org.first, archived: false, family_id: "0000009999") - - template.phases << Phase.new(title: 'Test phase', - description: 'My test phase', - number: 1, template: template) - - template.phases.first.sections << Section.new(title: 'Test section', - description: 'My test section', - number: 99, phase: template.phases.first) - - section = template.phases.first.sections.first - i = 1 - # Add each type of Question to the new section - QuestionFormat.all.each do |frmt| - question = Question.new(text: "Test question - #{frmt.title}", number: i, - question_format: frmt, section: section) - - if frmt.option_based? - 3.times do |j| - question.question_options << QuestionOption.new(text: "Option #{j}", number: j, question: question) - end - end - - section.questions << question - i += 1 - end - - template.save! - assert template.valid?, "unable to create new Template: #{template.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - @template = template.reload - end - - # Version the template - # ---------------------------------------------------------------------- - def version_the_template - @template = @template.generate_version! - end - - # Scaffold a new Plan based on the scaffolded Template - # ---------------------------------------------------------------------- - def scaffold_plan - scaffold_template if @template.nil? - - @plan = Plan.new(template: @template, title: 'Test Plan', grant_number: 'Grant-123', - principal_investigator: 'me', principal_investigator_identifier: 'me-1234', - description: "this is my plan's informative description", - identifier: '1234567890', data_contact: 'me@example.com', visibility: :privately_visible, - roles: [Role.new(user: User.last, creator: true)]) - - assert @plan.valid?, "unable to create new Plan: #{@plan.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - @plan.save! - end - - -# FUNCTIONAL/INTEGRATION TEST HELPERS - # ---------------------------------------------------------------------- - def assert_unauthorized_redirect_to_root_path - assert_response :redirect - assert_match "#{root_url}", @response.redirect_url - - follow_redirects - - assert_response :success - assert_select 'main h1', _("Welcome to #{Rails.configuration.branding[:application][:name]}.") - end - - # ---------------------------------------------------------------------- - def assert_authorized_redirect_to_plans_page - assert_response :redirect - assert_match "#{root_url}", @response.redirect_url - - # Sometimes Devise has an intermediary step prior to sending the user to the final destination - follow_redirects - - assert_response :success - assert_select 'main h1', _('My Dashboard') - end - - # ---------------------------------------------------------------------- - def follow_redirects - while @response.status >= 300 && @response.status < 400 - follow_redirect! - end - end - -# UNIT TEST HELPERS - # ---------------------------------------------------------------------- - def verify_deep_copy(object, exclusions) - clazz = Object.const_get(object.class.name) - assert clazz.respond_to?(:deep_copy), "#{object.class.name} does not have a deep_copy method!" - - copy = clazz.deep_copy(object) - object.attributes.each do |name, val| - if exclusions.include?(name) - assert_not_equal object.send(name), copy.send(name), "expected the deep_copy of #{object.class.name}.#{name} to be unique in the copy" - else - unless object.send(name).nil? || copy.send(name) - assert_equal object.send(name), copy.send(name), "expected the deep_copy of #{object.class.name}.#{name} to match" - end - end - end - end - - def assert_deep_copy(original, copy, **options) - if original.class == copy.class - relations = options.fetch(:relations, []).map(&:to_sym) - assert(original.object_id != copy.object_id) - assert_nil(copy.id, "id should be nil for #{copy.class}") if copy.respond_to?(:id) - assert_nil(copy.created_at, "created_at should be nil for #{copy.class}") if copy.respond_to?(:created_at) - assert_nil(copy.updated_at, "updated_at should be nil for #{copy.class}") if copy.respond_to?(:updated_at) - relations.each do |relation| - if copy.respond_to?(relation) - relation_obj = copy.send(relation) - if relation_obj.respond_to?(:each) - relation_obj.each do |obj| - assert_nil(obj.id, "id should be nil for the relation object from #{obj.class}") if copy.respond_to?(:id) - end - end - end - end - end - end - - # ---------------------------------------------------------------------- - def verify_has_many_relationship(object, new_association, initial_expected_count) - # Assumes that the association name matches the pluralized name of the class - rel = "#{class_name_to_attribute_name(new_association.class.name).pluralize}" - - assert_equal initial_expected_count, object.send(rel).count, "was expecting #{object.class.name} to initially have #{initial_expected_count} #{rel}" - - # Add another association for the object - object.send(rel) << new_association - object.save! - assert_equal (initial_expected_count + 1), object.send(rel).count, "was expecting #{object.class.name} to have #{initial_expected_count + 1} #{rel} after adding a new one - #{new_association.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - - # Remove the newly added association - object.send(rel).delete(new_association) - object.save! - assert_equal initial_expected_count, object.send(rel).count, "was expecting #{object.class.name} to have #{initial_expected_count} #{rel} after removing the new one we added" - end - - # ---------------------------------------------------------------------- - def verify_belongs_to_relationship(child, parent) - # Assumes that the association name matches the lower case name of the class - prnt = "#{class_name_to_attribute_name(parent.class.name)}" - chld = "#{class_name_to_attribute_name(child.class.name)}" - - child.send("#{prnt}=", parent) - child.save! - assert_equal parent, child.send(prnt), "was expecting #{chld} to have a #{prnt}.id == #{parent.id}" - - # Search the parent for the child - parent.reload - assert_includes parent.send("#{chld.pluralize}"), child, "was expecting the #{prnt}.#{chld.pluralize} to contain the #{chld}" - end - -# STUBS FOR CALLS To EXTERNAL SITES - # ---------------------------------------------------------------------- - def stub_blog_calls - blog_feed = "" + - "" + - "Testing" + - "http://www.example.com/stubbed/blog/feed" + - "" + - "Stub blog post" + - "http://www.example.com/stubbed/blog/articles/1" + - "This is a stuubed blog post" + - "Test" + - "Thu, 03 Nov 2016 12:38:17 +0000" + - "" + - "1 at http://www.example.com/stubbed/blog" + - "" + - "" - - stub_request(:get, "http://www.dcc.ac.uk/news/dmponline-0/feed"). - with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'User-Agent'=>'Faraday v0.9.2'}). - to_return(:status => 200, :body => blog_feed, :headers => {}) - end -end diff --git a/test/unit/.gitkeep b/test/unit/.gitkeep deleted file mode 100644 index e69de29..0000000 --- a/test/unit/.gitkeep +++ /dev/null diff --git a/test/unit/annotation_test.rb b/test/unit/annotation_test.rb deleted file mode 100644 index 9734c10..0000000 --- a/test/unit/annotation_test.rb +++ /dev/null @@ -1,63 +0,0 @@ -require 'test_helper' - -class AnnotationTest < ActiveSupport::TestCase - - setup do - scaffold_template - - @org = Org.last - @question = @template.phases.first.sections.first.questions.first - - @annotation = Annotation.create(org: @org, question: @question, text: 'Test', - type: Annotation.types[:example_answer]) - end - - # --------------------------------------------------- - test "required fields are required" do - assert_not Annotation.new.valid? - assert_not Annotation.new(org: @org, text: 'Tester').valid?, "expected the 'question' field to be required" - assert_not Annotation.new(question: @question, text: 'Tester').valid?, "expected the 'org' field to be required" - - # TODO: introduce validation on the model that requires text to be provided. - #assert_not Annotation.new(org: @org, question: @question).valid?, "expected the 'text' field to be required" - - # Ensure the bare minimum and complete versions are valid - a = Annotation.new(org: @org, question: @question, text: 'Tester') - assert a.valid?, "expected the 'org', 'question' and 'text' fields to be enough to create an Annotation! - #{a.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - end - - # --------------------------------------------------- - test "to_s returns the text" do - assert_equal @annotation.text, @annotation.to_s - end - - # --------------------------------------------------- - test "deep_copy" do - verify_deep_copy(@annotation, ['id', 'created_at', 'updated_at']) - end - - # --------------------------------------------------- - test "can CRUD Annotation" do - obj = Annotation.create(org: @org, question: @question, text: 'Tester') - assert_not obj.id.nil?, "was expecting to be able to create a new Annotation: #{obj.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - - obj.text = 'my tester' - obj.save! - obj.reload - assert_equal 'my tester', obj.text, "Was expecting to be able to update the text of the Annotation!" - - assert obj.destroy!, "Was unable to delete the Annotation!" - end - - # --------------------------------------------------- - test "can manage belongs_to relationship with Org" do - annotation = Annotation.new(question: @question, text: 'Testing') - verify_belongs_to_relationship(annotation, @org) - end - - # --------------------------------------------------- - test "can manage belongs_to relationship with Question" do - annotation = Annotation.new(org: @org, text: 'Testing') - verify_belongs_to_relationship(annotation, @question) - end -end \ No newline at end of file diff --git a/test/unit/answer_test.rb b/test/unit/answer_test.rb deleted file mode 100644 index 04cf539..0000000 --- a/test/unit/answer_test.rb +++ /dev/null @@ -1,150 +0,0 @@ -require 'test_helper' - -class AnswerTest < ActiveSupport::TestCase - - setup do - @user = User.last - - scaffold_plan - end - - # --------------------------------------------------- - test "can CRUD answers for text based questions" do - QuestionFormat.where(option_based: false).each do |qf| - q = @plan.template.questions.select{|q| q.question_format == qf }.first - - assert_not q.nil?, "expected the test template to have a question of type: #{qf.title}" - - answr = Answer.create(user: @user, plan: @plan, question: q, text: 'Tested ABC') - assert_not answr.id.nil?, "was expecting to be able to create a new Answer for a #{qf.title} question: #{answr.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - - answr.text = 'Testing an update' - answr.save! - answr.reload - assert_equal 'Testing an update', answr.text, "Was expecting to be able to update the text of the Answer for a #{qf.title} question!" - - assert answr.destroy!, "Was unable to delete the Answer for a #{qf.title} question!" - end - end - - # --------------------------------------------------- - test "can CRUD answers for option based questions" do - QuestionFormat.where(option_based: true).each do |qf| - q = @plan.template.questions.select{|q| q.question_format == qf }.first - - assert_not q.nil?, "expected the test template to have a question of type: #{qf.title}" - - answr = Answer.create(user: @user, plan: @plan, question: q, question_options: [q.question_options.first]) - assert_not_nil answr.id, "was expecting to be able to create a new Answer for a #{qf.title} question: #{answr.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - - answr.question_options = [q.question_options.last] - answr.save! - answr.reload - assert answr.question_options.include?(q.question_options.last), "Was expecting the answer to have the '#{q.question_options.last.text}' for a #{qf.title} question!" - assert_not answr.question_options.include?(q.question_options.first), "Was expecting the answer to no longer have the '#{q.question_options.first.text}' for a #{qf.title} question!" - - assert answr.destroy!, "Was unable to delete the Answer for a #{qf.title} question!" - end - end - - # --------------------------------------------------- - test "can copy an Answer" do - qf = QuestionFormat.where(option_based: true).first - q = @plan.template.questions.select{|q| q.question_format == qf }.first - - assert_not_nil q, "expected the test template to have a question of type: #{qf.title}" - answr = Answer.create(user: @user, plan: @plan, question: q, question_options: [q.question_options.first]) - - copy = Answer.deep_copy(answr) - unless answr.text.nil? || copy.text.nil? - assert_equal answr.text, copy.text, "expected the answer text to be the same" - assert_equal answr.question.id, copy.question.id, "expected the question to be the same" - answr.question_options.each do |opt| - assert copy.question_options.include?(opt), "expected the copy to have question options" - end - end - end - - # --------------------------------------------------- - test "can manage belongs_to relationship with User" do - answer = Answer.create(user: @user, plan: @plan, question: @plan.template.questions.first, text: 'Testing') - verify_belongs_to_relationship(answer, User.last) - end - - # --------------------------------------------------- - test "can manage belongs_to relationship with Plan" do - answer = Answer.create(user: @user, plan: @plan, question: @plan.template.questions.first, text: 'Testing') - verify_belongs_to_relationship(answer, @plan) - end - - # --------------------------------------------------- - test "can manage belongs_to relationship with Question" do - answer = Answer.create(user: @user, plan: @plan, question: @plan.template.questions.first, text: 'Testing') - q = @plan.template.phases.first.sections.first.questions.last - verify_belongs_to_relationship(answer, q) - end - - # --------------------------------------------------- - test "can manage has_many relationship with Notes" do - answer = Answer.create(user: @user, plan: @plan, question: @plan.template.questions.first, text: 'Testing') - note = Note.new(text: 'Test Note', user: @user) - verify_has_many_relationship(answer, note, answer.notes.count) - end - - test 'is_valid? returns false when no question is associated to an answer' do - answer = Answer.new(user: @user, plan: @plan) - refute(answer.is_valid?) - end - - test 'is_valid? returns false when an option based answer is empty' do - q = @plan.template.questions[@plan.template.questions.find_index{ |q| q.question_format.option_based? }] - answer = Answer.new(user: @user, plan: @plan, question: q) - refute(answer.is_valid?) - end - - test 'is_valid? returns false when a non-option based answer is empty' do - q = @plan.template.questions[@plan.template.questions.find_index{ |q| !q.question_format.option_based? }] - answer = Answer.new(user: @user, plan: @plan, question: q) - refute(answer.is_valid?) - end - - test 'is_valid? returns true when an option based answer is not empty' do - q = @plan.template.questions[@plan.template.questions.find_index{ |q| q.question_format.option_based? }] - answer = Answer.new(user: @user, plan: @plan, question: q) - answer.question_options << q.question_options.first - assert(answer.is_valid?) - end - - test 'is_valid? returns true when a non-option based answer is not empty' do - q = @plan.template.questions[@plan.template.questions.find_index{ |q| !q.question_format.option_based? }] - answer = Answer.new(user: @user, plan: @plan, question: q, text: 'foo') - assert(answer.is_valid?) - end - - test 'after_save callback only sets plan complete to true if the number of answers matches the number of questions' do - last_question = @plan.template.questions.last - @plan.template.questions.each do |q| - a = Answer.new(user: @user, plan: @plan, question: q, text: 'foo') - if q.question_format.option_based? - a.question_options << q.question_options.first - end - a.save - if q == last_question - assert(a.plan.complete) - else - refute(a.plan.complete) - end - end - end - - test 'after_save callback always updates plan.updated_at' do - plan_before = nil, plan_after = nil - @plan.template.questions.each do |q| - a = Answer.new(user: @user, plan: @plan, question: q, text: 'foo') - updated_at = a.plan.updated_at - a.save - new_updated_at = a.plan.updated_at - assert(updated_at < new_updated_at) - end - end -end diff --git a/test/unit/concerns/conditional_user_mailer_test.rb b/test/unit/concerns/conditional_user_mailer_test.rb deleted file mode 100644 index 9700ae1..0000000 --- a/test/unit/concerns/conditional_user_mailer_test.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'test_helper' - -class ConditionalUserMailerTest < ActiveSupport::TestCase - include ConditionalUserMailer - - def save_email_users_new_comment(user, value) - settings = Pref.default_settings - settings[:email][:users][:new_comment] = value - Pref.new(user: user, settings: settings).save - end - - setup do - @super_admin = User.find_by(email: 'super_admin@example.com') - @funder = User.find_by(email: 'funder_admin@example.com') - @org_admin = User.find_by(email: 'org_admin@example.com') - @org_user = User.find_by(email: 'org_user@example.com') - end - test 'raises ArgumentError for a non-valid key' do - e = assert_raises(ArgumentError) do - deliver_if(recipients: @super_admin, key: nil) - end - assert_equal('key must be String', e.message) - end - test 'returns false when a block is not given' do - assert_equal(deliver_if(recipients: @super_admin, key: 'foo'), false) - end - test 'block is NOT executed when an email preference does not exist' do - block = false - assert_equal(deliver_if(recipients: @super_admin, key: 'foo'){ block = true }, true) - refute block - end - test 'block is NOT executed when an email preference is disabled' do - save_email_users_new_comment(@super_admin, false) - block = false - assert_equal(deliver_if(recipients: @super_admin, key: 'users.new_comment') { block = true }, true) - refute block - end - test 'block is executed when an email preference is enabled' do - save_email_users_new_comment(@super_admin, true) - block = false - assert_equal(deliver_if(recipients: @super_admin, key: 'users.new_comment') { block = true }, true) - assert block - end - test 'block is executed for those users from an array with an email preference enabled' do - save_email_users_new_comment(@super_admin, true) - save_email_users_new_comment(@funder, false) - save_email_users_new_comment(@org_admin, false) - save_email_users_new_comment(@org_user, true) - block = {} - recipients = [ @super_admin, @funder, @org_admin, @org_user ] - assert_equal(deliver_if(recipients: recipients, key: 'users.new_comment') { |r| block[r.email] = true }, true) - assert_equal({ 'super_admin@example.com' => true, 'org_user@example.com' => true }, block) - end - test 'block is executed for those users from an ActiveRecord::Relation with an email preference enabled' do - users = User.where(id: [@super_admin.id, @funder.id, @org_admin.id, @org_user.id]) - save_email_users_new_comment(users.first, true) - save_email_users_new_comment(users.second, false) - save_email_users_new_comment(users.third, false) - save_email_users_new_comment(users.fourth, true) - expected = {} - expected[users.first.email] = true - expected[users.fourth.email] = true - block = {} - assert_equal(deliver_if(recipients: users, key: 'users.new_comment'){ |r| block[r.email] = true }, true) - assert_equal(expected, block) - end -end \ No newline at end of file diff --git a/test/unit/concerns/json_link_validator_test.rb b/test/unit/concerns/json_link_validator_test.rb deleted file mode 100644 index 5148133..0000000 --- a/test/unit/concerns/json_link_validator_test.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'test_helper' - -class JSONLinkValidatorTest < ActiveSupport::TestCase - include JSONLinkValidator - - test 'returns nil for a non-string value passed' do - assert_nil(parse_links(nil)) - assert_nil(parse_links(true)) - assert_nil(parse_links(false)) - assert_nil(parse_links([])) - assert_nil(parse_links({})) - assert_nil(parse_links(1)) - assert_nil(parse_links(1.1)) - end - - test 'returns nil for a non-array object parsed' do - assert_nil(parse_links("string")) - assert_nil(parse_links("1")) - assert_nil(parse_links("{}")) - assert_nil(parse_links("true")) - assert_nil(parse_links("false")) - assert_nil(parse_links("null")) - assert(parse_links("[]")) - end - - test 'returns nil for a non-hash item within the array parsed' do - assert_nil(parse_links("[{}, \"\"]")) - end - - test 'returns nil for a non-valid link key at array item' do - assert_nil(parse_links("[{\"link\": \"\", \"text\": \"\"}, {}]")) - end - - test 'returns nil for a non-valid text key at array item' do - assert_nil(parse_links("[{\"link\": \"\", \"text\": \"\"}, {\"link\": \"\"}]")) - end - - test 'returns nil for a non-string value for link key at array item' do - assert_nil(parse_links("[{\"link\": \"\", \"text\": \"\"}, {\"link\": [], \"text\": \"\"}]")) - end - - test 'returns nil for a non-string value for a text key at array item' do - assert_nil(parse_links("[{\"link\": \"\", \"text\": \"\"}, {\"link\": \"\", \"text\": []}]")) - end - - test 'returns Array for a valid array of link objects' do - assert_equal([{ "link" => "", "text" => ""}, { "link" => "", "text" => ""}], - parse_links("[{\"link\": \"\", \"text\": \"\"}, {\"link\": \"\", \"text\": \"\"}]")) - end -end \ No newline at end of file diff --git a/test/unit/concerns/versionable_test.rb b/test/unit/concerns/versionable_test.rb deleted file mode 100644 index 60391ec..0000000 --- a/test/unit/concerns/versionable_test.rb +++ /dev/null @@ -1,257 +0,0 @@ -require 'test_helper' - -class VersionableTest < ActiveSupport::TestCase - include Versionable - - setup do - @template = create_template - end - def create_template - funder = init_funder - template = init_template(funder) - phase = init_phase(template) - section = init_section(phase) - question = init_question(section) - init_annotation(funder, question) - return template - end - test "versionable concern is included" do - assert(self.respond_to?(:get_modifiable)) - end - test "#find_in_space raises ArgumentError when search_scape does not respond_to to each" do - exception = assert_raises(ArgumentError) do - find_in_space(nil, nil) - end - assert_equal(_('The search_space does not respond to each'), exception.message) - end - test "#find_in_space raises ArgumentError when search_scape does not have elements" do - exception = assert_raises(ArgumentError) do - find_in_space(nil, []) - end - assert_equal(_('The search space does not have elements associated'), exception.message) - end - test "#find_in_space looks for the object in the search_scape that has elements of its same class" do - # Looking for phase - phase = init_phase(@template) - assert_equal(@template.phases.first, find_in_space(phase, @template.phases), 'phase found in the space') - phase.number = 2 - assert_not(find_in_space(phase, @template.phases), 'phase not found in the space') - # Looking for section - section = init_section(Phase.new) - assert_equal(@template.phases.first.sections.first, - find_in_space(section, @template.phases.first.sections), - 'section found in the space') - section.number = 2 - assert_not(find_in_space(section, @template.phases.first.sections), 'section not found in the space') - # Looking for a question - question = init_question(Section.new) - assert_equal(@template.phases.first.sections.first.questions.first, - find_in_space(question, @template.phases.first.sections.first.questions), - ' question found in the space') - question.number = 2 - assert_not(find_in_space(question, @template.phases.first.sections.first.questions), 'question not found in the space') - # Looking for an annotation - annotation = init_annotation(@template.org, Question.new) - assert_equal(@template.phases.first.sections.first.questions.first.annotations.first, - find_in_space(annotation, @template.phases.first.sections.first.questions.first.annotations), 'annotation found in the space') - annotation.text = 'foo' - assert_not(find_in_space(annotation, @template.phases.first.sections.first.questions.first.annotations)) - # Looking for something else - assert_not(find_in_space({}, [{}])) - end - test "#find_in_space looks for the object in the relation" do - # Looking for section - section = init_section(@template.phases.first) - assert_equal(@template.phases.first.sections.first, - find_in_space(section, @template.phases), 'section found in the space through its phase number') - # Looking for question - question = init_question(@template.phases.first.sections.first) - assert_equal(@template.phases.first.sections.first.questions.first, - find_in_space(question, @template.phases), 'question found in the space through its phase/section number') - # Looking for annotation - annotation = init_annotation(@template.org, @template.phases.first.sections.first.questions.first) - assert_equal(@template.phases.first.sections.first.questions.first.annotations.first, - find_in_space(annotation, @template.phases), 'annotation found int the space through its phase/section/question number') - # Looking for a question in a not known search_space - assert_not(find_in_space(Question.new, [{}])) - end - test "#find_in_space looks for an object that does not belong to the hierarchy" do - question = init_question(@template.phases.first.sections.first) - question.phase.number = 2 - assert_not(find_in_space(question, @template.phases)) - end - - test "#get_new raises ArgumentError unless the object respond_to template" do - exception = assert_raises(ArgumentError) do - get_new(@template) - end - assert_equal(_('obj should be a Phase, Section, Question, or Annotation'), exception.message) - end - - test "#get_new raises RuntimeError when template is not latest" do - @template.published = true - @template.generate_version! - - hierarchy_objects = [ - Phase.new(template_id: @template.id), - Section.new(phase_id: @template.phases.first.id), - Question.new(section_id: @template.phases.first.sections.first.id), - Annotation.new(question_id: @template.phases.first.sections.first.questions.first.id) - ] - - hierarchy_objects.each do |obj| - exception = assert_raises(RuntimeError) do - get_new(obj) - end - assert_equal(_('A historical template cannot be retrieved for being modified'), exception.message) - end - end - - test "#get_new returns same object when template is not published" do - # Looking for phase - phase = Phase.new(template: @template) - new_phase = get_new(phase) - assert_equal(phase.template_id, new_phase.template_id, 'returns the phase without generating a new template hierarchy') - # Looking for section - section = Section.new(phase: @template.phases.first) - new_section = get_new(section) - assert_equal(section.phase_id, new_section.phase_id, 'returns the section without generating a new template hierarchy') - # Looking for question - question = Question.new(section: @template.phases.first.sections.first) - new_question = get_new(question) - assert_equal(question.section_id, new_question.section_id, 'returns the question without generating a new template hierarchy') - # Looking for annotation fails - annotation = Annotation.new(question: @template.phases.first.sections.first.questions.first) - new_annotation = get_new(annotation) - assert_equal(annotation.question_id, new_annotation.question_id, 'returns the same annotation without generating a new template hierarchy') - end - - test "#get_new returns new phase when template is published" do - @template.published = true - @template.save! - phase = Phase.new(template: @template) - new_phase = get_new(phase) - assert_not_equal(phase.template_id, new_phase.template_id) - end - - test "#get_new returns new section when template is published" do - @template.published = true - @template.save! - section = Section.new(phase: @template.phases.first) - new_section = get_new(section) - assert_not_equal(section.phase_id, new_section.phase_id) - end - - test "#get_new returns new question when template is published" do - @template.published = true - @template.save! - question = Question.new(section: @template.phases.first.sections.first) - new_question = get_new(question) - assert_not_equal(question.section_id, new_question.section_id) - end - - test "#get_new returns new annotation when template is published" do - @template.published = true - @template.save! - annotation = Annotation.new(question: @template.phases.first.sections.first.questions.first) - new_annotation = get_new(annotation) - assert_not_equal(annotation.question_id, new_annotation.question_id) - end - - test "#get_modifiable raises ArgumentError when the object is not template or object responding to template" do - exception = assert_raises(ArgumentError) do - get_modifiable({}) - end - assert_equal(_('obj should be a Template, Phase, Section, Question, or Annotation'), exception.message) - end - - test "#get_modifiable raises RuntimeError when template is not latest" do - @template.published = true - @template.generate_version! - - hierarchy_objects = [ - @template.phases.first, - @template.phases.first.sections.first, - @template.phases.first.sections.first.questions.first, - @template.phases.first.sections.first.questions.first.annotations.first - ] - - hierarchy_objects.each do |obj| - exception = assert_raises(RuntimeError) do - get_modifiable(obj) - end - assert_equal(_('A historical template cannot be retrieved for being modified'), exception.message) - end - end - - test "#get_modifiable returns same object when template is not published" do - # Looking for phase - phase = @template.phases.first - new_phase = get_modifiable(phase) - assert_equal(phase.id, new_phase.id, 'returns the same phase id') - assert_equal(phase.template_id, new_phase.template_id, 'returns the phase without generating a new template hierarchy') - # Looking for section - section = @template.phases.first.sections.first - new_section = get_modifiable(section) - assert_equal(section.id, new_section.id, 'returns the same section id') - assert_equal(section.phase.template, new_section.phase.template, 'returns the section without generating a new template hierarchy') - # Looking for a question - question = @template.phases.first.sections.first.questions.first - new_question = get_modifiable(question) - assert_equal(question.id, new_question.id, 'returns the same question id') - assert_equal(question.section.phase.template, new_question.section.phase.template, 'returns the question without generating a new template hierarchy') - # Looking for an annotation - annotation = @template.phases.first.sections.first.questions.first.annotations.first - new_annotation = get_modifiable(annotation) - assert_equal(annotation.id, new_annotation.id, 'returns the same annotation id') - assert_equal(annotation.question.section.phase.template, new_annotation.question.section.phase.template, 'returns the annotation without generating a new template hierarchy') - end - - test "#get_modifiable returns new phase when template is published" do - @template.published = true - @template.save! - phase = @template.phases.first - new_phase = get_modifiable(phase) - assert_not_equal(phase.id, new_phase.id, 'returns different phase id') - assert_not_equal(phase.template_id, new_phase.template_id, 'returns different template belonging') - end - - test "#get_modifiable returns new section when template is published" do - @template.published = true - @template.save! - section = @template.phases.first.sections.first - new_section = get_modifiable(section) - assert_not_equal(section.id, new_section.id, 'returns different section id') - assert_not_equal(section.phase.template, new_section.phase.template, 'returns different template belonging') - end - - test "#get_modifiable returns new question when template is published" do - @template.published = true - @template.save! - question = @template.phases.first.sections.first.questions.first - new_question = get_modifiable(question) - assert_not_equal(question.id, new_question.id, 'returns different question id') - assert_not_equal(question.section.phase.template, new_question.section.phase.template, 'returns different template belonging') - end - - test "#get_modifiable returns new annotation when template is published" do - @template.published = true - @template.save! - annotation = @template.phases.first.sections.first.questions.first.annotations.first - new_annotation = get_modifiable(annotation) - assert_not_equal(annotation.id, new_annotation.id, 'returns different annotation id') - assert_not_equal(annotation.question.section.phase.template, new_annotation.question.section.phase.template, 'returns different template belonging') - end - - test "#get_modifiable returns new question_option when template is published" do - @template.published = true - @template.save! - question = @template.phases.first.sections.first.questions.first - question.question_options << init_question_option(question) - question_option = question.question_options.first - new_question = get_modifiable(question) - new_question_option = new_question.question_options.first - assert_not_equal(question_option.id, new_question_option.id, 'returns different question_option id') - assert_not_equal(question_option.question.section.phase.template, new_question_option.question.section.phase.template, 'returns different template belonging') - end -end \ No newline at end of file diff --git a/test/unit/exported_plan_test.rb b/test/unit/exported_plan_test.rb deleted file mode 100644 index b8abd0d..0000000 --- a/test/unit/exported_plan_test.rb +++ /dev/null @@ -1,63 +0,0 @@ -require 'test_helper' - -class ExportedPlanTest < ActiveSupport::TestCase - - setup do - @user = User.last - - scaffold_plan - - @exported = ExportedPlan.create(user: @user, plan: @plan, - format: ExportedPlan::VALID_FORMATS.first) - end - - # --------------------------------------------------- - test "required fields are required" do - assert_not ExportedPlan.new.valid? - assert_not ExportedPlan.new(format: ExportedPlan::VALID_FORMATS.last).valid?, "expected the 'plan' field to be required" - assert_not ExportedPlan.new(plan: @plan).valid?, "expected the 'format' field to be required" - - # Ensure the bar minimum and complete versions are valid - a = ExportedPlan.new(plan: @plan, format: ExportedPlan::VALID_FORMATS.last) - assert a.valid?, "expected the 'plan', 'user' and 'format' fields to be enough to create an ExportedPlan! - #{a.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - end - - - # --------------------------------------------------- - test "as_csv" do - - end - - # --------------------------------------------------- - test "as_txt" do - - end - - # --------------------------------------------------- - test "can CRUD ExportedPlan" do - ExportedPlan::VALID_FORMATS.each do |vf| - ep = ExportedPlan.create(user: @user, plan: @plan, format: vf) - assert_not ep.id.nil?, "was expecting to be able to create a new ExportedPlan: #{ep.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - - expected = (vf == ExportedPlan::VALID_FORMATS.last ? ExportedPlan::VALID_FORMATS.first : ExportedPlan::VALID_FORMATS.last) - - ep.format = expected - ep.save! - ep.reload - assert_equal expected, ep.format, "Was expecting to be able to update the format of the ExportedPlan!" - - assert ep.destroy!, "Was unable to delete the ExportedPlan!" - end - end - - # --------------------------------------------------- - test "can manage belongs_to relationship with Plan" do - verify_belongs_to_relationship(@exported, Plan.last) - end - - # --------------------------------------------------- - test "can manage belongs_to relationship with User" do - verify_belongs_to_relationship(@exported, User.last) - end - -end diff --git a/test/unit/guidance_group_test.rb b/test/unit/guidance_group_test.rb deleted file mode 100644 index 47c1ce5..0000000 --- a/test/unit/guidance_group_test.rb +++ /dev/null @@ -1,106 +0,0 @@ -require 'test_helper' - -class GuidanceGroupTest < ActiveSupport::TestCase - include GlobalHelpers - - setup do - @user = User.first - @org = Org.last - # First clear out any existing templates - GuidanceGroup.all.each do |gg| - gg.destroy! - end - @guidance_group = GuidanceGroup.create(name: 'Test Guidance Group', org: @org, - optional_subset: false, published: true) - end - - # --------------------------------------------------- - test "required fields are required" do - assert_not GuidanceGroup.new.valid? - assert_not GuidanceGroup.new(org: @org).valid?, "expected the 'name' field to be required" - assert_not GuidanceGroup.new(name: 'Tester').valid?, "expected the 'organisation' field to be required" - - # Ensure the bar minimum and complete versions are valid - a = GuidanceGroup.new(name: 'Tester', org: @org) - assert a.valid?, "expected the 'name' and 'organisation' fields to be enough to create a GuidanceGroup! - #{a.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - end - - # --------------------------------------------------- - test "display_name returns organisation name and the guidance group name" do - assert_equal "#{@org.name}", @guidance_group.display_name, "Expected display_name to return the organisation name if there is only one GuidanceGroup" - - GuidanceGroup.create(name: 'Second Test', org: @org) - assert_equal "#{@org.name}: #{@guidance_group.name}", @guidance_group.display_name, "Expected display_name to return the organisation name and guidance group name if there are more than one GuidanceGroup" - end - - # --------------------------------------------------- - test "guidance_groups_excluding does not return guidance groups for the current organisation" do - assert_not GuidanceGroup.guidance_groups_excluding([@org]).include?(@guidance_group), "expected the exclusion to work for an array of orgs" - assert_not GuidanceGroup.guidance_groups_excluding(@org).include?(@guidance_group), "expected the exclusion to work for a single org" - end - - # --------------------------------------------------- - test "user can view guidance_group if it belongs to their organisation" do - org = @user.org - gg = GuidanceGroup.create(name: 'User Test', org: org) - - assert GuidanceGroup.can_view?(@user, gg) - end - - # --------------------------------------------------- - test "user can view guidance_group if it belongs to a funder" do - gg = GuidanceGroup.create(name: 'Funder Test', org: Org.funder.first) - - assert GuidanceGroup.can_view?(@user, gg) - end - - # --------------------------------------------------- - test "user can view guidance_group if it belongs to the managing curation centre" do - gg = GuidanceGroup.create(name: 'Managing CC Test', org: Org.managing_orgs.first) - - assert GuidanceGroup.can_view?(@user, gg) - end - - # --------------------------------------------------- - test "user can view all oftheir organisations, funders, and the managing curation centre's guidance groups" do - @org.users << @user - @org.save - @org.reload - - ggs = [@guidance_group, - GuidanceGroup.create(name: 'User Test', org: @org), - GuidanceGroup.create(name: 'Funder Test', org: Org.funder.first), - GuidanceGroup.create(name: 'Managing CC Test', org: Org.managing_orgs.first)] - - v = GuidanceGroup.all_viewable(@user) - - ggs.each do |gg| - assert v.include?(gg), "expected Guidance Group: '#{gg.name}' to be viewable" - end - end - - # --------------------------------------------------- - test "can CRUD GuidanceGroup" do - gg = GuidanceGroup.create(name: 'Tester', org: @org) - assert_not gg.id.nil?, "was expecting to be able to create a new GuidanceGroup!" - - gg.name = 'Testing an update' - gg.save! - gg.reload - assert_equal 'Testing an update', gg.name, "Was expecting to be able to update the text of the GuidanceGroup!" - - assert gg.destroy!, "Was unable to delete the GuidanceGroup!" - end - - # --------------------------------------------------- - test "can manage has_many relationship with Guidance" do - g = Guidance.new(text: 'Test Guidance') - verify_has_many_relationship(@guidance_group, g, @guidance_group.guidances.count) - end - - # --------------------------------------------------- - test "can manage belongs_to relationship with Org" do - verify_belongs_to_relationship(@guidance_group, @org) - end - -end diff --git a/test/unit/guidance_test.rb b/test/unit/guidance_test.rb deleted file mode 100644 index e67b289..0000000 --- a/test/unit/guidance_test.rb +++ /dev/null @@ -1,110 +0,0 @@ -require 'test_helper' - -class GuidanceTest < ActiveSupport::TestCase - - setup do - @user = User.first - - @guidance_group = GuidanceGroup.create(name: 'Tester', org: @user.org) - @guidance = Guidance.create(text: 'Testing some new guidance') - - @guidance_group.guidances << @guidance - @guidance_group.save! - - @question = Question.first - end - - # --------------------------------------------------- - test "required fields are required" do - assert_not Guidance.new.valid? - assert_not Guidance.new(guidance_group: GuidanceGroup.first).valid?, "expected the 'text' field to be required" - - # Ensure the bar minimum and complete versions are valid - a = Guidance.new(text: 'Testing guidance') - assert a.valid?, "expected the 'text' field to be enough to create a Guidance! - #{a.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - end - - # --------------------------------------------------- - test "correctly identifies guidance as belonging to the org" do - assert @guidance.in_group_belonging_to?(@user.org.id), "expected the guidance to belong to the org" - - @guidance.guidance_group = nil - @guidance.save! - - assert_not @guidance.in_group_belonging_to?(@user.org), "expected the guidance to NOT belong to the org" - end - - # --------------------------------------------------- - test "retrieves guidance by org" do - org = Org.create!(name: 'Tester 123', abbreviation: 'TEST', org_type: 1, links: {"org":[]}) - assert Guidance.by_org(org).empty?, "expected the newly created org to have no guidance" - - assert_not Guidance.by_org(@user.org).empty?, "expected the org to have guidance" - end - - # --------------------------------------------------- - test "correctly identifies whether the user can view the guidance" do - g = Guidance.create(text: 'Unviewable guidance') - - assert_not Guidance.can_view?(@user, g.id), "expected guidance that is not attached to a GuidanceGroup to be unviewable" - - assert Guidance.can_view?(@user, @guidance.id), "expected the user to be able to view guidance belonging to their org" - - @guidance_group.org = Org.managing_orgs.first - @guidance_group.save! - assert Guidance.can_view?(@user, @guidance.id), "expected the user to be able to view guidance belonging to the managing org" - - @guidance_group.org = Org.funder.first - @guidance_group.save! - assert Guidance.can_view?(@user, @guidance.id), "expected the user to be able to view guidance belonging to a funder" - end - - # --------------------------------------------------- - test "make sure a user can view all appropriate guidance" do - viewable = Guidance.all_viewable(@user) - - assert viewable.include?(@guidance), "expected the user to be able to view guidance belonging to their org" - - GuidanceGroup.create(name: 'managing guidance group test', org: Org.managing_orgs.first) - GuidanceGroup.create(name: 'funder guidance group test', org: Org.funder.first) - - Org.managing_orgs.first.guidance_groups.first.guidances.each do |g| - assert viewable.include?(g), "expected the user to be able to view all managing org guidance" - end - - Org.funder.first.guidance_groups.first.guidances.each do |g| - assert viewable.include?(g), "expected the user to be able to view all funder guidance" - end - end - - # --------------------------------------------------- - test "make sure all templates associated with the guidance are returned" do - # TODO: is this method even appropriate? - end - - # --------------------------------------------------- - test "can CRUD Guidance" do - g = Guidance.create(text: 'Testing guidance') - assert_not g.id.nil?, "was expecting to be able to create a new Guidance!" - - g.text = 'Testing an update' - g.save! - g.reload - assert_equal 'Testing an update', g.text, "Was expecting to be able to update the text of the Guidance!" - - # TODO: Uncomment this once the deprecated guidance-guidance_group relationship has been removed from Guidance - #assert g.destroy!, "Was unable to delete the Guidance!" - end - - # --------------------------------------------------- - test "can manage has_many relationship with Theme" do - t = Theme.new(title: 'Test Theme') - verify_has_many_relationship(@guidance, t, @guidance.themes.count) - end - - # --------------------------------------------------- - test "can manage belongs_to relationship with GuidanceGroup" do - gg = GuidanceGroup.new(name: 'Test GuidanceGroup', org: Org.last, published: true) - verify_belongs_to_relationship(@guidance, gg) - end -end \ No newline at end of file diff --git a/test/unit/identifier_schemes_test.rb b/test/unit/identifier_schemes_test.rb deleted file mode 100644 index 8ed4485..0000000 --- a/test/unit/identifier_schemes_test.rb +++ /dev/null @@ -1,45 +0,0 @@ -require 'test_helper' - -class IdentifierSchemesTest < ActiveSupport::TestCase - - def setup - @scheme = IdentifierScheme.first - end - - # --------------------------------------------------- - test "required fields are required" do - assert_not IdentifierScheme.new.valid? - assert_not IdentifierScheme.new(description: 'we are testing').valid? - - # Ensure that the bare minimum of fields is still valid - assert IdentifierScheme.new(name: 'testing').valid? - end - - # --------------------------------------------------- - test "name must be unique" do - assert_not IdentifierScheme.new(name: @scheme.name).valid? - - # Ensure that the bare minimum of fields is still valid - assert IdentifierScheme.new(name: 'testing').valid? - end - - # --------------------------------------------------- - test "can CRUD" do - is = IdentifierScheme.create(name: 'testing') - assert_not is.id.nil?, "was expecting to be able to create a new IdentifierScheme: #{is.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - - is.description = 'Testing It Out' - is.save! - is.reload - assert_equal 'Testing It Out', is.description, "Was expecting to be able to update the api_key of the IdentifierScheme!" - - assert is.destroy!, "Was unable to delete the IdentifierScheme!" - end - - # --------------------------------------------------- - test "can manage has_many relationship with UserIdentifiers" do - ui = UserIdentifier.new(user: User.first, identifier: 'tester') - verify_has_many_relationship(@scheme, ui, @scheme.users.count) - end - -end diff --git a/test/unit/language_test.rb b/test/unit/language_test.rb deleted file mode 100644 index c83de7a..0000000 --- a/test/unit/language_test.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'test_helper' - -class LanguageTest < ActiveSupport::TestCase - - def setup - @lang = Language.find_by(abbreviation: I18n.locale) - @user = User.first - @org = @user.org - end - - # --------------------------------------------------- - test "required fields are required" do - assert_not Language.new.valid? - assert_not Language.new(name: 'Klingon').valid? - assert_not Language.new(name: 'Klingon', description: 'Klingon', default_language: true).valid? - - assert Language.new(abbreviation: 'klgn').valid? - assert Language.new(abbreviation: 'klgn', name: 'Klingon', description: 'Klingon', default_language: true).valid? - end - - # --------------------------------------------------- - test "abbreviation must be unique" do - assert_not Language.new(abbreviation: Language.first.abbreviation).valid? - end - - # --------------------------------------------------- - test "can CRUD" do - lang = Language.create(abbreviation: 'kg', name: 'Klingon') - assert_not lang.id.nil?, "was expecting to be able to create a new Language : #{lang.errors.collect{ |e| e }.join(', ')}" - - lang.name = 'Imperial Klingon' - lang.save! - assert_equal 'Imperial Klingon', lang.reload.name, "was expecting the name to have been updated!" - - assert lang.destroy!, "Was unable to delete the Language!" - end - - # --------------------------------------------------- - test "can manage has_many relationship with Users" do - verify_has_many_relationship(Language.last, User.last, Language.last.users.count) - end - - # --------------------------------------------------- - test "can manage has_many relationship with Organisations" do - org = Org.create(name: 'testing', abbreviation: "TEST", links: {"org":[]}) - verify_has_many_relationship(Language.last, org, Language.last.orgs.count) - end - -end \ No newline at end of file diff --git a/test/unit/note_test.rb b/test/unit/note_test.rb deleted file mode 100644 index 911adfe..0000000 --- a/test/unit/note_test.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'test_helper' - -class NoteTest < ActiveSupport::TestCase - - setup do - @user = User.last - - scaffold_plan - - q = @plan.template.questions.select{|q| !q.question_format.option_based }.first - @answer = Answer.create(user: @user, plan: @plan, question: q, text: 'Testing') - - @note = Note.create(answer: @answer, user: @user, text: 'Test Note', archived: true, - archived_by: User.last) - end - - # --------------------------------------------------- - test "required fields are required" do - assert_not Note.new.valid? - assert_not Note.new(user: @user, answer: @answer).valid?, "expected the 'text' field to be required" - assert_not Note.new(answer: @answer, text: 'Testing').valid?, "expected the 'user' field to be required" - assert_not Note.new(user: @user, text: 'Testing').valid?, "expected the 'answer' field to be required" - - # Ensure the bar minimum and complete versions are valid - a = Note.new(user: @user, answer: @answer, text: 'Testing') - assert a.valid?, "expected the 'text', 'answer' and 'user' fields to be enough to create an Note! - #{a.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - end - - # --------------------------------------------------- - test "can CRUD Note" do - obj = Note.create(user: @user, answer: @answer, text: 'Tested ABC') - assert_not obj.id.nil?, "was expecting to be able to create a new Note: #{obj.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - - obj.text = 'Testing an update' - obj.save! - obj.reload - assert_equal 'Testing an update', obj.text, "Was expecting to be able to update the text of the Note!" - - assert obj.destroy!, "Was unable to delete the Note!" - end - - # --------------------------------------------------- - test "can manage belongs_to relationship with Answer" do - verify_belongs_to_relationship(@note, @answer) - end - - # --------------------------------------------------- - test "can manage belongs_to relationship with User" do - verify_belongs_to_relationship(@note, @user) - end -end diff --git a/test/unit/notification_test.rb b/test/unit/notification_test.rb deleted file mode 100644 index 7be2e1b..0000000 --- a/test/unit/notification_test.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'test_helper' - -class NotificationTest < ActiveSupport::TestCase - - setup do - @super_admin = User.find_by(email: 'super_admin@example.com') - - @notification = Notification.create!( - notification_type: Notification.notification_types[:global], - title: 'notification_1', - level: Notification.levels[:info], - body: 'notification 1', - dismissable: true, - starts_at: Time.now, - expires_at: Time.now + 1.days) - end - - # Validity - test 'validations valid' do - 1.upto(10) { |i| assert(@notification.valid?) } - end - - # Date validation - test 'validations inconsistent dates' do - @notification.expires_at = Date.today - 1.days - assert_not(@notification.valid?) - end - - # Missing parameters - test 'validations missing params' do - @notification.dismissable = nil - assert(@notification.valid?) - @notification.dismissable = false - @notification.notification_type = nil - assert_not(@notification.valid?) - @notification.notification_type = Notification.notification_types[:global] - @notification.title = nil - assert_not(@notification.valid?) - @notification.title = "Testing" - @notification.body = nil - assert_not(@notification.valid?) - @notification.body = "Testing" - @notification.level = nil - assert_not(@notification.valid?) - @notification.level = Notification.levels[:info] - @notification.starts_at = nil - assert_not(@notification.valid?) - @notification.starts_at = Time.now - @notification.expires_at = nil - assert_not(@notification.valid?) - end -end diff --git a/test/unit/org_test.rb b/test/unit/org_test.rb deleted file mode 100644 index 645948a..0000000 --- a/test/unit/org_test.rb +++ /dev/null @@ -1,191 +0,0 @@ -require 'test_helper' - -class OrgTest < ActiveSupport::TestCase - setup do - @org = Org.create!(name: 'Testing', abbreviation: 'TST', links: {"org":[]}) - - @language = Language.find_by(abbreviation: I18n.default_locale) - end - - # ---------- required fields are required ------------ - test "required fields should be required" do - org = Org.new - assert_not(org.valid?) - - org.name = 'ABCD' - org.links = {"org":[]} - assert(org.valid?) - end - - # ---------- short_name ---------- - test "short_name should return the abbreviation if it exists" do - assert_equal(@org.abbreviation, @org.short_name) - end - - test "short_name should return the name if no abbreviation exists" do - @org.abbreviation = nil - assert_equal(@org.name, @org.short_name) - end - - # --------------------------------------------------- - test "to_s returns the name" do - assert_equal @org.name, @org.to_s - end - - # --------------------------------------------------- - test "type returns the correct value" do - @org.institution = true - assert_equal "Institution", @org.org_type_to_s - - @org.institution = false - @org.funder = true - assert_equal "Funder", @org.org_type_to_s - - @org.funder = false - @org.organisation = true - assert_equal "Organisation", @org.org_type_to_s - - @org.organisation = false - @org.research_institute = true - assert_equal "Research Institute", @org.org_type_to_s - - @org.research_institute = false - @org.project = true - assert_equal "Project", @org.org_type_to_s - - @org.project = false - @org.school = true - assert_equal "School", @org.org_type_to_s - - @org.school = false - assert_equal "None", @org.org_type_to_s - - @org.funder = true - @org.organisation = true - assert_equal "Funder, Organisation", @org.org_type_to_s - end - - # --------------------------------------------------- - test "should resize logo to a height of 100" do - ['logo.jpg', # this one is at 160x160 - 'logo_300x300.jpg', - 'logo_100x100.jpg'].each do |file| - - path = File.expand_path("../../assets/#{file}", __FILE__) - @org.logo = Dragonfly.app.fetch_file("#{path}") - - assert @org.valid?, "expected the logo to have been attached to the org" - assert_equal 100, @org.logo.height, "expected the logo to have been resized properly" - end - end - - # --------------------------------------------------- - test "should remove all associated User's api tokens if no TokenPermissionTypes are present" do - @org.token_permission_types << TokenPermissionType.first - usr = User.new(email: 'tester@testing.org', password: 'testing123') - usr.keep_or_generate_token! - - original = usr.api_token - @org.users << usr - - # Make sure that the user's API token was saved - @org.save! - usr = @org.reload.users.find_by(email: 'tester@testing.org') - assert_equal original, usr.api_token - - # TODO: Determine if this should just be removed or if it should still be removing these - # Make sure that the user's API token is cleared out when all API permissions - # for the org have been removed - #@org.token_permission_types.clear - #@org.save! - #usr = @org.reload.users.find_by(email: 'tester@testing.org') - #assert_equal nil, usr.api_token - end - - # --------------------------------------------------- - test "published_templates should return all published templates" do - 3.times do |i| - template = Template.create(org: @org, version: 1, title: "Testing #{i}") - if i < 2 - template.published = true - template.save! - end - end - - assert_not @org.published_templates.select{|t| t.title == "Testing 0"}.empty?, "expected the 1st template to be included" - assert_not @org.published_templates.select{|t| t.title == "Testing 1"}.empty?, "expected the 2nd template to be included" - assert @org.published_templates.select{|t| t.title == "Testing 2"}.empty?, "expected the 3rd template to NOT be included" - end - - # --------------------------------------------------- - test "check_api_credentials removes all user api_tokens" do - org = Org.last - org.users.each do |user| - user.api_token = '12345' - user.save! - end - - org.check_api_credentials - org.reload - - org.users.each do |user| - assert_equal "", user.api_token, "expected the api_token for #{user.name} to have been deleted" - end - end - - # --------------------------------------------------- - test "can CRUD" do - org = Org.create(name: 'testing', links: {"org":[]}) - assert_not org.id.nil?, "was expecting to be able to create a new Org: #{org.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - - org.abbreviation = 'TEST' - org.save! - org.reload - assert_equal 'TEST', org.abbreviation, "Was expecting to be able to update the abbreviation of the Org!" - - assert org.destroy!, "Was unable to delete the Org!" - end - - # --------------------------------------------------- - test "can manage has_many relationship with Users" do - usr = User.create(email: 'test@testing.org', password: 'testing1234') - verify_has_many_relationship(@org, usr, @org.users.count) - end - - # --------------------------------------------------- - test "can manage has_many relationship with Templates" do - tmplt = Template.new(title: 'Added through test', version: 1) - verify_has_many_relationship(@org, tmplt, @org.templates.count) - end - - # --------------------------------------------------- - test "can manage has_many relationship with Customisations" do - tmplt = Template.new(title: 'Testing template', version: 1) - verify_has_many_relationship(@org, tmplt, @org.templates.count) - end - - # --------------------------------------------------- - test "can manage has_many relationship with GuidanceGroups" do - gg = GuidanceGroup.new(name: 'Tester') - verify_has_many_relationship(@org, gg, @org.guidance_groups.count) - end - - # --------------------------------------------------- - test "can manage has_many relationship with Annotations" do - a = Annotation.new(question: Question.first, text: 'Test Annotation') - verify_has_many_relationship(@org, a, @org.annotations.count) - end - - # --------------------------------------------------- - test "can manage has_many relationship with TokenPermissionTypes" do - tpt = TokenPermissionType.new(token_type: 'testing') - verify_has_many_relationship(@org, tpt, @org.token_permission_types.count) - end - - # --------------------------------------------------- - test "Guidance Group should be created after_create of the Org" do - org = Org.create!(name: 'Testing Guidance Group for Org', abbreviation: 'TGG', links: {"org":[]}) - assert_equal org.guidance_groups.count, 1 - org.destroy! - end -end diff --git a/test/unit/perm_test.rb b/test/unit/perm_test.rb deleted file mode 100644 index a651315..0000000 --- a/test/unit/perm_test.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'test_helper' - -class PermTest < ActiveSupport::TestCase - setup do - @user = User.last - - @perm = Perm.create(name: 'testing') - end - - # --------------------------------------------------- - test "required fields are required" do - assert_not Perm.new.valid? - - # Ensure the bare minimum and complete versions are valid - a = Perm.new(name: 'Testing 2') - assert a.valid?, "expected the 'name' field to be enough to create an Perm! - #{a.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - end - - # --------------------------------------------------- - test "name field must be unique" do - assert_not Perm.new(name: 'testing').valid? - end - - # --------------------------------------------------- - test "can CRUD Perm" do - obj = Perm.create(name: 'Tested ABC') - assert_not obj.id.nil?, "was expecting to be able to create a new Perm: #{obj.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - - obj.name = 'Testing an update' - obj.save! - obj.reload - assert_equal 'Testing an update', obj.name, "Was expecting to be able to update the name of the Perm!" - - assert obj.destroy!, "Was unable to delete the Perm!" - end - - # --------------------------------------------------- - test "can manage has_many relationship with User" do - verify_has_many_relationship(@perm, @user, @perm.users.count) - end - -end diff --git a/test/unit/phase_test.rb b/test/unit/phase_test.rb deleted file mode 100644 index 5383b59..0000000 --- a/test/unit/phase_test.rb +++ /dev/null @@ -1,62 +0,0 @@ -require 'test_helper' - -class PhaseTest < ActiveSupport::TestCase - - setup do - # Need to clear the tables until we get seed.rb out of test_helper.rb - Template.delete_all - @funder = init_funder - @template = init_template(@funder, published: true) - @phase = init_phase(@template) - end - - test "required fields are required" do - assert_not Phase.new.valid? - assert_not Phase.new(title: 'Testing', number: 1).valid?, "expected the template field to be required" - assert_not Phase.new(number: 2, template: @template).valid?, "expected the title field to be required" - assert_not Phase.new(title: 'Testing', template: @template).valid?, "expected the number field to be required" - - # Ensure the bare minimum and complete versions are valid - a = Phase.new(title: 'Testing', template: @template, number: 2) - assert a.valid?, "expected the 'title', 'number' and 'template' fields to be enough to create an Phase! - #{a.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - end - - test "titles scope returns a list of all phase id with their titles for the specified template" do - init_phase(@template, { title: 'test scope 1', number: 2 }) - init_phase(@template, { title: 'test scope 2', number: 3 }) - titles = Phase.titles(@template) - assert_equal 3, titles.length, 'expected 3 phases, the orignal and 2 new' - assert_not titles.select{ |p| p.title == 'test scope 2' }.empty?, "expected to find the second phase" - end - - test "titles scope does not return phases from other templates" do - init_phase(@template, { title: 'test scope 1', number: 2 }) - template2 = init_template(@funder, { title: 'template 2' }) - init_phase(template2, { title: 'other template scope' }) - titles = Phase.titles(@template) - assert titles.select{ |p| p.title == 'other template scope' }.empty?, "expected to not find the other template's phase" - end - - test "#deep_copy creates a new phase object and attaches new section objects" do - assert_deep_copy(@phase, @phase.deep_copy, relations: [:sections]) - end - - test "num_questions returns the total number of questions for the phase" do - section = init_section(@phase) - section2 = init_section(@phase, { title: 'Section B', number: 2 }) - init_question(section) - init_question(section, { text: 'Question number 2', number: 2 }) - init_question(section2) - assert_equal 3, @phase.num_questions, 'expected 3 questions for the phase, 2 for the 1st section and 1 for the 2nd section' - end - - test "num_questions does not count questions that belong to other templates" do - section = init_section(@phase) - init_question(section) - template2 = init_template(@funder, { title: 'template 2' }) - phase2 = init_phase(template2, { title: 'other template scope' }) - section2 = init_section(phase2) - init_question(section2) - assert_equal 1, @phase.num_questions, 'expected 1 question for the phase' - end -end diff --git a/test/unit/plan_test.rb b/test/unit/plan_test.rb deleted file mode 100644 index 9d84917..0000000 --- a/test/unit/plan_test.rb +++ /dev/null @@ -1,382 +0,0 @@ -require 'test_helper' - -class PlanTest < ActiveSupport::TestCase - - setup do - - @org = Org.first - @template = Template.first - - @creator = User.last - @administrator = User.create!(email: 'administrator@example.com', password: 'password123') - @editor = User.create!(email: 'editor@example.com', password: 'password123') - @reader = User.create!(email: 'reader@example.com', password: 'password123') - - @plan = Plan.create(title: 'Test Plan', template: @template, grant_number: 'Plan12345', - identifier: '000912', description: 'This is a test plan', - principal_investigator: 'John Doe', principal_investigator_identifier: 'ABC', - data_contact: 'john.doe@example.com', visibility: :privately_visible) - - @plan.assign_creator(@creator.id) - @plan.assign_administrator(@administrator.id) - @plan.assign_editor(@editor.id) - @plan.assign_reader(@reader.id) # AKA a commenter - @plan.save! - @plan.reload - end - - test "readable_by? false when organisation settings is false for org admin" do - @creator.update!(org: @org) - @org_admin = User.create!(email: "org-admin@example.com", - password: "password", - org: @org) - - @org_admin.perms << Perm.grant_permissions - @org_admin.perms << Perm.modify_guidance - @org_admin.perms << Perm.modify_templates - @org_admin.perms << Perm.change_org_details - - assert @org_admin.reload.can_org_admin? - Rails.configuration.stub(:branding, { - service_configuration: { plans: { org_admins_read_all: false } } - }) do - refute @plan.readable_by?(@org_admin) - end - end - - test "readable_by? true when organisation settings is true for org admin" do - @creator.update!(org: @org) - @org_admin = User.create!(email: "org-admin@example.com", - password: "password", - org: @org) - - @org_admin.perms << Perm.grant_permissions - @org_admin.perms << Perm.modify_guidance - @org_admin.perms << Perm.modify_templates - @org_admin.perms << Perm.change_org_details - - assert @org_admin.reload.can_org_admin? - Rails.configuration.stub(:branding, { - service_configuration: { plans: { org_admins_read_all: true } } - }) do - assert @plan.readable_by?(@org_admin) - end - end - - # --------------------------------------------------- - test "required fields are required" do - # TODO: uncomment the validation on Plan and then retest this. The validations appear to be breaking the - # current plan save process in the controller, so determine why and fix. -=begin - assert_not Plan.new.valid? - assert_not Plan.new(title: 'Testing').valid?, "expected the template field to be required" - - # Make sure that the Settings gem is defaulting the title for us - assert Plan.new(template: @template).valid?, "expected the title field to have been set by default by the Settings gem" - - # Ensure the bare minimum and complete versions are valid - a = Plan.new(title: 'Testing', template: @template) - assert a.valid?, "expected the 'title', 'template' and at least one 'user' fields to be enough to create an Plan! - #{a.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" -=end - end - - - # --------------------------------------------------- - test "correctly creates a new answer" do - q = @template.phases.first.sections.last.questions.last - q.answers = [] - q.save! - - answer = @plan.answer(q.id) - assert_nil answer.id, "expected a new Answer" - unless q.default_value.nil? - assert_equal q.default_value, answer.text, "expected the new Answer to use the Default Answer for the Question" - end - end - - # --------------------------------------------------- - test "correctly retrieves the answer for the question" do - q = @template.phases.first.sections.last.questions.last - answer = @plan.answer(q.id) - answer.text = "testing" - answer.save! - - answr = @plan.answer(q.id) - assert_not answr.id.nil?, "expected the latest Answer" - assert_equal "testing", answr.text, "expected the Answer returned to have the correct text" - end - - # --------------------------------------------------- - test "retrieves the selected guidance groups" do - # Create a new theme and attach it to our template's question and a guidance group - t = Theme.create!(title: 'Test A') - q = @template.phases.first.sections.first.questions.first - # This is to make the default guidance group created in callback to be published. - # This ensures the selected gudiance group test passes with appropriate GuidanceGroup. - gug = @org.guidance_groups.first - gug.published = true - gug.save! - g = gug.guidances.first - - g.themes << t - g.save - q.themes << t - q.save - - # Create a new guidance group and guidance that is attached to a theme but NOT used by our template - t = Theme.create!(title: 'Test B') - gg = GuidanceGroup.create!(name: 'Tester', org: @creator.org) - g = Guidance.create!(text: 'Testing guidance', guidance_group: gg, themes: [t]) - - pggs = @plan.get_guidance_group_options - assert pggs.include?(gug) - assert_not pggs.include?(gg) - end - - # --------------------------------------------------- - test "retrieves the available guidance for a the plan as a hash" do - guidance_groups = GuidanceGroup.includes(guidances: :themes).where(published: true) - @plan.guidance_groups << guidance_groups - @plan.save! - - phase = @template.phases.first - hash = @plan.guidance_by_question_as_hash - - phase.sections.includes(questions: :themes).each do |section| - section.questions.each do |question| - question.themes.each do |theme| - guidance_groups.includes(guidances: :themes).each do |guidance_group| - themed_guidance = guidance_group.guidances.collect{ |g| g.themes.collect(&:title) }.flatten.uniq - if themed_guidance.include?(theme.title) - assert hash[question.id][guidance_group.name][theme.title].length > 0, "expected themed guidance to appear for Question: #{question.id}, GuidanceGroup: #{guidance_group.name} and Theme: #{theme.title}" - end - end - end - end - end - end - - # --------------------------------------------------- - test "checks whether the specified user can edit the plan" do - @plan.assign_administrator(@administrator) - @plan.assign_editor(@editor) - @plan.assign_reader(@reader) - - # TODO: It seems like editable_by? should return true if the user is the creator or we've called assign_editor - # or assign_administrator. seems to be an issue with the assign_user private method on the Plan model - #assert @plan.editable_by?(@creator), "expected the creator to NOT be able to edit the plan" - #assert @plan.editable_by?(@editor), "expected the editor to be able to edit the plan" - #assert @plan.editable_by?(@administrator), "expected the administrator to NOT be able to edit the plan" - - assert_not @plan.editable_by?(@reader), "expected the reader to NOT be able to edit the plan" - end - - # --------------------------------------------------- - test "checks whether the specified user can read the plan" do - @plan.assign_administrator(@administrator) - @plan.assign_editor(@editor) - @plan.assign_reader(@reader) - - # TODO: It seems like readable_by? should return true if the user is the creator or we've called assign_editor - # or assign_administrator or assign_reader. seems to be an issue with the assign_user method on Plan - #assert @plan.readable_by?(@creator), "expected the creator to NOT be able to read the plan" - #assert @plan.readable_by?(@editor), "expected the editor to be able to read the plan" - #assert @plan.readable_by?(@administrator), "expected the administrator to be able to read the plan" - #assert @plan.readable_by?(@reader), "expected the reader to be able to read the plan" - end - - # --------------------------------------------------- - test "checks whether the specified user can administer the plan" do - @plan.assign_administrator(@administrator) - @plan.assign_editor(@editor) - @plan.assign_reader(@reader) - - # TODO: It seems like creator should be able to administer their own plan or we have called assign_administrator - # seems to be an issue with the assign_user private method on the Plan model - #assert @plan.administerable_by?(@creator), "expected the creator to NOT be able to administer the plan" - #assert @plan.administerable_by?(@administrator), "expected the administrator to be able to administer the plan" - - assert_not @plan.administerable_by?(@editor), "expected the editor to NOT be able to administer the plan" - assert_not @plan.administerable_by?(@reader), "expected the reader to NOT be able to administer the plan" - end - - # --------------------------------------------------- - test "checks that status returns the correct information" do - q = 0 - @template.phases.first.sections.map{|s| q += s.questions.count } - hash = @plan.status - - # Expecting the hash to look something like this: - # ----------------------------------------------- - #{"num_questions"=>13, - # "num_answers"=>0, - # "sections"=>{ - # 1=>{"questions"=>[1, 2], "num_questions"=>2, "num_answers"=>0}, - # 2=>{"questions"=>[3], "num_questions"=>1, "num_answers"=>0}}, - # "questions"=>{ - # 1=>{"answer_id"=>nil, "answer_created_at"=>nil, "answer_text"=>nil, - # "answer_option_ids"=>nil, "answered_by"=>nil}, - # 2=>{"answer_id"=>nil, "answer_created_at"=>nil, "answer_text"=>nil, - # "answer_option_ids"=>nil, "answered_by"=>nil}, - # 3=>{"answer_id"=>nil, "answer_created_at"=>nil, "answer_text"=>nil, - # "answer_option_ids"=>nil, "answered_by"=>nil}}, - # "space_used"=>30} - - assert_equal q, hash["num_questions"], "expected the number of questions to match" - - @template.phases.first.sections.each do |s| - assert_not hash["sections"][s.id].nil?, "expected section #{s.id} to be in sections portion" - s.questions.each do |q| - assert hash["sections"][s.id]["questions"].include?(q.id), "expected section #{s.id}, question #{q.id} to be in section portion" - - assert_not hash["questions"][q.id].nil?, "expected question #{q.id} to appear in the questions portion" - end - end - end - - # --------------------------------------------------- - test "checks that user is a properly assigned as a creator" do - assert @plan.owned_by?(@creator), "expected the creator to be able to be the owner" - assert @plan.administerable_by?(@creator), "expected the creator to be able to administer" - assert @plan.editable_by?(@creator), "expected the creator to be able to edit" - assert @plan.readable_by?(@creator), "expected the creator to be able to comment" - end - - # --------------------------------------------------- - test "checks that user is a properly assigned as a editor" do - assert_not @plan.owned_by?(@editor), "expected the editor to NOT be the owner" - assert_not @plan.administerable_by?(@editor), "expected the editor to NOT be able to administer" - assert @plan.editable_by?(@editor), "expected the editor to be able to edit" - assert @plan.readable_by?(@editor), "expected the editor to be able to comment" - end - - # --------------------------------------------------- - test "checks that user is a properly assigned as a commenter" do - assert_not @plan.owned_by?(@reader), "expected the reader to NOT be the owner" - assert_not @plan.administerable_by?(@reader), "expected the reader to NOT be able to administer" - assert_not @plan.editable_by?(@reader), "expected the reader to NOT be able to edit" - assert @plan.readable_by?(@reader), "expected the commenter to be able to comment" - end - - # --------------------------------------------------- - test "checks that user is a properly assigned as a administrator" do - assert_not @plan.owned_by?(@administrator), "expected the adminstrator to NOT be the owner" - assert @plan.administerable_by?(@administrator), "expected the adminstrator to be able to administer" - assert @plan.editable_by?(@administrator), "expected the adminstrator to be able to edit" - assert @plan.readable_by?(@administrator), "expected the adminstrator to be able to comment" - end - - # --------------------------------------------------- - test "checks that user is a properly assigned as a reviewer" do - val = Role.access_values_for(:reviewer, :commenter).min - usr = User.create(email: 'test@testing.org', password: 'testing1234') - @plan.roles << Role.new(user: usr, access: val) - @plan.save! - assert @plan.reviewable_by?(usr), "expected the reviewer to be able to review" - assert @plan.readable_by?(usr), "expected the reviewer to be able to comment" - end - - # --------------------------------------------------- - test "name returns the title" do - assert_equal @plan.title, @plan.name - end - - # --------------------------------------------------- - test "owner returns the creator" do - @plan.assign_creator(@creator) - - # TODO: Investigate whether or not this should pass. It seems logical that the creator should be the owner by - # default but perhaps there is a use-case for someone creating plans for another user - #assert_equal @creator, @plan.owner, "expected the owner to match the creator" - - plan = Plan.create!(template: Template.last, title: 'Testing no creator', visibility: :is_test) - assert plan.owner.nil?, "expected a new plan with no creator assigned to return nil" - end - - # --------------------------------------------------- - test "returns the shared roles" do - plan = Plan.create!(template: Template.last, title: 'Testing no creator', visibility: :is_test) - # plan created creator, admin and commenter roles (15, 14, 8) - plan.assign_creator(@creator) - Role.create(user: User.first, plan: plan, access: 14) - Role.create(user: User.last, plan: plan, access: 8) - # assert that the plan is shared with above roles and doesnt include owner - assert_equal(plan.shared, true) - end - - # --------------------------------------------------- - test "checks that last edited matches the latest_update" do - now = Time.new - @template.phases.last.updated_at = now - assert_equal now.to_date, @plan.last_edited - end - - # --------------------------------------------------- - test "can CRUD Plan" do - obj = Plan.create(title: 'Testing CRUD', template: Template.where.not(id: @template.id).first, visibility: :is_test, - roles: [Role.new(user: User.last, creator: true)], description: "should change") - assert_not obj.id.nil?, "was expecting to be able to create a new Plan! - #{obj.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - - obj.description = 'changed' - obj.save! - obj.reload - assert_equal 'changed', obj.description, "Was expecting to be able to update the title of the Plan!" - - assert obj.destroy!, "Was unable to delete the Plan!" - end - - # --------------------------------------------------- - test "can manage has_many relationship with Answers" do - a = Answer.new(user: User.last, plan: @plan, question: @plan.questions.first, text: 'Test!') - verify_has_many_relationship(@plan, a, @plan.answers.count) - end - - # --------------------------------------------------- - test "can manage has_many relationship with Role" do - role = Role.new(user: User.first, editor: true) - verify_has_many_relationship(@plan, role, @plan.roles.count) - end - - # --------------------------------------------------- - test "can manage has_many relationship with ExportedPlan" do - ep = ExportedPlan.create(format: ExportedPlan::VALID_FORMATS.last) - verify_has_many_relationship(@plan, ep, @plan.exported_plans.count) - end - - # --------------------------------------------------- - test "can manage belongs_to relationship with Template" do - plan = Plan.new(title: 'Tester', visibility: :is_test) - verify_belongs_to_relationship(plan, Template.first) - end - - # --------------------------------------------------- - test "owner_and_coowners returns the correct users" do - usrs = @plan.owner_and_coowners - assert_equal 2, usrs.length, "expected only 2 users" - usrs.each do |usr| - assert [@creator, @administrator].include?(usr), "expected only the creator and co-owner but found #{usr.email}" - end - end - - # --------------------------------------------------- - test "can request feedback" do - scaffold_org_admin(@creator.org) - - @plan.request_feedback(@creator) - assert @plan.feedback_requested, "expected the feedback flag to be set to true" - assert @plan.reviewable_by?(@user), "expected the Org Admin to be a reviewer" - end - - # --------------------------------------------------- - test "can complete feedback" do - scaffold_org_admin(@creator.org) - val = Role.access_values_for(:reviewer, :commenter).min - @plan.feedback_requested = true - @plan.roles << Role.new(user: @user, access: val) - @plan.save! - - @plan.complete_feedback(@user) - assert_not @plan.feedback_requested, "expected the feedback flag to be set to false" - assert_not @plan.reviewable_by?(@user), "expected the Org Admin to no longer be a reviewer" - end -end diff --git a/test/unit/question_format_test.rb b/test/unit/question_format_test.rb deleted file mode 100644 index 89597c0..0000000 --- a/test/unit/question_format_test.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'test_helper' - -class QuestionFormatTest < ActiveSupport::TestCase - - def setup - @question = Question.first - end - - # --------------------------------------------------- - test "required fields are required" do - assert_not QuestionFormat.new.valid? - assert_not QuestionFormat.new(description: 'Random Number').valid? - - assert QuestionFormat.new(title: 'Random').valid? - assert QuestionFormat.new(title: 'Random', description: 'Random Number').valid? - end - - # --------------------------------------------------- - test "abbreviation must be unique" do - assert_not QuestionFormat.new(title: QuestionFormat.first.title).valid? - end - - # --------------------------------------------------- - test "to_s should return the title" do - assert_equal QuestionFormat.first.title, QuestionFormat.first.to_s - end - - # --------------------------------------------------- - test "can CRUD" do - qf = QuestionFormat.create(title: 'Random', description: 'Random Number') - assert_not qf.id.nil?, "was expecting to be able to create a new QuestionFormat : #{qf.errors.collect{ |e| e }.join(', ')}" - - qf.description = 'Random String' - qf.save! - assert_equal 'Random String', qf.reload.description, "was expecting the description to have been updated!" - - assert qf.destroy!, "Was unable to delete the QuestionFormat!" - end - - # --------------------------------------------------- - test "can manage has_many relationship with Questions" do - qf = QuestionFormat.new(title: 'Random', description: 'Random Number') - verify_has_many_relationship(qf, @question, qf.questions.count) - end - -end \ No newline at end of file diff --git a/test/unit/question_option_test.rb b/test/unit/question_option_test.rb deleted file mode 100644 index 71f1611..0000000 --- a/test/unit/question_option_test.rb +++ /dev/null @@ -1,53 +0,0 @@ -require 'test_helper' - -class QuestionOptionTest < ActiveSupport::TestCase - include GlobalHelpers - - setup do - @user = User.first - - @question = QuestionFormat.find_by(option_based: true).questions.first - - @plan = Plan.create(title: 'Test Plan', template: @question.section.phase.template, visibility: :privately_visible) - - @option = QuestionOption.create(question: @question, text: 'Test QuestionOption', number: 1) - end - - # --------------------------------------------------- - test "required fields are required" do - assert_not QuestionOption.new.valid? - assert_not QuestionOption.new(question: @question, text: 'Test').valid?, "expected the 'number' field to be required" - assert_not QuestionOption.new(question: @question, number: 1).valid?, "expected the 'text' and 'number' field to be required" - assert_not QuestionOption.new(text: 'Test', number: 1).valid?, "expected the 'question' and 'number' field to be required" - - # Ensure the bare minimum and complete versions are valid - a = QuestionOption.new(question: @question, text: 'Test', number: 1) - assert a.valid?, "expected the 'text', 'question' and 'number fields to be enough to create an QuestionOption! - #{a.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - end - - # --------------------------------------------------- - test "can CRUD Guidance" do - obj = QuestionOption.create(question: @question, text: 'Test', number: 1) - assert_not obj.id.nil?, "was expecting to be able to create a new QuestionOption! #{obj.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - - obj.text = 'Testing an update' - obj.save! - obj.reload - assert_equal 'Testing an update', obj.text, "Was expecting to be able to update the text of the QuestionOption!" - - assert obj.destroy!, "Was unable to delete the QuestionOption!" - end - - # --------------------------------------------------- - test "can manage belongs_to relationship with Question" do - option = QuestionOption.new(text: 'Test', number: 1) - verify_belongs_to_relationship(option, @question) - end - - # --------------------------------------------------- - test "can manage has_many relationship with Answers" do - answer = Answer.new(user: @user, plan: @plan, question: @question, text: 'Testing new answer', - question_options: [@question.question_options.first]) - verify_has_many_relationship(@option, answer, @option.answers.count) - end -end \ No newline at end of file diff --git a/test/unit/question_test.rb b/test/unit/question_test.rb deleted file mode 100644 index c6b3066..0000000 --- a/test/unit/question_test.rb +++ /dev/null @@ -1,81 +0,0 @@ -require 'test_helper' - -class QuestionTest < ActiveSupport::TestCase - - setup do - # Need to clear the tables until we get seed.rb out of test_helper.rb - Template.delete_all - @funder = init_funder - @institution = init_institution - @template = init_template(@institution, published: true) - @phase = init_phase(@template) - @section = init_section(@phase) - @question = init_question(@section) - end - - test "required fields are required" do - assert_not Question.new.valid? - assert_not Question.new(section: @section, number: 7).valid?, "expected the 'text' field to be required" - assert_not Question.new(number: 7, text: 'Testing').valid?, "expected the 'section' field to be required" - assert_not Question.new(section: @section, text: 'Testing').valid?, "expected the 'number' field to be required" - - # Ensure the bare minimum and complete versions are valid - a = Question.new(section: @section, text: 'Testing', number: 7) - assert a.valid?, "expected the 'text', 'section' and 'number' fields to be enough to create an Question! - #{a.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - end - - test "to_s returns the Question text" do - assert_equal @question.text, @question.to_s - end - - test "option_based? returns the correct boolean value" do - assert_not @question.option_based? -# TODO: replace with a call to the init_question_format factory method once seeds.rb is no longer being loaded - @question.question_format = QuestionFormat.find_by(option_based: true) - @question.save! - assert @question.option_based? - end - - test "#deep_copy creates a new question object and attaches new annotations/question_options objects" do - init_annotation(@institution, @question) - init_question_option(@question) - assert_deep_copy(@question, @question.deep_copy, relations: [:annotations, :question_options]) - end - -# TODO: This method should get moved to a view helper instead - test "returns the correct themed guidance for the org" do - theme = init_theme - guidance_group = init_guidance_group(@institution) - funder_guidance_group = init_guidance_group(@funder, { title: 'Test funder guidance group' } ) - guidance = init_guidance(guidance_group, { themes: [theme] }) - funder_guidance = init_guidance(funder_guidance_group, { themes: [theme] }) - - @question.themes << theme - @question.save! - - institution_guidances = @question.guidance_for_org(@institution) - # method retuns a hash {'descriptive string': 'guidances array'} - assert_equal 1, institution_guidances.length - assert_equal guidance, institution_guidances.first.last - - funder_guidances = @question.guidance_for_org(@funder) - # method retuns a hash {'descriptive string', 'guidances array'} - assert_equal 1, funder_guidances.length - assert_equal funder_guidance, funder_guidances.first.last - end - - # --------------------------------------------------- - test "returns the correct annotation for the org" do - annotation = init_annotation(@institution, @question, { type: Annotation.types[:example_answer] }) - annotation2 = init_annotation(@institution, @question) - funder_annotation = init_annotation(@funder, @question, { text: 'Test funder example answer', type: Annotation.types[:example_answer] } ) - funder_annotation2 = init_annotation(@funder, @question, { text: 'Test funder guidance'} ) - - institutional_annotations = @question.get_example_answers(@institution) - assert_equal 1, institutional_annotations.length - assert_equal annotation, institutional_annotations.first - funder_annotations = @question.get_example_answers(@funder) - assert_equal 1, funder_annotations.length - assert_equal funder_annotation, funder_annotations.first - end -end diff --git a/test/unit/region_test.rb b/test/unit/region_test.rb deleted file mode 100644 index 422f8fd..0000000 --- a/test/unit/region_test.rb +++ /dev/null @@ -1,55 +0,0 @@ -require 'test_helper' - -class RegionTest < ActiveSupport::TestCase - - setup do - @region = Region.create(name: 'Test Super Region', abbreviation: 'TSR', description: 'Testing') - - @region.sub_regions = [Region.new(name: 'Test Sub Region 1'), Region.new(name: 'Test Sub Region 2')] - end - - # --------------------------------------------------- - test "required fields are required" do - assert_not Region.new.valid? - - # Ensure the bar minimum and complete versions are valid - a = Region.new(name: 'Test Region') - assert a.valid?, "expected the 'name' field to be enough to create an Region! - #{a.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - end - - # --------------------------------------------------- - test "name and abbreviation should be unique" do - assert_not Region.new(name: 'Test Super Region').valid?, "expected that 'name' must be unique" - assert_not Region.new(abbreviation: 'TSR').valid?, "expected that 'abbreviation' must be unique" - - assert_not Region.new(name: 'Test Super Region', abbreviation: '123').valid?, "expected that 'name' must be unique even if abbreviation is unique" - assert_not Region.new(abbreviation: 'TSR', name: 'test super').valid?, "expected that 'abbreviation' must be unique even if name is unique" - end - - # --------------------------------------------------- - test "can CRUD Region" do - obj = Region.create(name: 'Test Region') - assert_not obj.id.nil?, "was expecting to be able to create a new Region: #{obj.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - - obj.abbreviation = 'ABC' - obj.save! - obj.reload - assert_equal 'ABC', obj.abbreviation, "Was expecting to be able to update the text of the Region!" - - assert obj.destroy!, "Was unable to delete the Region!" - end - - # Need to roll our own here because of the name of the relationship attributes - # --------------------------------------------------- - test "can manage has_and_belongs_to_many relationship with Region" do - count = Region.first.sub_regions.count - - @region.super_region = Region.first - @region.save! - - # Search the parent for the child - assert Region.first.sub_regions.include?(@region), "was expecting the Region.sub_regions to contain the test region" - assert_equal (count + 1), Region.first.sub_regions.count - end - -end \ No newline at end of file diff --git a/test/unit/role_test.rb b/test/unit/role_test.rb deleted file mode 100644 index f385627..0000000 --- a/test/unit/role_test.rb +++ /dev/null @@ -1,85 +0,0 @@ -require 'test_helper' - -class RoleTest < ActiveSupport::TestCase - - setup do - @user = User.last - - scaffold_plan - - @role = Role.create(user: User.first, plan: @plan, access: 15) - end - - # --------------------------------------------------- - test "required fields are required" do - assert_not Role.new.valid? - assert_not Role.new(plan: Plan.first, access: 1).valid?, "expected the 'user' field to be required" - assert_not Role.new(user: @user, access: 1).valid?, "expected the 'plan' field to be required" - - # Ensure the bar minimum and complete versions are valid - plan = Plan.create(title: 'Test Plan', template: Template.last, visibility: :is_test) - a = Role.new(user: @user, plan: plan, access: 15) - assert a.valid?, "expected the 'user', 'plan' and 'access' fields to be enough to create an Role! - #{a.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - end - - # --------------------------------------------------- - test "access is properly defaulted" do - assert_equal 15, @role.access - end - - # --------------------------------------------------- - test "FlagShihTzu bit flag fields are properly mapped" do - assert @role.creator?, "expected the role to be creator" - - @role.administrator = true - assert @role.administrator?, "expected the role to be administrator after setting 'administrator'" - @role.administrator = false - - [1, 3, 5, 7, 9, 11, 13, 15].each do |a| - @role.access = a - assert @role.creator?, "expected the role to be creator after setting 'access_level' >= #{a}" - end - - [2, 3, 6, 7, 10, 11, 14, 15].each do |a| - @role.access = a - assert @role.administrator?, "expected the role to be administrator after setting 'access_level' >= #{a}" - end - - [4, 5, 6, 7, 12, 13, 14, 15].each do |a| - @role.access = a - assert @role.editor?, "expected the role to be editor after setting 'access_level' >= #{a}" - end - - [8, 9, 10, 11, 12, 13, 14, 15].each do |a| - @role.access = a - assert @role.commenter?, "expected the role to be commenter after setting 'access_level' >= #{a}" - end - end - - # --------------------------------------------------- - test "can CRUD Role" do - plan = Plan.create(title: 'Test Plan', template: Template.last, visibility: :is_test) - obj = Role.create(user: @user, plan: plan, access: 1) - assert_not obj.id.nil?, "was expecting to be able to create a new Role: #{obj.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - - obj.access = 2 - obj.save! - obj.reload - assert_equal 2, obj.access, "Was expecting to be able to update the text of the Role!" - - assert obj.destroy!, "Was unable to delete the Role!" - end - - # --------------------------------------------------- - test "can manage belongs_to relationship with User" do - role = Role.new(plan: Plan.first, access: 3) - verify_belongs_to_relationship(role, User.first) - end - - # --------------------------------------------------- - test "can manage belongs_to relationship with Plan" do - role = Role.new(user: User.first, access: 3) - verify_belongs_to_relationship(role, Plan.first) - end - -end \ No newline at end of file diff --git a/test/unit/section_test.rb b/test/unit/section_test.rb deleted file mode 100644 index b14d556..0000000 --- a/test/unit/section_test.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'test_helper' - -class SectionTest < ActiveSupport::TestCase - - setup do - # Need to clear the tables until we get seed.rb out of test_helper.rb - Template.delete_all - funder = init_funder - template = init_template(funder, published: true) - @phase = init_phase(template) - @section = init_section(@phase) - end - - test "required fields are required" do - assert_not Section.new.valid? - assert_not Section.new(phase: @phase, number: 9).valid?, "expected the 'title' field to be required" - assert_not Section.new(title: 'Tester', number: 9).valid?, "expected the 'phase' field to be required" - assert_not Section.new(phase: @phase, title: 'Tester').valid?, "expected the 'number' field to be required" - - # Ensure the bare minimum and complete versions are valid - a = Section.new(phase: @phase, title: 'Tester', number: 9) - assert a.valid?, "expected the 'phase', 'title' and 'number' fields to be enough to create an Section! - #{a.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - end - - test "to_s returns the title" do - assert_equal @section.title, @section.to_s - end - - test "#deep_copy creates a new section object and attaches new question objects" do - assert_deep_copy(@section, @section.deep_copy, relations: [:questions]) - end - - test "default values are properly set on section creation" do - assert(@section.modifiable, 'expected a new section to be modifiable') - end -end \ No newline at end of file diff --git a/test/unit/settings/plan_test.rb b/test/unit/settings/plan_test.rb deleted file mode 100644 index a654851..0000000 --- a/test/unit/settings/plan_test.rb +++ /dev/null @@ -1,217 +0,0 @@ -require 'test_helper' - -module Settings - class PlanTest < ActiveSupport::TestCase - - setup do - @org = Org.last - - scaffold_plan - end - - def settings(extras = {}) - {margin: (@margin || { top: 10, bottom: 10, left: 10, right: 10 }), - font_face: (@font_face || Settings::Template::VALID_FONT_FACES.first), - font_size: (@font_size || 11) - }.merge(extras) - end - - def default_formatting - Settings::Template::DEFAULT_SETTINGS[:formatting] - end - - # --------------------------------------------------- - test "settings should use defaults if none are defined" do - assert(!@plan.settings(:export).value?) - assert_equal(default_formatting, @plan.settings(:export).formatting) - end - - # --------------------------------------------------- - test "settings should use defined valid settings" do - @plan.settings(:export).formatting = settings - @plan.save! - - assert(@plan.settings(:export).value?) - assert_equal(settings, @plan.settings(:export).formatting) - assert_not_equal(default_formatting, @plan.settings(:export).formatting) - end - - # --------------------------------------------------- - test "setting negative margin should not be valid" do - @margin = { top: -10, bottom: 10, left: 10, right: 10 } - - @plan.settings(:export).formatting = settings - - assert(!@plan.valid?) - assert(!@plan.save) - - assert_equal(_('Margin cannot be negative'), - @plan.errors.messages[:'template.setting_objects.formatting'].first) - - @plan.reload - assert_equal(default_formatting, @plan.settings(:export).formatting) - end - - # --------------------------------------------------- - test "setting unknown margin should not be valid" do - @margin = { top: 10, bottom: 10, left: 10, right: 10, top_left: 10 } - - @plan.settings(:export).formatting = settings - - assert(!@plan.valid?) - assert(!@plan.save) - - assert_equal(_('Unknown margin. Can only be \'top\', \'bottom\', \'left\' or \'right\''), - @plan.errors.messages[:'template.setting_objects.formatting'].first) - - @plan.reload - assert_equal(default_formatting, @plan.settings(:export).formatting) - end - - # --------------------------------------------------- - test "setting negative font-size should not be valid" do - @font_size = -11 - - @plan.settings(:export).formatting = settings - - assert(!@plan.valid?) - assert(!@plan.save) - - assert_equal(_('Invalid font size'), - @plan.errors.messages[:"template.setting_objects.formatting"].first) - - @plan.reload - - assert_equal(default_formatting, @plan.settings(:export).formatting) - end - - # --------------------------------------------------- - test "setting unknown key should not be valid" do - @plan.settings(:export).formatting = settings(foo: :bar) - - assert(!@plan.valid?) - assert(!@plan.save) - - assert_equal(_('Unknown formatting setting'), - @plan.errors.messages[:'template.setting_objects.formatting'].first) - - @plan.reload - - assert_equal(default_formatting, @plan.settings(:export).formatting) - end - - # --------------------------------------------------- - test "not setting font_face should not be valid" do - @plan.settings(:export).formatting = settings.reject {|k,v| k == :font_face } - - assert(!@plan.valid?) - assert(!@plan.save) - - assert_equal(_('A required setting has not been provided'), - @plan.errors.messages[:'template.setting_objects.formatting'].first) - - @plan.reload - - assert_equal(default_formatting, @plan.settings(:export).formatting) - end - - # --------------------------------------------------- - test "not setting font_size should not be valid" do - @plan.settings(:export).formatting = settings.reject {|k,v| k == :font_size } - - assert(!@plan.valid?) - assert(!@plan.save) - - assert_equal(_('A required setting has not been provided'), - @plan.errors.messages[:'template.setting_objects.formatting'].first) - - @plan.reload - - assert_equal(default_formatting, @plan.settings(:export).formatting) - end - - # --------------------------------------------------- - test "not setting margin should not be valid" do - @plan.settings(:export).formatting = settings.reject {|k,v| k == :margin } - - assert(!@plan.valid?) - assert(!@plan.save) - - assert_equal(_('A required setting has not been provided'), - @plan.errors.messages[:'template.setting_objects.formatting'].first) - - @plan.reload - - assert_equal(default_formatting, @plan.settings(:export).formatting) - end - - # --------------------------------------------------- - test "setting non-hash as margin should not be valid" do - @margin = :foo - - @plan.settings(:export).formatting = settings - - assert(!@plan.valid?) - assert(!@plan.save) - - assert_equal(_('Margin value is invalid'), - @plan.errors.messages[:'template.setting_objects.formatting'].first) - - @plan.reload - - assert_equal(default_formatting, @plan.settings(:export).formatting) - end - - # --------------------------------------------------- - test "setting non-integer as font_size should not be valid" do - @font_size = "foo" - - @plan.settings(:export).formatting = settings - - assert(!@plan.valid?) - assert(!@plan.save) - - assert_equal(_('Invalid font size'), - @plan.errors.messages[:'template.setting_objects.formatting'].first) - - @plan.reload - - assert_equal(default_formatting, @plan.settings(:export).formatting) - end - - # --------------------------------------------------- - test "setting non-string as font_face should not be valid" do - @font_face = 1 - - @plan.settings(:export).formatting = settings - - assert(!@plan.valid?) - assert(!@plan.save) - - assert_equal(_('Invalid font face'), - @plan.errors.messages[:'template.setting_objects.formatting'].first) - - @plan.reload - - assert_equal(default_formatting, @plan.settings(:export).formatting) - end - - # --------------------------------------------------- - test "setting unknown string as font_face should not be valid" do - @font_face = 'Monaco, Monospace, Sans-Serif' - - @plan.settings(:export).formatting = settings - - assert(!@plan.valid?) - assert(!@plan.save) - - assert_equal(_('Invalid font face'), - @plan.errors.messages[:'template.setting_objects.formatting'].first) - - @plan.reload - - assert_equal(default_formatting, @plan.settings(:export).formatting) - end - - end -end \ No newline at end of file diff --git a/test/unit/settings/template_test.rb b/test/unit/settings/template_test.rb deleted file mode 100644 index a65e8ca..0000000 --- a/test/unit/settings/template_test.rb +++ /dev/null @@ -1,217 +0,0 @@ -require 'test_helper' - -module Settings - class TemplateTest < ActiveSupport::TestCase - - setup do - @org = Org.last - - scaffold_template - end - - def settings(extras = {}) - {margin: (@margin || { top: 10, bottom: 10, left: 10, right: 10 }), - font_face: (@font_face || Settings::Template::VALID_FONT_FACES.first), - font_size: (@font_size || 11) - }.merge(extras) - end - - def default_formatting - Settings::Template::DEFAULT_SETTINGS[:formatting] - end - - # --------------------------------------------------- - test "settings should use defaults if none are defined" do - assert(!@template.settings(:export).value?) - assert_equal(default_formatting, @template.settings(:export).formatting) - end - - # --------------------------------------------------- - test "settings should use defined valid settings" do - @template.settings(:export).formatting = settings - @template.save! - - assert(@template.settings(:export).value?) - assert_equal(settings, @template.settings(:export).formatting) - assert_not_equal(default_formatting, @template.settings(:export).formatting) - end - - # --------------------------------------------------- - test "setting negative margin should not be valid" do - @margin = { top: -10, bottom: 10, left: 10, right: 10 } - - @template.settings(:export).formatting = settings - - assert(!@template.valid?) - assert(!@template.save) - - assert_equal(_('Margin cannot be negative'), - @template.errors.messages[:'setting_objects.formatting'].first) - - @template.reload - assert_equal(default_formatting, @template.settings(:export).formatting) - end - - # --------------------------------------------------- - test "setting unknown margin should not be valid" do - @margin = { top: 10, bottom: 10, left: 10, right: 10, top_left: 10 } - - @template.settings(:export).formatting = settings - - assert(!@template.valid?) - assert(!@template.save) - - assert_equal(_('Unknown margin. Can only be \'top\', \'bottom\', \'left\' or \'right\''), - @template.errors.messages[:'setting_objects.formatting'].first) - - @template.reload - assert_equal(default_formatting, @template.settings(:export).formatting) - end - - # --------------------------------------------------- - test "setting negative font-size should not be valid" do - @font_size = -11 - - @template.settings(:export).formatting = settings - - assert(!@template.valid?) - assert(!@template.save) - - assert_equal(_('Invalid font size'), - @template.errors.messages[:'setting_objects.formatting'].first) - - @template.reload - - assert_equal(default_formatting, @template.settings(:export).formatting) - end - - # --------------------------------------------------- - test "setting unknown key should not be valid" do - @template.settings(:export).formatting = settings(foo: :bar) - - assert(!@template.valid?) - assert(!@template.save) - - assert_equal(_('Unknown formatting setting'), - @template.errors.messages[:'setting_objects.formatting'].first) - - @template.reload - - assert_equal(default_formatting, @template.settings(:export).formatting) - end - - # --------------------------------------------------- - test "not setting font_face should not be valid" do - @template.settings(:export).formatting = settings.reject {|k,v| k == :font_face } - - assert(!@template.valid?) - assert(!@template.save) - - assert_equal(_('A required setting has not been provided'), - @template.errors.messages[:'setting_objects.formatting'].first) - - @template.reload - - assert_equal(default_formatting, @template.settings(:export).formatting) - end - - # --------------------------------------------------- - test "not setting font_size should not be valid" do - @template.settings(:export).formatting = settings.reject {|k,v| k == :font_size } - - assert(!@template.valid?) - assert(!@template.save) - - assert_equal(_('A required setting has not been provided'), - @template.errors.messages[:'setting_objects.formatting'].first) - - @template.reload - - assert_equal(default_formatting, @template.settings(:export).formatting) - end - - # --------------------------------------------------- - test "not setting margin should not be valid" do - @template.settings(:export).formatting = settings.reject {|k,v| k == :margin } - - assert(!@template.valid?) - assert(!@template.save) - - assert_equal(_('A required setting has not been provided'), - @template.errors.messages[:'setting_objects.formatting'].first) - - @template.reload - - assert_equal(default_formatting, @template.settings(:export).formatting) - end - - # --------------------------------------------------- - test "setting non-hash as margin should not be valid" do - @margin = :foo - - @template.settings(:export).formatting = settings - - assert(!@template.valid?) - assert(!@template.save) - - assert_equal(_('Margin value is invalid'), - @template.errors.messages[:'setting_objects.formatting'].first) - - @template.reload - - assert_equal(default_formatting, @template.settings(:export).formatting) - end - - # --------------------------------------------------- - test "setting non-integer as font_size should not be valid" do - @font_size = "foo" - - @template.settings(:export).formatting = settings - - assert(!@template.valid?) - assert(!@template.save) - - assert_equal(_('Invalid font size'), - @template.errors.messages[:'setting_objects.formatting'].first) - - @template.reload - - assert_equal(default_formatting, @template.settings(:export).formatting) - end - - # --------------------------------------------------- - test "setting non-string as font_face should not be valid" do - @font_face = 1 - - @template.settings(:export).formatting = settings - - assert(!@template.valid?) - assert(!@template.save) - - assert_equal(_('Invalid font face'), - @template.errors.messages[:'setting_objects.formatting'].first) - - @template.reload - - assert_equal(default_formatting, @template.settings(:export).formatting) - end - - # --------------------------------------------------- - test "setting unknown string as font_face should not be valid" do - @font_face = 'Monaco, Monospace, Sans-Serif' - - @template.settings(:export).formatting = settings - - assert(!@template.valid?) - assert(!@template.save) - - assert_equal(_('Invalid font face'), - @template.errors.messages[:'setting_objects.formatting'].first) - - @template.reload - - assert_equal(default_formatting, @template.settings(:export).formatting) - end - - end -end \ No newline at end of file diff --git a/test/unit/template_test.rb b/test/unit/template_test.rb deleted file mode 100644 index 78401f0..0000000 --- a/test/unit/template_test.rb +++ /dev/null @@ -1,427 +0,0 @@ -require 'test_helper' - -class TemplateTest < ActiveSupport::TestCase - - setup do - # Need to clear the tables until we get seed.rb out of test_helper.rb - Template.destroy_all - - @funder = init_funder - @org = init_organisation - @institution = init_institution - @funder_org = init_funder_organisation - - @basic_template = init_template(@funder, published: true) - end - - def init_full_template(template) - phase = init_phase(template) - section = init_section(phase) - init_question(section) - return template - end - - def settings(extras = {}) - {margin: (@margin || { top: 10, bottom: 10, left: 10, right: 10 }), - font_face: (@font_face || Settings::Template::VALID_FONT_FACES.first), - font_size: (@font_size || 11) - }.merge(extras) - end - - def default_formatting - Settings::Template::DEFAULT_SETTINGS[:formatting] - end - - test "default values are properly set on template creation" do - template = init_template(@funder) - assert_equal false, template.published, 'expected a new template to not be published' - assert_equal false, template.archived, 'expected a new template to not be archived' - assert_not_nil template.family_id, 'expected a new template to have a family_id' - assert_equal false, template.is_default, 'expected a new template to not be the default template' - assert template.publicly_visible?, 'expected a new funder template to be publicly visible' - - tmplt = init_template(@org) - tmplt2 = init_template(@funder_org) - assert tmplt.organisationally_visible?, 'expected a new non-funder template to be organisationally visible' - assert tmplt2.organisationally_visible?, 'expected a new non-funder template to be organisationally visible' - end - - test "required fields are required" do - assert_not Template.new.valid? - assert_not Template.new(version: 1, title: 'Tester').valid?, "expected the 'org' field to be required" - assert_not Template.new(org: @funder, version: 1).valid?, "expected the 'title' field to be required" - end - - test "unarchived returns only unarchived templates" do - # Create an unarchived and an archived template (set archived after init because it will default to false on creation) - archived = init_template(@funder, { title: 'Archived Template' }) - archived.update_attributes(archived: true) - results = Template.unarchived - assert_equal 1, results.length, 'expected there to be only 1 unarchived template' - assert_equal @basic_template, results.first, 'expected the correct template to have been returned' - end - - test "archived returns only archived templates" do - # Create an unarchived and an archived template (set archived after init because it will default to false on creation) - archived = init_template(@funder, { title: 'Archived Template' }) - archived.update_attributes(archived: true) - results = Template.archived - assert_equal 1, results.length, 'expected there to be only 1 archived template' - assert_equal archived, results.first, 'expected the correct template to have been returned' - end - - test "able to determine the latest version number" do - version2 = @basic_template.generate_version! - version2.save! - results = Template.latest_version_per_family(@basic_template.family_id) - assert_equal 1, results.length, 'expected only one version to be returned for the specific family' - assert_equal version2.version, results.first.version, 'expected the new version' - end - - test "able to retrieve the latest version" do - version2 = @basic_template.generate_version! - version2.save! - result = Template.latest_version(@basic_template.family_id) - assert_equal 1, result.length, 'expected only one version to be returned' - assert_equal version2, result.first, 'expected the new version' - end - - test "able to version a template" do - template = init_full_template(@basic_template) - assert_equal 0, template.version, 'expected the initial template version to be zero' - version2 = template.generate_version! - assert_equal 1, version2.version, 'expected the version number to be one more than the original template\'s' - assert_equal template.family_id, version2.family_id, 'expected the new version to have the same family_id' - assert_equal template.visibility, version2.visibility, 'expected the new version to have the same visibility' - assert_equal template.is_default, version2.is_default, 'expected the new version to have the same default flag' - assert_equal false, version2.archived, 'expected the new version to no be archived' - # All components were transferred over to the new version - assert_equal template.phases.length, version2.phases.length, 'expected the new version to have the same number of phases' - template.phases.each_with_index do |phase, idx| - assert_phases_equal(phase, version2.phases[idx]) - end - end - - test "#generate_copy! raises RuntimeError when a non Org object is passed" do - init_full_template(@basic_template) - exception = assert_raises(RuntimeError) do - @basic_template.generate_copy!(nil) - end - assert_equal(_('generate_copy! requires an organisation target'), exception.message) - end - - test "#generate_copy! creates a new copy of a template" do - template = init_full_template(@basic_template) - template.update_attributes(is_default: true, published: true) # Update these flags to verify that the copy sets them properly - copy = template.generate_copy!(@institution) - assert_not_equal template.id, copy.id, 'expecetd the copy to have a different id' - assert_not_equal template.family_id, copy.family_id, 'expected the copy to have a different family id' - assert_equal @institution, copy.org, 'expected the copy to have the correct Org' - assert_equal 0, copy.version, 'expected the copy\'s version number to be zero' - assert_not copy.published?, 'expected the copy to not be published' - assert_not copy.is_default?, 'expected the copy to not be the default template' - assert_equal 'organisationally_visible', copy.visibility, 'expected the visibility to be organisational' - assert_equal 'Copy of %{template}' % { template: template.title }, copy.title, 'expected the template title to be "Copy of %{template}"' - assert_equal template.description, copy.description, 'expected the template descriptions to match' - assert_equal template.phases.length, copy.phases.length, 'expected the copy to have the same number of phases' - template.phases.each_with_index do |phase, idx| - assert_phases_equal(phase, copy.phases[idx]) - end - end - - test "can properly determine if current template is the latest version" do - assert @basic_template.latest?, 'expected the initial template to be the latest version' - version2 = @basic_template.generate_version! - version2.save! - assert_not @basic_template.latest?, 'expected the initial template to no longer be the latest version' - assert version2.latest?, 'expected the new version to be the latest version' - end - - test "#customize! raises RuntimeError when a non Org object is passed" do - init_full_template(@basic_template) - exception = assert_raises(RuntimeError) do - @basic_template.customize!(nil) - end - assert_equal(_('customize! requires an organisation target'), exception.message) - end - - test "#customize! raises RuntimeError when the template belongs to a non funder" do - template = init_template(@org, published: true) - exception = assert_raises(StandardError) do - template.customize!(@institution) - end - end - - test "#customize! allows user to customize the default template" do - template = init_template(@org, published: true, is_default: true) - customization = template.customize!(@institution) - assert_not_nil(customization) - end - - test "#customize! generates a new template" do - template = init_full_template(@basic_template) - customization = template.customize!(@institution) - assert(customization.family_id.present?, 'expected a newly family_id value') - assert_equal(template.family_id, customization.customization_of, 'expected the customization_of id to match the base template\'s family_id') - assert_equal(0, customization.version, 'expected the initial customization version to be zero') - assert_equal(@institution, customization.org, 'expected the customizatio\'s org to match the one specified') - assert_not(customization.published, 'expected the customization to not be published') - assert_equal('organisationally_visible', customization.visibility, 'expected the customization\'s visibility to be organisationally visible') - assert_not(customization.is_default, 'expected the customization to not be the default template') - - # Following statements go further than checking that the instance method behaves adequately - assert_equal template.phases.length, customization.phases.length, 'expected the customization to have the same number of phases as the base template' - template.phases.each_with_index do |phase, idx| - assert_phases_equal(phase, customization.phases[idx]) - end - end - test "#customize! is thread-safe and therefore only one customization_of/version/org_id record exists in the db" do - template = init_template(@funder, published: true) - await = true - should_assert = true - threads = 3.times.map do |i| - Thread.new do - while await do ; end - begin - template.customize!(@org) - rescue ActiveRecord::StatementInvalid => e - # SQLite only supports one writer at a time. (e.g. https://www.sqlite.org/rescode.html#busy) - should_assert = false if e.message.include?("SQLite3::BusyException") - end - end - end - await = false - threads.map(&:join) - # ActiveRecord::Base.connection.adapter_name != 'SQLite' - assert_equal(1, Template.where(customization_of: template.family_id, version: 0, org_id: @org.id).count) if should_assert - end - - test "template customizations can be transferred after base template changes" do - init_full_template(@basic_template) - customization = @basic_template.customize!(@institution) - first_question = customization.phases.first.sections.first.questions.first - init_annotation(customization.org, first_question) - customization.save! - end - - test "base_org returns the current template org if the template is not customized" do - assert_equal @basic_template.org, @basic_template.base_org, 'expected an uncustomized template to consider its own org the base_org' - end - test "base_org returns the parent template org if the template is customized" do - customization = @basic_template.customize!(@institution) - assert_equal @basic_template.org, customization.base_org, 'expected a customized template to consider the parent template\'s org the base_org' - end - - test "#generate_version! raises RuntimeError when the template is not published" do - template = init_template(@org, published: false) - exception = assert_raises(RuntimeError) do - template.generate_version! - end - assert_equal(_('generate_version! requires a published template'), exception.message) - end - - test "#generate_version! creates a new version for a published and non-customised template" do - template = init_template(@org, published: true) - new_template = template.generate_version! - assert_equal(@basic_template.version + 1, new_template.version) - assert_not(new_template.published) - end - - test "#generate_version! is thread-safe and therefore only one family_id/version record exists in the db" do - template = init_template(@org, published: true) - await = true - should_assert = true - threads = 3.times.map do |i| - Thread.new do - while await do ; end - begin - template.generate_version! - rescue ActiveRecord::StatementInvalid => e - # SQLite only supports one writer at a time. (e.g. https://www.sqlite.org/rescode.html#busy) - should_assert = false if e.message.include?("SQLite3::BusyException") - end - end - end - await = false - threads.map(&:join) - assert_equal(1, Template.where(family_id: template.family_id, version: 1).count) if should_assert - end - - test "#upgrade_customization! raises RuntimeError when the template is not a customisation of another template" do - template = init_template(@org, published: true) - exception = assert_raises(RuntimeError) do - template.upgrade_customization! - end - assert_equal(_('upgrade_customization! requires a customised template'), exception.message) - end - - test "#upgrade_customization! creates a new version" do - customization = @basic_template.customize!(@institution) - customization.published = true - transferred = customization.upgrade_customization! - assert_equal(customization.version + 1, transferred.version, 'expected the version number to have been incremented when the current cusomization was published') - assert_equal(customization.family_id, transferred.family_id, 'expected the family_id to be retained when upgrade_customization! is called') - end - - test "#upgrade_customization! appends modifiable phases to the new customisation" do - init_full_template(@basic_template) - customization = @basic_template.customize!(@institution) - customization.phases << Phase.new(title: 'New customised phase', number: 2, modifiable: true) - customization.phases << Phase.new(title: 'New customised phase 2', number: 3, modifiable: true) - - transferred = customization.upgrade_customization! - assert_not_equal(customization.object_id, transferred.object_id, 'customization and transferred are distinct objects') - assert_equal(3, transferred.phases.length, 'expected 3 phases after upgrading a customised template') - end - - test "#upgrade_customization! appends modifiable sections into an unmodifiable phase" do - init_full_template(@basic_template) - customization = @basic_template.customize!(@institution) - customization.phases.first.sections << Section.new(title: 'New customised section', number: 2, modifiable: true) - customization.phases.first.sections << Section.new(title: 'New customised section 2', number: 3, modifiable: true) - - transferred = customization.upgrade_customization! - assert_not_equal(customization.object_id, transferred.object_id, 'customization and transferred are distinct objects') - assert_equal(3, transferred.phases.first.sections.length, 'expected 3 sections after upgrading a customised template') - end - - test "#upgrade_customization! appends modifiable questions into an unmodifiable section" do - init_full_template(@basic_template) - customization = @basic_template.customize!(@institution) - customization.phases.first.sections.first.questions << Question.new(text: 'New customised question', number: 2, modifiable: true) - customization.phases.first.sections.first.questions << Question.new(text: 'New customised question 2', number: 3, modifiable: true) - - transferred = customization.upgrade_customization! - assert_not_equal(customization.object_id, transferred.object_id, 'customization and transferred are distinct objects') - assert_equal(3, transferred.phases.first.sections.first.questions.length, 'expected 3 questions after upgrading a customised template') - end - - test "#upgrade_customization! appends annotations added to an unmodifiable question" do - init_full_template(@basic_template) - customization = @basic_template.customize!(@institution) - customization.phases.first.sections.first.questions.first.annotations << - Annotation.new(text: 'New customised guidance', type: Annotation.types[:guidance], org: customization.org) - customization.phases.first.sections.first.questions.first.annotations << - Annotation.new(text: 'New customised example_answer', type: Annotation.types[:example_answer], org: customization.org) - - @basic_template.phases.first.sections.first.questions.first.annotations << - Annotation.new(text: 'New funder guidance', type: Annotation.types[:guidance], org: @basic_template.org) - @basic_template.phases.first.sections.first.questions.first.annotations << - Annotation.new(text: 'New funder example_answer', type: Annotation.types[:example_answer], org: @basic_template.org) - - transferred = customization.upgrade_customization! - assert_not_equal(customization.object_id, transferred.object_id, 'customization and transferred are distinct objects') - assert_equal(4, transferred.phases.first.sections.first.questions.first.annotations.length, 'expected 4 annotations after upgrading a customised template') - end - - test "#generate_version? returns true when the template is published" do - @basic_template.published = true - assert(@basic_template.generate_version?) - end - - test "#generate_version? returns false when the template is not published" do - @basic_template.published = false - assert_not(@basic_template.generate_version?) - end - - test "#customize? returns false when no org is passed" do - assert_not(@basic_template.customize?(nil)) - end - - test "#customize? returns false when the template is not owned by a funder or is not the default template" do - template = init_template(@institution, { title: 'institutional template', is_default: false }) - assert_not template.customize?(@funder) - end - - test "#customize? returns true when the org does not have a customization of the template" do - assert(@basic_template.customize?(@institution)) - end - - test "#customize? returns false when the org has already a customization of the template" do - @basic_template.customize!(@institution) - assert_not(@basic_template.customize?(@institution)) - end - - test "#upgrade_customization? returns false when the template is not a customization of another template" do - assert_not(@basic_template.upgrade_customization?) - end - - test "#upgrade_customization? returns false when the template is already according to the latest published funder template" do - @basic_template.published = true - customization = @basic_template.customize!(@institution) - assert_not(customization.upgrade_customization?) - end - - test "#upgrade_customization? returns true when the template is stale, i.e a new version from funder has been published" do - @basic_template.published = true - customization = @basic_template.customize!(@institution) - customization.created_at = customization.created_at.yesterday - new_version = @basic_template.generate_version! - new_version.published = true - new_version.save! - assert(customization.upgrade_customization?) - end - - test "default template retains the correct flags when versioned" do - @basic_template.update!({ org: @org, published: true, is_default: true, visibility: Template.visibilities[:publicly_visible] }) - new_version = @basic_template.generate_version! - assert_not new_version.published?, 'expected the new version to not be published' - assert new_version.is_default?, 'expected the new version to be flagged as the default template' - assert new_version.publicly_visible?, 'expected the new version to be publicly visible' - end - - test " a customization of the default template is not marked as the default" do - @basic_template.update!({ org: @org, published: true, is_default: true, visibility: Template.visibilities[:publicly_visible] }) - customization = @basic_template.customize!(@institution) - assert_not customization.published?, 'expected the customization to not be published' - assert_not customization.is_default?, 'expected the customization to not be flagged as the default template' - assert_not customization.publicly_visible?, 'expected the customization to not be publicly visible' - end - - test "::find_or_generate_version! raises RuntimeError when attempting to retrieve a historical version for being modified" do - @basic_template.generate_version! - exception = assert_raises(RuntimeError) do - Template.find_or_generate_version!(@basic_template) - end - assert_equal(_('A historical template cannot be retrieved for being modified'), exception.message) - end - - test "::find_or_generate_version! does not generate a new version" do - new_template = @basic_template.generate_version! - template = Template.find_or_generate_version!(new_template) - assert_equal(new_template, template) - end - - test "::find_or_generate_version! generates a new version" do - template = Template.find_or_generate_version!(@basic_template) - assert_not_equal(@basic_template, template) - end - - test "publishing a template automatically unpublishes other versions" do - @basic_template.published = true - @basic_template.save! - v2 = @basic_template.generate_version! - v2.published = true - v2.save! - assert v2.reload.published?, 'expected the new version to be published' - assert_not @basic_template.reload.published?, 'expected the old version to be unpublished' - end - - test "draft? returns false for a published template" do - @basic_template.published = true - assert_not @basic_template.draft? - end - - test "draft? returns true for an unpublished version of a template that has a published version" do - @basic_template.published = true - version = @basic_template.generate_version! - assert version.draft? - end - - test "draft? returns false for a template that has no published versions" do - @basic_template.published = true - version = @basic_template.generate_version! - @basic_template.update(published: false) - assert_not version.draft? - end -end diff --git a/test/unit/theme_test.rb b/test/unit/theme_test.rb deleted file mode 100644 index 140af1b..0000000 --- a/test/unit/theme_test.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'test_helper' - -class ThemeTest < ActiveSupport::TestCase - - setup do - @theme = Theme.create(title: 'Test Theme', description: 'My test theme', locale: I18n.locale) - end - - # --------------------------------------------------- - test "required fields are required" do - assert_not Theme.new.valid? - - # Ensure the bare minimum are valid - a = Theme.new(title: 'Tester') - assert a.valid?, "expected the 'title' field to be enough to create an Theme! - #{a.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - end - - # --------------------------------------------------- - test "to_s returns the title" do - assert_equal @theme.title, @theme.to_s - end - - # --------------------------------------------------- - test "can CRUD Theme" do - obj = Theme.create(title: 'Tester') - assert_not obj.id.nil?, "was expecting to be able to create a new Theme!" - - obj.description = 'Testing an update' - obj.save! - obj.reload - assert_equal 'Testing an update', obj.description, "Was expecting to be able to update the description of the Theme!" - - assert obj.destroy!, "Was unable to delete the Theme!" - end - - # --------------------------------------------------- - test "can manage has_many relationship with Question" do - question = Question.new(section: Section.first, text: 'Testing', number: 7) - verify_has_many_relationship(@theme, question, @theme.questions.count) - end - - # --------------------------------------------------- - test "can manage has_many relationship with Guidance" do - guidance = Guidance.new(text: 'Testing') - verify_has_many_relationship(@theme, guidance, @theme.guidances.count) - end - -end diff --git a/test/unit/token_permission_type_test.rb b/test/unit/token_permission_type_test.rb deleted file mode 100644 index 13c3346..0000000 --- a/test/unit/token_permission_type_test.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'test_helper' - -class TokenPermissionTypeTest < ActiveSupport::TestCase - - def setup - @tpt = TokenPermissionType.create(token_type: 'testing', text_description: 'abcd') - end - - # --------------------------------------------------- - test "required fields are required" do - assert_not TokenPermissionType.new.valid?, "was expecting TokenPermissionType.token_type to be required!" - assert TokenPermissionType.new(token_type: 'tester').valid?, "was only expecting TokenPermissionType.token_type to be required!" - end - - # --------------------------------------------------- - test "token_type must be unique" do - assert_not TokenPermissionType.new(token_type: @tpt.token_type).valid?, "was expecting TokenPermissionType.token_type to be unique!" - end - - # --------------------------------------------------- - test "to_s returns the token_type" do - assert_equal @tpt.token_type, @tpt.to_s - end - - # --------------------------------------------------- - test "can CRUD" do - tpt = TokenPermissionType.create(token_type: 'tester') - assert_not tpt.id.nil?, "was expecting to be able to create a new TokenPermissionType - #{tpt.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - - tpt.text_description = 'testing updates' - tpt.save! - assert_equal 'testing updates', tpt.reload.text_description, "was expecting the text_description to have been updated!" - - assert tpt.destroy!, "Was unable to delete the TokenPermissionType!" - end - - # --------------------------------------------------- - test "can manage has_many relationship with Org" do - org = Org.new(name: 'Testing', links: {"org":[]}) - verify_has_many_relationship(@tpt, org, @tpt.orgs.count) - end - -end \ No newline at end of file diff --git a/test/unit/user_identifier_test.rb b/test/unit/user_identifier_test.rb deleted file mode 100644 index 007059a..0000000 --- a/test/unit/user_identifier_test.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'test_helper' - -class UserIdentifierTest < ActiveSupport::TestCase - - def setup - @user = User.first - @scheme = IdentifierScheme.first - end - - # --------------------------------------------------- - test "required fields are required" do - assert_not UserIdentifier.new.valid? - assert_not UserIdentifier.new(user: @user).valid? - assert_not UserIdentifier.new(identifier_scheme: @scheme).valid? - assert_not UserIdentifier.new(identifier: 'TEST').valid? - assert_not UserIdentifier.new(user: @user, identifier_scheme: @scheme).valid? - assert_not UserIdentifier.new(user: @user, identifier: 'TEST').valid? - assert_not UserIdentifier.new(identifier_scheme: @scheme, identifier: 'TEST').valid? - - assert UserIdentifier.new(user: @user, identifier_scheme: @scheme, identifier: 'TEST').valid? - end - - # --------------------------------------------------- - test "can only have one identifier per User/IdentifierScheme" do - ui = UserIdentifier.create(user: @user, identifier_scheme: @scheme, identifier: 'TEST') - - @user.user_identifiers << UserIdentifier.new(identifier_scheme: @scheme, identifier: 'abc') - - assert_not @user.valid?, "Expected to NOT be able to add more than one identifier for the same user/scheme" - assert_equal ui.identifier, @user.user_identifiers.select{ |i| i.identifier_scheme == @scheme }.first.identifier, "Expected the initial identifier to have been retained" - end - -end \ No newline at end of file diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb deleted file mode 100644 index 6f34a9c..0000000 --- a/test/unit/user_test.rb +++ /dev/null @@ -1,373 +0,0 @@ -require 'test_helper' - -class UserTest < ActiveSupport::TestCase - - def setup - scaffold_plan - - @user = User.create(email: 'me@example.edu', - password: 'password', - password_confirmation: 'password', - firstname: 'Test', - surname: 'User', - shibboleth_id: 'test-shib', - accept_terms: 'true', - org: Org.last, - api_token: 'ABC123', - language: Language.find_by(abbreviation: I18n.locale)) - - @notification = Notification.create!( - notification_type: Notification.notification_types[:global], - title: 'notification_1', - level: Notification.levels[:info], - body: 'notification 1', - dismissable: false, - starts_at: Date.today, - expires_at: Date.tomorrow) - end - - # --------------------------------------------------- - test "required fields are required" do - assert_not User.new.valid? - assert_not User.new(password: 'password').valid? - assert_not User.new(email: 'me@example.org').valid? - assert_not User.new(firstname: 'test', surname: 'user').valid? - assert_not User.new(firstname: 'test', surname: 'user', password: 'password').valid? - assert_not User.new(firstname: 'test', surname: 'user', email: 'me@example.org').valid? - - # Ensure the bar minimum and complete versions are valid - a = User.new(email: 'me_testing@example.edu', password: 'password') - assert a.valid?, "expected 'email' and 'password' to be enough to create a User - #{a.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - assert @user.valid? - end - - # --------------------------------------------------- - test "email address must be unique" do - assert_not User.new(email: 'me@example.edu', password: 'password').valid? - end - - # --------------------------------------------------- - test "password must be at least 8 characters" do - assert_not User.new(email: 'me@example.org', password: 'pass').valid? - assert_not User.new(email: 'me@example.org', password: 'pass12').valid? - assert_not User.new(email: 'me@example.org', password: 'Pass12').valid? - assert_not User.new(email: 'me@example.org', password: 'Pass12*').valid? - - assert User.new(email: 'me@example.org', password: 'Password12*').valid? - assert User.new(email: 'me@example.org', password: 'passwords').valid? - assert User.new(email: 'me@example.org', password: 'Password').valid? - end - - # --------------------------------------------------- - test "name returns the correct value" do - # Name should return 'First Last' if we do not specify email - assert @user.name(false).include?(@user.firstname), "expected the first name to be included when specifying non-email" - assert @user.name(false).include?(@user.surname), "expected the last name to be included when specifying non-email" - - # Should return email if we do not pass in a variable - assert_equal @user.email, @user.name, "expected the email by default" - - # Name should return the email if no first and last are present - @user.firstname = nil - @user.surname = nil - assert_equal @user.email, @user.name(false), "expected the email if there is no first and last name" - end - - # --------------------------------------------------- - test "only accepts valid email addresses" do - assert @user.valid? - - @user.email = 'testing' - assert_not @user.valid? - @user.email = 'testing.tester.org' - assert_not @user.valid? - @user.email = 'testing@tester' - assert_not @user.valid? - - @user.email = 'testing@tester.org' - assert @user.valid? - end - - # --------------------------------------------------- - test "api token is removed after call to remove_token" do - @user.api_token = 'ABCDEFGHIJKLMNOP' - @user.save! - assert_equal 'ABCDEFGHIJKLMNOP', @user.reload.api_token, "expected the api_token to have been initialized" - - @user.remove_token! - assert_equal '', @user.reload.api_token, "expected the api_token to have been removed" - end - - # --------------------------------------------------- - test "api token gets kept or created" do - @user.api_token = 'ABCDEFGHIJKLMNOP' - @user.save! - assert_equal 'ABCDEFGHIJKLMNOP', @user.reload.api_token, "expected the api_token to have been initialized" - - @user.keep_or_generate_token! - assert_equal 'ABCDEFGHIJKLMNOP', @user.reload.api_token, "expected the api_token to have been kept" - - @user.remove_token! - assert_equal '', @user.reload.api_token, "expected the api_token to have been removed" - - @user.keep_or_generate_token! - assert_not_equal '', @user.reload.api_token, "expected the api_token to have been generated" - end - - # --------------------------------------------------- - test "responds to all of the authentication options" do - super_admins = User.joins(:perms).where('perms.name = ?', 'add_organisations').to_a - org_admins = User.joins(:perms).where('perms.name = ?', 'modify_templates').to_a - users = User.includes(:perms).where(perms: {id: nil}).to_a - - # remove all of the users who also have super_admin privileges - org_admins = org_admins.delete_if{|u| super_admins.include?(u) } - - admin_methods = [:can_add_orgs?, :can_change_org?, :can_grant_api_to_orgs?] - - org_admin_methods = [:can_grant_permissions?, :can_modify_templates?, - :can_modify_guidance?, :can_use_api?, :can_modify_org_details?] - - [:can_super_admin?, :can_org_admin?].each do |auth| - assert_respond_to super_admins.first, auth, "expected User to respond to #{auth}" - end - - # Super Admin - permission checks - admin_methods.each do |auth| - #assert super_admins.first.send(auth), "expected that Super Admin #{auth}" - assert_not org_admins.first.send(auth), "did NOT expect that Organisation Admin #{auth}" - assert_not @user.send(auth), "did NOT expect that User #{auth}" - end - - # Organisational Admin - permission checks - org_admin_methods.each do |auth| - #assert super_admins.first.send(auth), "expected that the Super Admin #{auth}" - #assert org_admins.first.send(auth), "expected that the Organisational Admin #{auth}" - assert_not @user.send(auth), "did NOT expect that User #{auth}" - end - end - - # --------------------------------------------------- - test "new user is active by default" do - assert @user.active? - end - - # --------------------------------------------------- - test "can only have one identifier per IdentifierScheme" do - @scheme = IdentifierScheme.first - - count = @user.user_identifiers.count - @user.user_identifiers << UserIdentifier.new(identifier_scheme: @scheme, identifier: 'abc') - @user.save! - @user.reload - - assert_equal (count + 1), @user.user_identifiers.count, "Expected the initial identifier to be saved" - - @user.user_identifiers << UserIdentifier.new(identifier_scheme: @scheme, identifier: 'abc') - assert_not @user.valid?, "Expected to NOT be able to add more than one identifier for the same scheme" - assert_equal (count + 1), @user.user_identifiers.count, "Expected the initial identifier to be saved" - end - - # --------------------------------------------------- - test "can find a user via an OAuth response" do - scheme = IdentifierScheme.create!(name: 'tester', active: true) - @user.user_identifiers << UserIdentifier.new(identifier_scheme: scheme, identifier: '12345') - @user.save! - - class Auth - def provider - "tester" - end - def uid - "12345" - end - end - - assert_equal @user, User.from_omniauth(Auth.new) - - class UnknownAuth - def provider - "unknown" - end - def uid - "12345" - end - end - - assert_raise "'Unknown OAuth provider: unknown" do - User.from_omniauth(UnknownAuth.new) - end - end - - # --------------------------------------------------- - test "Plans query filter is working properly" do - 3.times do |i| - plan = Plan.create(title: "My test #{i}", template: @template, visibility: 1) - @user.roles << Role.new(plan: plan, access: 1) - end - @user.save! - - plan = @user.plans.filter("2").first - assert_equal "My test 2", plan.title, "Expected the plans filter to search the title" - end - - # --------------------------------------------------- - test "Returns the appropriate identifier for the specified scheme" do - 3.times do |i| - scheme = IdentifierScheme.create!({name: "test-#{i}", active: true}) - - @user.user_identifiers << UserIdentifier.new(identifier_scheme: scheme, identifier: i.to_s) - end - @user.save! - - 3.times do |i| - scheme = IdentifierScheme.find_by(name: "test-#{i}") - - assert_equal i.to_s, @user.identifier_for(scheme).identifier, "expected the identifier for #{scheme.name} to be '#{i.to_s}'" - end - end - - # --------------------------------------------------- - test "can_super_admin is properly set" do - perms = Perm.where('name IN (?)', ['add_organisations', 'change_org_affiliation', 'grant_api_to_orgs']) - user = User.create!(email: 'tester@example.edu', password: 'password') - - assert_not user.can_super_admin?, "expected a user with no permissions to NOT be a super_admin" - - perms.each do |p| - last = p - user.perms.delete(last) unless last.nil? - user.perms << p - user.save! - - assert user.can_super_admin?, "expected the addition of the #{p.name} perm to enable the user to become a super_admin" - end - - user.perms = [] - user.save! - - user.perms = perms - user.save! - assert user.can_super_admin?, "expected the addition of all the super_admin perms to allow the user to be a super_admin" - end - - # --------------------------------------------------- - test "can_org_admin is properly set" do - perms = Perm.where('name IN (?)', ['grant_permissions', 'modify_templates', 'modify_guidance', 'change_org_details']) - user = User.create!(email: 'tester@example.edu', password: 'password') - - assert_not user.can_org_admin?, "expected a user with no permissions to NOT be a org_admin" - - perms.each do |p| - last = p - user.perms.delete(last) unless last.nil? - user.perms << p - user.save! - - assert user.can_org_admin?, "expected the addition of the #{p.name} perm to enable the user to become a org_admin" - end - - user.perms = [] - user.save! - - user.perms = perms - user.save! - assert user.can_org_admin?, "expected the addition of all the super_admin perms to allow the user to be a org_admin" - end - - # --------------------------------------------------- - test "can CRUD" do - usr = User.create(email: 'test@testing.org', password: 'testing1234') - assert_not usr.id.nil?, "was expecting to be able to create a new User: #{usr.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" - - usr.firstname = 'Tester' - usr.save! - usr.reload - assert_equal 'Tester', usr.firstname, "Was expecting to be able to update the firstname of the User!" - - assert usr.destroy!, "Was unable to delete the User!" - end - - # --------------------------------------------------- - test "can manage has_many relationship with Perms" do - perm = Perm.new(name: 'Added through test') - verify_has_many_relationship(@user, perm, @user.perms.count) - end - - # --------------------------------------------------- - test "can manage has_many relationship with UserIdentifiers" do - id = UserIdentifier.new(identifier_scheme: IdentifierScheme.first, identifier: 'tester') - verify_has_many_relationship(@user, id, @user.user_identifiers.count) - end - - # --------------------------------------------------- - test "can manage has_many relationship with Roles" do - role = Role.new(plan: @plan, access: 1) - verify_has_many_relationship(@user, role, @user.roles.count) - end - - # --------------------------------------------------- - test "can manage has_many relationship with Answers" do - answer = Answer.new(plan: @plan, - question: @plan.template.phases.first.sections.first.questions.first, - text: 'Testing') - verify_has_many_relationship(@user, answer, @user.answers.count) - end - - # --------------------------------------------------- - test "can manage has_many relationship with Notes" do - answer = Answer.create(plan: @plan, - question: @plan.template.phases.first.sections.first.questions.first, - text: 'Testing') - note = Note.new(answer: answer, text: 'Testing') - verify_has_many_relationship(@user, note, @user.notes.count) - end - - # --------------------------------------------------- - test "can manage has_many relationship with ExportedPlans" do - plan = ExportedPlan.new(plan: @plan, format: ExportedPlan::VALID_FORMATS.last) - verify_has_many_relationship(@user, plan, @user.exported_plans.count) - end - - # --------------------------------------------------- - test "can manage belongs_to relationship with Org" do - org = Org.new(name: 'Tester', abbreviation: 'TST', links: {"org":[]}) - verify_belongs_to_relationship(@user, org) - end - - # --------------------------------------------------- - test "can manage belongs_to relationship with Language" do - language = Language.new(name: 'esperonto', abbreviation: 'zz') - verify_belongs_to_relationship(@user, language) - end - test "after_save removes API token and its perms associated" do - previous_api_token = @user.api_token - @user.perms = [Perm.add_orgs, Perm.grant_permissions] - previous_perms = @user.perms.to_a - @user.org = Org.where.not(id: @user.org_id).first - @user.save - assert_not_equal(previous_api_token, @user.api_token) - assert_not_equal(previous_perms, @user.perms.to_a) - end - test "after_save does not remove API token and its perms associated if user can_change_org" do - previous_api_token = @user.api_token - @user.perms = [Perm.add_orgs, Perm.grant_permissions, Perm.change_affiliation] - previous_perms = @user.perms - @user.org = Org.where.not(id: @user.org_id).first - @user.save - assert_equal(previous_api_token, @user.api_token) - assert_equal(previous_perms, @user.perms) - end - - # Cannot dismiss Notifications that are non-dismissable - test 'cannot acknowledge a notification that is not dismissable' do - @user.acknowledge(@notification) - assert_not(@notification.acknowledged?(@user)) - end - # Can dismiss Notifications that are dismissable - test 'can acknowledge a notification' do - @notification.update!(dismissable: true) - @user.acknowledge(@notification) - assert(@notification.acknowledged?(@user)) - end -end