From 67215a44724012178c546e7a6912a5eb3921ad76 Mon Sep 17 00:00:00 2001 From: adi-herwana-nus Date: Fri, 30 Aug 2024 17:36:00 +0800 Subject: [PATCH 1/5] chore(rails): upgrade to rails 7.1.4 - removed unused rails_utils dependency - removed deprecated fixture_path config keys - updated database extension to sync with internal change --- Gemfile | 7 +- Gemfile.lock | 167 ++++++++++-------- app/models/course/survey.rb | 1 + app/models/course/survey/question.rb | 1 + config/application.rb | 2 +- config/initializers/rails_utils.rb | 4 - .../database_event/active_record/base.rb | 2 +- .../question/programming_management_spec.rb | 4 +- spec/libraries/duplicator_spec.rb | 7 +- spec/rails_helper.rb | 2 +- 10 files changed, 106 insertions(+), 91 deletions(-) delete mode 100644 config/initializers/rails_utils.rb diff --git a/Gemfile b/Gemfile index cfcbb8f78f4..fe90f4c7efa 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,7 @@ ruby '3.1.4' gem 'tzinfo-data', platforms: [:mswin, :mswin64] # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '~> 7.0.8.4' +gem 'rails', '~> 7.1.4' # Use PostgreSQL for the backend gem 'pg' @@ -180,11 +180,6 @@ gem 'jwt' # Use cancancan for authorization gem 'cancancan' -# Some helpers for structuring CSS/JavaScript -# Official version https://github.com/winston/rails_utils/pull/30 is no longer maintained. -# We also want stricter sanitization. -gem 'rails_utils', git: 'https://github.com/Coursemology/rails_utils.git' - # Using CarrierWave for file uploads gem 'carrierwave', '2.2.6' # Generate sequential filenames diff --git a/Gemfile.lock b/Gemfile.lock index 3348eddd678..0a2f05c5583 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,13 +13,6 @@ GIT activerecord-userstamp (3.0.5) rails (>= 6) -GIT - remote: https://github.com/Coursemology/rails_utils.git - revision: 1a72571368da0e4172e155428813749b2b134f3c - specs: - rails_utils (6.0.0) - rails (>= 6) - GIT remote: https://github.com/Coursemology/rwordnet revision: 54a85eed974002cb2cfb858cbcaed8e1069b5cb8 @@ -43,75 +36,84 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.0.8.4) - actionpack (= 7.0.8.4) - activesupport (= 7.0.8.4) + actioncable (7.1.4) + actionpack (= 7.1.4) + activesupport (= 7.1.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8.4) - actionpack (= 7.0.8.4) - activejob (= 7.0.8.4) - activerecord (= 7.0.8.4) - activestorage (= 7.0.8.4) - activesupport (= 7.0.8.4) + zeitwerk (~> 2.6) + actionmailbox (7.1.4) + actionpack (= 7.1.4) + activejob (= 7.1.4) + activerecord (= 7.1.4) + activestorage (= 7.1.4) + activesupport (= 7.1.4) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.8.4) - actionpack (= 7.0.8.4) - actionview (= 7.0.8.4) - activejob (= 7.0.8.4) - activesupport (= 7.0.8.4) + actionmailer (7.1.4) + actionpack (= 7.1.4) + actionview (= 7.1.4) + activejob (= 7.1.4) + activesupport (= 7.1.4) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp - rails-dom-testing (~> 2.0) - actionpack (7.0.8.4) - actionview (= 7.0.8.4) - activesupport (= 7.0.8.4) - rack (~> 2.0, >= 2.2.4) + rails-dom-testing (~> 2.2) + actionpack (7.1.4) + actionview (= 7.1.4) + activesupport (= 7.1.4) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.8.4) - actionpack (= 7.0.8.4) - activerecord (= 7.0.8.4) - activestorage (= 7.0.8.4) - activesupport (= 7.0.8.4) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actiontext (7.1.4) + actionpack (= 7.1.4) + activerecord (= 7.1.4) + activestorage (= 7.1.4) + activesupport (= 7.1.4) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8.4) - activesupport (= 7.0.8.4) + actionview (7.1.4) + activesupport (= 7.1.4) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) active_record_upsert (0.11.2) activerecord (>= 5.2, < 7.2) pg (>= 0.18, < 2.0) - activejob (7.0.8.4) - activesupport (= 7.0.8.4) + activejob (7.1.4) + activesupport (= 7.1.4) globalid (>= 0.3.6) - activemodel (7.0.8.4) - activesupport (= 7.0.8.4) - activerecord (7.0.8.4) - activemodel (= 7.0.8.4) - activesupport (= 7.0.8.4) + activemodel (7.1.4) + activesupport (= 7.1.4) + activerecord (7.1.4) + activemodel (= 7.1.4) + activesupport (= 7.1.4) + timeout (>= 0.4.0) activerecord-import (1.8.1) activerecord (>= 4.2) - activestorage (7.0.8.4) - actionpack (= 7.0.8.4) - activejob (= 7.0.8.4) - activerecord (= 7.0.8.4) - activesupport (= 7.0.8.4) + activestorage (7.1.4) + actionpack (= 7.1.4) + activejob (= 7.1.4) + activerecord (= 7.1.4) + activesupport (= 7.1.4) marcel (~> 1.0) - mini_mime (>= 1.1.0) - activesupport (7.0.8.4) + activesupport (7.1.4) + base64 + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) minitest (>= 5.1) + mutex_m tzinfo (~> 2.0) acts_as_tenant (1.0.1) rails (>= 6.0) @@ -139,6 +141,7 @@ GEM aws-eventstream (~> 1, >= 1.0.2) base64 (0.2.0) bcrypt (3.1.20) + bigdecimal (3.1.8) builder (3.3.0) bullet (7.2.0) activesupport (>= 3.0.0) @@ -196,6 +199,7 @@ GEM dotenv-rails (3.1.2) dotenv (= 3.1.2) railties (>= 6.1) + drb (2.2.1) edge (0.6.1) activerecord (>= 5.0.0) email_spec (2.3.0) @@ -276,6 +280,10 @@ GEM ruby-vips (>= 2.0.17, < 3) image_size (3.4.0) in_threads (1.6.0) + io-console (0.7.2) + irb (1.14.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) jbuilder (2.12.0) actionview (>= 5.0.0) activesupport (>= 5.0.0) @@ -329,7 +337,6 @@ GEM net-smtp marcel (1.0.4) matrix (0.4.2) - method_source (1.1.0) mime-types (3.5.2) mime-types-data (~> 3.2015) mime-types-data (3.2024.0820) @@ -341,6 +348,7 @@ GEM mini_portile2 (2.8.7) minitest (5.25.1) multi_json (1.15.0) + mutex_m (0.2.0) net-imap (0.4.14) date net-protocol @@ -364,6 +372,8 @@ GEM racc pg (1.5.8) progress (3.6.0) + psych (5.1.2) + stringio public_suffix (6.0.0) puma (6.4.2) nio4r (~> 2.0) @@ -378,20 +388,23 @@ GEM rack (< 3) rack-test (2.1.0) rack (>= 1.3) - rails (7.0.8.4) - actioncable (= 7.0.8.4) - actionmailbox (= 7.0.8.4) - actionmailer (= 7.0.8.4) - actionpack (= 7.0.8.4) - actiontext (= 7.0.8.4) - actionview (= 7.0.8.4) - activejob (= 7.0.8.4) - activemodel (= 7.0.8.4) - activerecord (= 7.0.8.4) - activestorage (= 7.0.8.4) - activesupport (= 7.0.8.4) + rackup (1.0.0) + rack (< 3) + webrick + rails (7.1.4) + actioncable (= 7.1.4) + actionmailbox (= 7.1.4) + actionmailer (= 7.1.4) + actionpack (= 7.1.4) + actiontext (= 7.1.4) + actionview (= 7.1.4) + activejob (= 7.1.4) + activemodel (= 7.1.4) + activerecord (= 7.1.4) + activestorage (= 7.1.4) + activesupport (= 7.1.4) bundler (>= 1.15.0) - railties (= 7.0.8.4) + railties (= 7.1.4) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -406,18 +419,21 @@ GEM rails-i18n (7.0.9) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - railties (7.0.8.4) - actionpack (= 7.0.8.4) - activesupport (= 7.0.8.4) - method_source + railties (7.1.4) + actionpack (= 7.1.4) + activesupport (= 7.1.4) + irb + rackup (>= 1.0.0) rake (>= 12.2) - thor (~> 1.0) - zeitwerk (~> 2.5) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.2.1) rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) + rdoc (6.7.0) + psych (>= 4.0.0) recaptcha (5.17.0) record_tag_helper (1.0.1) actionview (>= 5) @@ -442,6 +458,8 @@ GEM redis-store (1.10.0) redis (>= 4, < 6) regexp_parser (2.9.2) + reline (0.5.10) + io-console (~> 0.5) request_store (1.7.0) rack (>= 1.4) responders (3.1.1) @@ -547,6 +565,7 @@ GEM rack (>= 2.2.4, < 4) ssrf_filter (1.1.2) stackprof (0.2.26) + stringio (3.1.1) temple (0.10.3) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -566,6 +585,7 @@ GEM activesupport (>= 3.0) warden (1.2.9) rack (>= 2.0.9) + webrick (1.8.1) websocket (1.2.10) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) @@ -635,10 +655,9 @@ DEPENDENCIES puma rack-cors rack-mini-profiler - rails (~> 7.0.8.4) + rails (~> 7.1.4) rails-controller-testing rails-html-sanitizer (>= 1.0.4) - rails_utils! recaptcha record_tag_helper redis-rails diff --git a/app/models/course/survey.rb b/app/models/course/survey.rb index b6005197d01..86365f2e979 100644 --- a/app/models/course/survey.rb +++ b/app/models/course/survey.rb @@ -5,6 +5,7 @@ class Course::Survey < ApplicationRecord include Course::ClosingReminderConcern + attribute :question_type, :integer enum question_type: { text_response: 0, multiple_choice: 1, multiple_response: 2 } validates :end_at, presence: true, if: :allow_response_after_end diff --git a/app/models/course/survey/question.rb b/app/models/course/survey/question.rb index cfbd97192fe..50c391bf06c 100644 --- a/app/models/course/survey/question.rb +++ b/app/models/course/survey/question.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true class Course::Survey::Question < ApplicationRecord + attribute :question_type, :integer enum question_type: { text: 0, multiple_choice: 1, multiple_response: 2 } validates :description, presence: true diff --git a/config/application.rb b/config/application.rb index 98fbf572c3a..2e62bce8895 100644 --- a/config/application.rb +++ b/config/application.rb @@ -13,7 +13,7 @@ module Application # rubocop:disable Style/ClassAndModuleChildren class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 7.0 + config.load_defaults 7.1 # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers diff --git a/config/initializers/rails_utils.rb b/config/initializers/rails_utils.rb deleted file mode 100644 index 0bfe0892e80..00000000000 --- a/config/initializers/rails_utils.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true -RailsUtils.configure do |config| - config.selector_format = :hyphenated -end diff --git a/lib/extensions/database_event/active_record/base.rb b/lib/extensions/database_event/active_record/base.rb index a7cef300bf3..0d4d0b0cdd0 100644 --- a/lib/extensions/database_event/active_record/base.rb +++ b/lib/extensions/database_event/active_record/base.rb @@ -58,7 +58,7 @@ def wait_for_identifier(identifier, deadline, while_callback, &block) def wait_until(deadline, while_callback, &block) while deadline.nil? || Time.zone.now < deadline wait_timeout = deadline ? deadline - Time.zone.now : nil - result = connection.instance_variable_get(:@connection). + result = connection.instance_variable_get(:@raw_connection). wait_for_notify(wait_timeout, &block) return result if while_callback.nil? || !while_callback.call end diff --git a/spec/features/course/assessment/question/programming_management_spec.rb b/spec/features/course/assessment/question/programming_management_spec.rb index 036ae690e3c..fb02c5d38c5 100644 --- a/spec/features/course/assessment/question/programming_management_spec.rb +++ b/spec/features/course/assessment/question/programming_management_spec.rb @@ -119,8 +119,8 @@ question = create(:course_assessment_question_programming, assessment: assessment, template_file_count: 0, package_type: :zip_upload) - empty_package = File.join(fixture_path, 'course/empty_programming_question_template.zip') - valid_package = File.join(fixture_path, 'course/programming_question_template.zip') + empty_package = File.join(file_fixture_path, 'course/empty_programming_question_template.zip') + valid_package = File.join(file_fixture_path, 'course/programming_question_template.zip') visit edit_course_assessment_question_programming_path(course, assessment, question) find('span', text: 'Evaluate and test code').click diff --git a/spec/libraries/duplicator_spec.rb b/spec/libraries/duplicator_spec.rb index da3fc55f255..e31cf1ebc5a 100644 --- a/spec/libraries/duplicator_spec.rb +++ b/spec/libraries/duplicator_spec.rb @@ -410,8 +410,11 @@ def initialize_duplicate(duplicator, other) end temporary_table(:children_parents) do |t| - t.integer :children_id, foreign_key: { references: :complex_active_records, primary_key: :id } - t.integer :parent_id, foreign_key: { references: :complex_active_records, primary_key: :id } + # Something in Rails 7.1 upgrade broke the one-line declarations here. + t.integer :children_id + t.foreign_key :complex_active_records, column: :children_id, primary_key: :id + t.integer :parent_id + t.foreign_key :complex_active_records, column: :parent_id, primary_key: :id end class DuplicationTraceableActiveRecordWithSource < ApplicationRecord diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 5c08a13a168..3fb1d38b5d8 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -46,7 +46,7 @@ RSpec.configure do |config| # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures - config.fixture_path = "#{::Rails.root}/spec/fixtures" + # config.fixture_paths = ["#{::Rails.root}/spec/fixtures"] config.file_fixture_path = "#{::Rails.root}/spec/fixtures" # If you're not using ActiveRecord, or you'd prefer not to run each of your From 75fb79780a5672a72a411d1167f33dc9927f387b Mon Sep 17 00:00:00 2001 From: adi-herwana-nus Date: Mon, 16 Sep 2024 13:57:40 +0800 Subject: [PATCH 2/5] chore(rails): upgrade to rails 7.2.1 - migrated secret_key_base from Rails secrets to (encrypted) credentials - updated all enum declarations from deprecated keyword args to positional args - updated gem calculated_attributes to compatible self-maintained fork - updated third-party gems unread,active_record_upsert to compatible bleeding-edge commits - mock _default_attributes also for specs mocking load_schema (needed due to internal refactoring in 7.2) - set eager_load to true in all environments to ensure table_name overrides loaded - updated carrierwave to v3, amended relevant test case --- Gemfile | 14 +- Gemfile.lock | 178 +++++++++--------- README.md | 4 +- .../submission/workflow_event_concern.rb | 2 +- app/models/course.rb | 2 +- app/models/course/assessment.rb | 2 +- .../assessment/question/multiple_response.rb | 2 +- .../course/assessment/question/programming.rb | 2 +- .../question/programming_test_case.rb | 2 +- .../text_response_comprehension_solution.rb | 2 +- .../question/text_response_solution.rb | 2 +- .../discussion/post/codaveri_feedback.rb | 2 +- app/models/course/forum/topic.rb | 2 +- app/models/course/group_user.rb | 2 +- app/models/course/monitoring/monitor.rb | 2 +- app/models/course/monitoring/session.rb | 2 +- app/models/course/notification.rb | 2 +- app/models/course/settings/email.rb | 4 +- app/models/course/survey.rb | 3 - app/models/course/survey/question.rb | 3 +- app/models/course/user_invitation.rb | 4 +- app/models/course/video/event.rb | 2 +- app/models/course_user.rb | 4 +- app/models/instance/user_invitation.rb | 2 +- app/models/instance/user_role_request.rb | 2 +- app/models/instance_user.rb | 2 +- app/models/user.rb | 2 +- app/models/user_notification.rb | 2 +- config/application.rb | 2 +- config/credentials/development.key | 1 + config/credentials/development.yml.enc | 1 + config/credentials/test.key | 1 + config/credentials/test.yml.enc | 1 + config/environments/development.rb | 7 +- config/environments/test.rb | 9 +- config/secrets.yml | 22 --- db/schema.rb | 2 +- lib/autoload/trackable_job.rb | 2 +- .../conditional/active_record/base.rb | 2 +- spec/libraries/acts_as_condition_spec.rb | 4 + spec/libraries/acts_as_conditional_spec.rb | 8 + .../acts_as_duplication_traceable.rb | 8 + spec/libraries/acts_as_exp_record_spec.rb | 4 + .../acts_as_lesson_plan_item_spec.rb | 8 + .../user_satisfiability_graph_spec.rb | 4 + spec/libraries/filename_validator_spec.rb | 4 + .../has_one_many_attachments_spec.rb | 8 + spec/libraries/materials_spec.rb | 4 + spec/rails_helper.rb | 11 +- .../course_duplication_service_spec.rb | 2 +- 50 files changed, 204 insertions(+), 165 deletions(-) create mode 100644 config/credentials/development.key create mode 100644 config/credentials/development.yml.enc create mode 100644 config/credentials/test.key create mode 100644 config/credentials/test.yml.enc delete mode 100644 config/secrets.yml diff --git a/Gemfile b/Gemfile index fe90f4c7efa..e4980dff48f 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,7 @@ ruby '3.1.4' gem 'tzinfo-data', platforms: [:mswin, :mswin64] # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '~> 7.1.4' +gem 'rails', '~> 7.2.1' # Use PostgreSQL for the backend gem 'pg' @@ -18,7 +18,7 @@ gem 'rack-cors' # Instance/Course settings gem 'settings_on_rails', git: 'https://github.com/Coursemology/settings_on_rails' # Manage read/unread status -gem 'unread' +gem 'unread', '~> 0.14.0' # Extension for validating hostnames and domain names gem 'validates_hostname' # A Ruby state machine library @@ -29,14 +29,14 @@ gem 'activerecord-userstamp', git: 'https://github.com/Coursemology/activerecord # Allow actions to be deferred until after a record is committed. gem 'after_commit_action' # Allow declaring the calculated attributes of a record -gem 'calculated_attributes', git: 'https://github.com/aha-app/calculated_attributes' +gem 'calculated_attributes', git: 'https://github.com/Coursemology/calculated_attributes.git' # For multiple table inheritance # TODO: Figure out breaking changes in v2 as polymorphism is not working correctly. gem 'active_record-acts_as', git: 'https://github.com/Coursemology/active_record-acts_as.git' # Organise ActiveRecord model into a tree structure gem 'edge' -# Upsert action for Postgres -gem 'active_record_upsert', '0.11.2' +# Upsert action for Postgres with validations +gem 'active_record_upsert', git: 'https://github.com/jesjos/active_record_upsert', ref: 'c3e07ae' # Create pretty URLs and work with human-friendly strings gem 'friendly_id' @@ -106,6 +106,8 @@ group :development, :test do gem 'rubocop', '~> 1.66' # Factory Bot for factories + # fix for https://github.com/thoughtbot/factory_bot/issues/1690 + gem 'factory_bot', '~> 6.5.0' gem 'factory_bot_rails' # Checks that all translations are used and defined @@ -181,7 +183,7 @@ gem 'jwt' gem 'cancancan' # Using CarrierWave for file uploads -gem 'carrierwave', '2.2.6' +gem 'carrierwave', '~> 3' # Generate sequential filenames gem 'filename' # Required by CarrierWave, for image resizing diff --git a/Gemfile.lock b/Gemfile.lock index 0a2f05c5583..5a07532bc81 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,6 +13,13 @@ GIT activerecord-userstamp (3.0.5) rails (>= 6) +GIT + remote: https://github.com/Coursemology/calculated_attributes.git + revision: 3d9fe0c99070c61de634b7235044200d61b65a65 + specs: + calculated_attributes (1.0.1) + activerecord (>= 6.0.0, < 8) + GIT remote: https://github.com/Coursemology/rwordnet revision: 54a85eed974002cb2cfb858cbcaed8e1069b5cb8 @@ -27,94 +34,89 @@ GIT rails (>= 6) GIT - remote: https://github.com/aha-app/calculated_attributes - revision: d2f69a1acefd5646ce1908fed3785a63d8399374 + remote: https://github.com/jesjos/active_record_upsert + revision: c3e07aecf28d6a81a06fcada4710b103dfca823b + ref: c3e07ae specs: - calculated_attributes (1.0.0) - activerecord (>= 6.0.0, < 8) + active_record_upsert (0.11.2) + activerecord (>= 5.2, < 8.0) + pg (>= 0.18, < 2.0) GEM remote: https://rubygems.org/ specs: - actioncable (7.1.4) - actionpack (= 7.1.4) - activesupport (= 7.1.4) + actioncable (7.2.1) + actionpack (= 7.2.1) + activesupport (= 7.2.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.1.4) - actionpack (= 7.1.4) - activejob (= 7.1.4) - activerecord (= 7.1.4) - activestorage (= 7.1.4) - activesupport (= 7.1.4) - mail (>= 2.7.1) - net-imap - net-pop - net-smtp - actionmailer (7.1.4) - actionpack (= 7.1.4) - actionview (= 7.1.4) - activejob (= 7.1.4) - activesupport (= 7.1.4) - mail (~> 2.5, >= 2.5.4) - net-imap - net-pop - net-smtp + actionmailbox (7.2.1) + actionpack (= 7.2.1) + activejob (= 7.2.1) + activerecord (= 7.2.1) + activestorage (= 7.2.1) + activesupport (= 7.2.1) + mail (>= 2.8.0) + actionmailer (7.2.1) + actionpack (= 7.2.1) + actionview (= 7.2.1) + activejob (= 7.2.1) + activesupport (= 7.2.1) + mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.1.4) - actionview (= 7.1.4) - activesupport (= 7.1.4) + actionpack (7.2.1) + actionview (= 7.2.1) + activesupport (= 7.2.1) nokogiri (>= 1.8.5) racc - rack (>= 2.2.4) + rack (>= 2.2.4, < 3.2) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - actiontext (7.1.4) - actionpack (= 7.1.4) - activerecord (= 7.1.4) - activestorage (= 7.1.4) - activesupport (= 7.1.4) + useragent (~> 0.16) + actiontext (7.2.1) + actionpack (= 7.2.1) + activerecord (= 7.2.1) + activestorage (= 7.2.1) + activesupport (= 7.2.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.1.4) - activesupport (= 7.1.4) + actionview (7.2.1) + activesupport (= 7.2.1) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - active_record_upsert (0.11.2) - activerecord (>= 5.2, < 7.2) - pg (>= 0.18, < 2.0) - activejob (7.1.4) - activesupport (= 7.1.4) + activejob (7.2.1) + activesupport (= 7.2.1) globalid (>= 0.3.6) - activemodel (7.1.4) - activesupport (= 7.1.4) - activerecord (7.1.4) - activemodel (= 7.1.4) - activesupport (= 7.1.4) + activemodel (7.2.1) + activesupport (= 7.2.1) + activerecord (7.2.1) + activemodel (= 7.2.1) + activesupport (= 7.2.1) timeout (>= 0.4.0) activerecord-import (1.8.1) activerecord (>= 4.2) - activestorage (7.1.4) - actionpack (= 7.1.4) - activejob (= 7.1.4) - activerecord (= 7.1.4) - activesupport (= 7.1.4) + activestorage (7.2.1) + actionpack (= 7.2.1) + activejob (= 7.2.1) + activerecord (= 7.2.1) + activesupport (= 7.2.1) marcel (~> 1.0) - activesupport (7.1.4) + activesupport (7.2.1) base64 bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) + concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) - mutex_m - tzinfo (~> 2.0) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) acts_as_tenant (1.0.1) rails (>= 6.0) addressable (2.8.7) @@ -163,13 +165,12 @@ GEM capybara-selenium (0.0.6) capybara selenium-webdriver - carrierwave (2.2.6) - activemodel (>= 5.0.0) - activesupport (>= 5.0.0) + carrierwave (3.0.7) + activemodel (>= 6.0.0) + activesupport (>= 6.0.0) addressable (~> 2.6) image_processing (~> 1.1) marcel (~> 1.0.0) - mini_mime (>= 0.1.3) ssrf_filter (~> 1.0) childprocess (5.0.0) codecov (0.6.0) @@ -211,7 +212,7 @@ GEM tzinfo excon (0.111.0) exifr (1.4.0) - factory_bot (6.4.6) + factory_bot (6.5.0) activesupport (>= 5.0.0) factory_bot_rails (6.4.3) factory_bot (~> 6.4) @@ -275,7 +276,7 @@ GEM image_optim (~> 0.24) railties sprockets - image_processing (1.12.2) + image_processing (1.13.0) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) image_size (3.4.0) @@ -348,7 +349,6 @@ GEM mini_portile2 (2.8.7) minitest (5.25.1) multi_json (1.15.0) - mutex_m (0.2.0) net-imap (0.4.14) date net-protocol @@ -391,20 +391,20 @@ GEM rackup (1.0.0) rack (< 3) webrick - rails (7.1.4) - actioncable (= 7.1.4) - actionmailbox (= 7.1.4) - actionmailer (= 7.1.4) - actionpack (= 7.1.4) - actiontext (= 7.1.4) - actionview (= 7.1.4) - activejob (= 7.1.4) - activemodel (= 7.1.4) - activerecord (= 7.1.4) - activestorage (= 7.1.4) - activesupport (= 7.1.4) + rails (7.2.1) + actioncable (= 7.2.1) + actionmailbox (= 7.2.1) + actionmailer (= 7.2.1) + actionpack (= 7.2.1) + actiontext (= 7.2.1) + actionview (= 7.2.1) + activejob (= 7.2.1) + activemodel (= 7.2.1) + activerecord (= 7.2.1) + activestorage (= 7.2.1) + activesupport (= 7.2.1) bundler (>= 1.15.0) - railties (= 7.1.4) + railties (= 7.2.1) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -419,10 +419,10 @@ GEM rails-i18n (7.0.9) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - railties (7.1.4) - actionpack (= 7.1.4) - activesupport (= 7.1.4) - irb + railties (7.2.1) + actionpack (= 7.2.1) + activesupport (= 7.2.1) + irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) @@ -489,7 +489,7 @@ GEM rspec-mocks (3.13.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (6.1.4) + rspec-rails (6.1.5) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) @@ -521,12 +521,14 @@ GEM rubocop-ast (>= 1.31.1, < 2.0) ruby-oembed (0.18.0) ruby-progressbar (1.13.0) - ruby-vips (2.2.1) + ruby-vips (2.2.2) ffi (~> 1.12) + logger rubyzip (2.3.2) sanitize (6.1.3) crass (~> 1.0.2) nokogiri (>= 1.12.0) + securerandom (0.3.1) selenium-webdriver (4.22.0) base64 (~> 0.2) logger (~> 1.4) @@ -578,8 +580,9 @@ GEM concurrent-ruby (~> 1.0) unicode-display_width (2.5.0) uniform_notifier (1.16.0) - unread (0.13.1) + unread (0.14.0) activerecord (>= 6.1) + useragent (0.16.10) validates_hostname (1.0.13) activerecord (>= 3.0) activesupport (>= 3.0) @@ -604,7 +607,7 @@ PLATFORMS DEPENDENCIES active_record-acts_as! - active_record_upsert (= 0.11.2) + active_record_upsert! activerecord-import (>= 0.2.0) activerecord-userstamp! acts_as_tenant @@ -617,7 +620,7 @@ DEPENDENCIES capybara capybara-screenshot capybara-selenium - carrierwave (= 2.2.6) + carrierwave (~> 3) codecov consistency_fail coursemology-polyglot @@ -627,6 +630,7 @@ DEPENDENCIES dotenv-rails edge email_spec + factory_bot (~> 6.5.0) factory_bot_rails ffi (>= 1.14.2) filename @@ -655,7 +659,7 @@ DEPENDENCIES puma rack-cors rack-mini-profiler - rails (~> 7.1.4) + rails (~> 7.2.1) rails-controller-testing rails-html-sanitizer (>= 1.0.4) recaptcha @@ -686,7 +690,7 @@ DEPENDENCIES stackprof traceroute tzinfo-data - unread + unread (~> 0.14.0) validates_hostname workflow workflow-activerecord (>= 4.1, < 7.0) diff --git a/README.md b/README.md index 31063a4cc74..94b46b4023d 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ Coursemology is an open source gamified learning platform that enables educators ### System Requirements -1. Ruby (= 3.1.4) -2. Ruby on Rails (= 7.0.8.4) +1. Ruby (= 3.3.5) +2. Ruby on Rails (= 7.2.1) 3. PostgreSQL (>= 9.5) 4. ImageMagick or GraphicsMagick (For [MiniMagick](https://github.com/minimagick/minimagick) - if PDF processing doesn't work for the import of scribing questions, download Ghostscript) 5. Node.js (v18 LTS) diff --git a/app/models/concerns/course/assessment/submission/workflow_event_concern.rb b/app/models/concerns/course/assessment/submission/workflow_event_concern.rb index 44986893acc..51741c682ad 100644 --- a/app/models/concerns/course/assessment/submission/workflow_event_concern.rb +++ b/app/models/concerns/course/assessment/submission/workflow_event_concern.rb @@ -230,7 +230,7 @@ def publish_delayed_posts update_delayed_topics_and_posts(submission_question_topics) # Publish delayed annotations for each programming question of a submission - programming_answers = answers.where('actable_type = ?', Course::Assessment::Answer::Programming) + programming_answers = answers.where('actable_type = ?', Course::Assessment::Answer::Programming.name) annotation_topics = programming_answers.flat_map(&:specific). flat_map(&:files).flat_map(&:annotations).map(&:discussion_topic) update_delayed_topics_and_posts(annotation_topics) diff --git a/app/models/course.rb b/app/models/course.rb index 5e7bfdf7a5a..f4cc5c01b4b 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -27,7 +27,7 @@ class Course < ApplicationRecord validates :instance, presence: true validates :conditional_satisfiability_evaluation_time, presence: true - enum default_timeline_algorithm: CourseUser.timeline_algorithms + enum :default_timeline_algorithm, CourseUser.timeline_algorithms has_many :enrol_requests, inverse_of: :course, dependent: :destroy has_many :course_users, inverse_of: :course, dependent: :destroy diff --git a/app/models/course/assessment.rb b/app/models/course/assessment.rb index e3189aed0ae..d3f8b81c9bb 100644 --- a/app/models/course/assessment.rb +++ b/app/models/course/assessment.rb @@ -19,7 +19,7 @@ class Course::Assessment < ApplicationRecord after_commit :grade_with_new_test_cases, on: :update before_save :save_tab - enum randomization: { prepared: 0 } + enum :randomization, { prepared: 0 } validates :autograded, inclusion: { in: [true, false] } validates :session_password, length: { maximum: 255 }, allow_nil: true diff --git a/app/models/course/assessment/question/multiple_response.rb b/app/models/course/assessment/question/multiple_response.rb index f19a0656c7e..3f19d02e8c6 100644 --- a/app/models/course/assessment/question/multiple_response.rb +++ b/app/models/course/assessment/question/multiple_response.rb @@ -2,7 +2,7 @@ class Course::Assessment::Question::MultipleResponse < ApplicationRecord acts_as :question, class_name: 'Course::Assessment::Question' - enum grading_scheme: [:all_correct, :any_correct] + enum :grading_scheme, [:all_correct, :any_correct] validate :validate_has_option validate :validate_multiple_choice_has_correct_solution, if: :multiple_choice? diff --git a/app/models/course/assessment/question/programming.rb b/app/models/course/assessment/question/programming.rb index b9037a10f63..256ac5f1424 100644 --- a/app/models/course/assessment/question/programming.rb +++ b/app/models/course/assessment/question/programming.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true class Course::Assessment::Question::Programming < ApplicationRecord # rubocop:disable Metrics/ClassLength - enum package_type: { zip_upload: 0, online_editor: 1 } + enum :package_type, { zip_upload: 0, online_editor: 1 } # The table name for this model is singular. self.table_name = table_name.singularize diff --git a/app/models/course/assessment/question/programming_test_case.rb b/app/models/course/assessment/question/programming_test_case.rb index a6b1a2fc24a..b388017f8ac 100644 --- a/app/models/course/assessment/question/programming_test_case.rb +++ b/app/models/course/assessment/question/programming_test_case.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true class Course::Assessment::Question::ProgrammingTestCase < ApplicationRecord - enum test_case_type: { private_test: 0, public_test: 1, evaluation_test: 2 } + enum :test_case_type, { private_test: 0, public_test: 1, evaluation_test: 2 } validates :identifier, length: { maximum: 255 }, presence: true validates :test_case_type, presence: true diff --git a/app/models/course/assessment/question/text_response_comprehension_solution.rb b/app/models/course/assessment/question/text_response_comprehension_solution.rb index 7d4128755f7..1e3a9f6e9bf 100644 --- a/app/models/course/assessment/question/text_response_comprehension_solution.rb +++ b/app/models/course/assessment/question/text_response_comprehension_solution.rb @@ -2,7 +2,7 @@ class Course::Assessment::Question::TextResponseComprehensionSolution < ApplicationRecord self.table_name = 'course_assessment_question_text_response_compre_solutions' - enum solution_type: [:compre_keyword, :compre_lifted_word] + enum :solution_type, [:compre_keyword, :compre_lifted_word] before_validation :sanitise_solution_and_derive_lemma diff --git a/app/models/course/assessment/question/text_response_solution.rb b/app/models/course/assessment/question/text_response_solution.rb index c95df844731..74767c14314 100644 --- a/app/models/course/assessment/question/text_response_solution.rb +++ b/app/models/course/assessment/question/text_response_solution.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true class Course::Assessment::Question::TextResponseSolution < ApplicationRecord - enum solution_type: [:exact_match, :keyword] + enum :solution_type, [:exact_match, :keyword] before_validation :strip_whitespace before_save :sanitize_explanation diff --git a/app/models/course/discussion/post/codaveri_feedback.rb b/app/models/course/discussion/post/codaveri_feedback.rb index a53cd4fcffe..ce48d56640b 100644 --- a/app/models/course/discussion/post/codaveri_feedback.rb +++ b/app/models/course/discussion/post/codaveri_feedback.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true class Course::Discussion::Post::CodaveriFeedback < ApplicationRecord - enum status: { pending_review: 0, accepted: 1, rejected: 2 } + enum :status, { pending_review: 0, accepted: 1, rejected: 2 } validates :codaveri_feedback_id, presence: true validates :original_feedback, presence: true diff --git a/app/models/course/forum/topic.rb b/app/models/course/forum/topic.rb index df16a3e58f0..31441660d18 100644 --- a/app/models/course/forum/topic.rb +++ b/app/models/course/forum/topic.rb @@ -15,7 +15,7 @@ class Course::Forum::Topic < ApplicationRecord after_create :mark_as_read_for_creator after_update :mark_as_read_for_updater - enum topic_type: { normal: 0, question: 1, sticky: 2, announcement: 3 } + enum :topic_type, { normal: 0, question: 1, sticky: 2, announcement: 3 } validates :title, length: { maximum: 255 }, presence: true validates :slug, length: { maximum: 255 }, allow_nil: true diff --git a/app/models/course/group_user.rb b/app/models/course/group_user.rb index 1357cade7a0..34a92f1df2b 100644 --- a/app/models/course/group_user.rb +++ b/app/models/course/group_user.rb @@ -2,7 +2,7 @@ class Course::GroupUser < ApplicationRecord after_initialize :set_defaults, if: :new_record? - enum role: { normal: 0, manager: 1 } + enum :role, { normal: 0, manager: 1 } validate :course_user_and_group_in_same_course validates :role, presence: true diff --git a/app/models/course/monitoring/monitor.rb b/app/models/course/monitoring/monitor.rb index 24ed537ce69..88f52011033 100644 --- a/app/models/course/monitoring/monitor.rb +++ b/app/models/course/monitoring/monitor.rb @@ -2,7 +2,7 @@ class Course::Monitoring::Monitor < ApplicationRecord DEFAULT_MIN_INTERVAL_MS = 3000 - enum browser_authorization_method: { user_agent: 0, seb_config_key: 1 } + enum :browser_authorization_method, { user_agent: 0, seb_config_key: 1 } has_one :assessment, class_name: 'Course::Assessment', inverse_of: :monitor has_many :sessions, class_name: 'Course::Monitoring::Session', inverse_of: :monitor diff --git a/app/models/course/monitoring/session.rb b/app/models/course/monitoring/session.rb index 41a1eb5caa3..0e228fb11b3 100644 --- a/app/models/course/monitoring/session.rb +++ b/app/models/course/monitoring/session.rb @@ -2,7 +2,7 @@ class Course::Monitoring::Session < ApplicationRecord DEFAULT_MAX_SESSION_DURATION = 1.day - enum status: { stopped: 0, listening: 1 } + enum :status, { stopped: 0, listening: 1 } belongs_to :monitor, class_name: 'Course::Monitoring::Monitor', inverse_of: :sessions diff --git a/app/models/course/notification.rb b/app/models/course/notification.rb index da846b94c6f..0e292e604e5 100644 --- a/app/models/course/notification.rb +++ b/app/models/course/notification.rb @@ -3,7 +3,7 @@ # # @api notifications class Course::Notification < ApplicationRecord - enum notification_type: { feed: 0, email: 1 } + enum :notification_type, { feed: 0, email: 1 } validates :activity, presence: true validates :course, presence: true diff --git a/app/models/course/settings/email.rb b/app/models/course/settings/email.rb index bd3a198b3ff..d7b2a322496 100644 --- a/app/models/course/settings/email.rb +++ b/app/models/course/settings/email.rb @@ -10,8 +10,8 @@ class Course::Settings::Email < ApplicationRecord Course::Settings::Email.send(:after_assessment_category_initialize, self) end - enum component: { announcements: 0, assessments: 1, forums: 2, surveys: 3, users: 4, videos: 5 } - enum setting: { new_announcement: 0, + enum :component, { announcements: 0, assessments: 1, forums: 2, surveys: 3, users: 4, videos: 5 } + enum :setting, { new_announcement: 0, opening_reminder: 1, closing_reminder: 2, closing_reminder_summary: 3, diff --git a/app/models/course/survey.rb b/app/models/course/survey.rb index 86365f2e979..a94945fecae 100644 --- a/app/models/course/survey.rb +++ b/app/models/course/survey.rb @@ -5,9 +5,6 @@ class Course::Survey < ApplicationRecord include Course::ClosingReminderConcern - attribute :question_type, :integer - enum question_type: { text_response: 0, multiple_choice: 1, multiple_response: 2 } - validates :end_at, presence: true, if: :allow_response_after_end validates :anonymous, inclusion: { in: [true, false] } validates :allow_modify_after_submit, inclusion: { in: [true, false] } diff --git a/app/models/course/survey/question.rb b/app/models/course/survey/question.rb index 50c391bf06c..f2721778887 100644 --- a/app/models/course/survey/question.rb +++ b/app/models/course/survey/question.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class Course::Survey::Question < ApplicationRecord - attribute :question_type, :integer - enum question_type: { text: 0, multiple_choice: 1, multiple_response: 2 } + enum :question_type, { text: 0, multiple_choice: 1, multiple_response: 2 } validates :description, presence: true validates :required, inclusion: { in: [true, false] } diff --git a/app/models/course/user_invitation.rb b/app/models/course/user_invitation.rb index 3a1897449aa..07aac1f371f 100644 --- a/app/models/course/user_invitation.rb +++ b/app/models/course/user_invitation.rb @@ -10,8 +10,8 @@ class Course::UserInvitation < ApplicationRecord validates :phantom, inclusion: [true, false] validate :no_existing_unconfirmed_invitation - enum role: CourseUser.roles - enum timeline_algorithm: CourseUser.timeline_algorithms + enum :role, CourseUser.roles + enum :timeline_algorithm, CourseUser.timeline_algorithms belongs_to :course, inverse_of: :invitations belongs_to :confirmer, class_name: 'User', inverse_of: nil, optional: true diff --git a/app/models/course/video/event.rb b/app/models/course/video/event.rb index 92841ed9094..e5ba9447c2b 100644 --- a/app/models/course/video/event.rb +++ b/app/models/course/video/event.rb @@ -16,5 +16,5 @@ class Course::Video::Event < ApplicationRecord upsert_keys [:session_id, :sequence_num] - enum event_type: [:play, :pause, :speed_change, :seek_start, :seek_end, :buffer, :end] + enum :event_type, [:play, :pause, :speed_change, :seek_start, :seek_end, :buffer, :end] end diff --git a/app/models/course_user.rb b/app/models/course_user.rb index a0e521545df..2f27ad966ab 100644 --- a/app/models/course_user.rb +++ b/app/models/course_user.rb @@ -7,8 +7,8 @@ class CourseUser < ApplicationRecord after_initialize :set_defaults, if: :new_record? before_validation :set_defaults, if: :new_record? - enum role: { student: 0, teaching_assistant: 1, manager: 2, owner: 3, observer: 4 } - enum timeline_algorithm: { fixed: 0, fomo: 1, stragglers: 2, otot: 3 } + enum :role, { student: 0, teaching_assistant: 1, manager: 2, owner: 3, observer: 4 } + enum :timeline_algorithm, { fixed: 0, fomo: 1, stragglers: 2, otot: 3 } # A set of roles which comprise the staff of a course, including the observer. STAFF_ROLES_SYM = Set[:teaching_assistant, :manager, :owner, :observer] diff --git a/app/models/instance/user_invitation.rb b/app/models/instance/user_invitation.rb index 545af0f2b2e..c8263eca7e1 100644 --- a/app/models/instance/user_invitation.rb +++ b/app/models/instance/user_invitation.rb @@ -11,7 +11,7 @@ class Instance::UserInvitation < ApplicationRecord validates :generate_invitation_key, presence: true validate :no_existing_unconfirmed_invitation - enum role: InstanceUser.roles + enum :role, InstanceUser.roles belongs_to :confirmer, class_name: 'User', inverse_of: nil, optional: true diff --git a/app/models/instance/user_role_request.rb b/app/models/instance/user_role_request.rb index 47b45e2df87..c6751979d18 100644 --- a/app/models/instance/user_role_request.rb +++ b/app/models/instance/user_role_request.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Instance::UserRoleRequest < ApplicationRecord include Workflow - enum role: InstanceUser.roles.except(:normal) + enum :role, InstanceUser.roles.except(:normal) after_initialize :set_default_role, if: :new_record? diff --git a/app/models/instance_user.rb b/app/models/instance_user.rb index f7f6cd1ab4d..c134665c059 100644 --- a/app/models/instance_user.rb +++ b/app/models/instance_user.rb @@ -5,7 +5,7 @@ class InstanceUser < ApplicationRecord acts_as_tenant :instance, inverse_of: :instance_users after_initialize :set_defaults, if: :new_record? - enum role: { normal: 0, instructor: 1, administrator: 2 } + enum :role, { normal: 0, instructor: 1, administrator: 2 } validates :role, presence: true validates :instance, presence: true diff --git a/app/models/user.rb b/app/models/user.rb index ced0fa89ffb..0fdd47c8020 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -11,7 +11,7 @@ class User < ApplicationRecord acts_as_reader mount_uploader :profile_photo, ImageUploader - enum role: { normal: 0, administrator: 1 } + enum :role, { normal: 0, administrator: 1 } AVAILABLE_LOCALES = I18n.available_locales.map(&:to_s) diff --git a/app/models/user_notification.rb b/app/models/user_notification.rb index 37107888911..cec66fb92bf 100644 --- a/app/models/user_notification.rb +++ b/app/models/user_notification.rb @@ -5,7 +5,7 @@ class UserNotification < ApplicationRecord acts_as_readable on: :created_at - enum notification_type: { popup: 0, email: 1 } + enum :notification_type, { popup: 0, email: 1 } validates :notification_type, presence: true validates :activity, presence: true diff --git a/config/application.rb b/config/application.rb index 2e62bce8895..790487d891c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -13,7 +13,7 @@ module Application # rubocop:disable Style/ClassAndModuleChildren class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 7.1 + config.load_defaults 7.2 # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers diff --git a/config/credentials/development.key b/config/credentials/development.key new file mode 100644 index 00000000000..9a9a1030d0e --- /dev/null +++ b/config/credentials/development.key @@ -0,0 +1 @@ +921054b69df2e11efe48fcc33e5e6c3b \ No newline at end of file diff --git a/config/credentials/development.yml.enc b/config/credentials/development.yml.enc new file mode 100644 index 00000000000..21eb8ca51ae --- /dev/null +++ b/config/credentials/development.yml.enc @@ -0,0 +1 @@ +cpFVpuK16dpiu5+pMhjCp2acLOuK/s3flbhMx6vRG+wXLfoGt+RyNkdO1wXI8s/S9MPWpz0OYDLKOx+WlYfp/11z4pl59DbO4dMGVvpHOJxvRRGmcIvmNgAOFUUDOESFzBYu+WIdbGQqUthuJXzBb7CiNtOxhFiXu6uvnH3qfVZBXjFBpqjO/5Us2irVG7NJNOpMp9Dkh4PEye4d4kS1BjppcVe9wwMI1qZBZggW0VOBATisrAQGwJnoQY4eH7dNw6NfkoEKC4LpVH74xhrtL/tNYmNRmiQ2gvb/y0P0MGDBsdP+vakhGmS1ZDbAxwnMXUMhPQhEmFAphUuX2t9imSty/G2pKiaQA5ltAgpxwJbhT8MFyH8ZlBN+07NrJClYxLZMcujMToJjbyNNl3E2FRZRcLRIYCydVXCyyO0+oBSGIxJDbl7gYlbVd0U+6r4NtxNCmHU8m68IlESmXoomtFqAO+BYO4Xuutj/QavX1s7wDR7HBTpRK8vBDIDV1953kWoAj4Ck/3fgQjAAu7px47rlIN8S38ZoJXb8j5ShRLAwwQ46ZoIngzM=--De0M4cyIsF6pIvXU--2DxvDZDez7Q9AZcNkJFCzQ== \ No newline at end of file diff --git a/config/credentials/test.key b/config/credentials/test.key new file mode 100644 index 00000000000..c364b9b83ad --- /dev/null +++ b/config/credentials/test.key @@ -0,0 +1 @@ +00bcc95e1b3a44b1ba9766807383d3cb \ No newline at end of file diff --git a/config/credentials/test.yml.enc b/config/credentials/test.yml.enc new file mode 100644 index 00000000000..57c68291fcc --- /dev/null +++ b/config/credentials/test.yml.enc @@ -0,0 +1 @@ +Os8x0dJeIe65MOnAk0gFzYpdoRmLUPm5FtE3EyWuf1HZuOXqEGvSSvRmGyHkSynTBJ1dcR1zOKr0WY0lCJr++Q23Vrx1mqTnoA+p3aqdjE26x+vdvyAYAJ2OAgPARSeiO4I8h0rbTVkSpJMTjiBoHMj3ehKTkexr3sYnnjM/5CST6ytwSJhV4uWMuc1JPkTFlBI2FCO8EQ==--VLikj22h5LPwB5hI--lo1VepV16EWRIhkByrtyhw== \ No newline at end of file diff --git a/config/environments/development.rb b/config/environments/development.rb index 3e1dbe33f1a..031025244cc 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -9,8 +9,11 @@ # since you don't have to restart the web server when you make code changes. config.cache_classes = false - # Do not eager load code on boot. - config.eager_load = false + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true # Show full error reports. config.consider_all_requests_local = true diff --git a/config/environments/test.rb b/config/environments/test.rb index fa9a971a1d3..56beca8296c 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -12,10 +12,11 @@ config.cache_classes = true - # Do not eager load code on boot. This avoids loading your whole application - # just for the purpose of running a single test. If you are using a tool that - # preloads Rails for running tests, you may have to set it to true. - config.eager_load = false + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true diff --git a/config/secrets.yml b/config/secrets.yml deleted file mode 100644 index a0158636b12..00000000000 --- a/config/secrets.yml +++ /dev/null @@ -1,22 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Your secret key is used for verifying the integrity of signed cookies. -# If you change this key, all old signed cookies will become invalid! - -# Make sure the secret is at least 30 characters and all random, -# no regular words or you'll be exposed to dictionary attacks. -# You can use `rails secret` to generate a secure secret key. - -# Make sure the secrets in this file are kept private -# if you're sharing your code publicly. - -development: - secret_key_base: e38b6588c625976a7b79de42b6916cb32d4d349e90c8911a9dd5dda88de0675c9bc632ba90b2092e2690b5426bfc952463bf03e7ab3168172f5ee4e494f55ec9 - -test: - secret_key_base: fce273299448c1ea3ce9c2fd0138eaf745cd6498edb512cb1e9bba1faae527bd6803b88d8592953854c18ff41679e3ba8e629e45622f2332cecfab66aa0f91bd - -# Do not keep production secrets in the repository, -# instead read values from the environment. -production: - secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> diff --git a/db/schema.rb b/db/schema.rb index c0962513208..31c418521e5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2024_09_17_170847) do +ActiveRecord::Schema[7.2].define(version: 2024_09_17_170847) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" enable_extension "uuid-ossp" diff --git a/lib/autoload/trackable_job.rb b/lib/autoload/trackable_job.rb index 03a594d997f..f2144dcdb7f 100644 --- a/lib/autoload/trackable_job.rb +++ b/lib/autoload/trackable_job.rb @@ -14,7 +14,7 @@ module TrackableJob extend ActiveSupport::Concern class Job < ApplicationRecord - enum status: [:submitted, :completed, :errored] + enum :status, [:submitted, :completed, :errored] after_save :signal_finished, unless: :submitted? diff --git a/lib/extensions/conditional/active_record/base.rb b/lib/extensions/conditional/active_record/base.rb index 940b7f2a078..8498fc0ac82 100644 --- a/lib/extensions/conditional/active_record/base.rb +++ b/lib/extensions/conditional/active_record/base.rb @@ -4,7 +4,7 @@ module ClassMethods # Functions from conditional-and-condition framework. # Declare this function in the conditional model that requires conditions. def acts_as_conditional - enum satisfiability_type: [:all_conditions, :at_least_one_condition] + enum :satisfiability_type, [:all_conditions, :at_least_one_condition] has_many :conditions, -> { includes :actable }, class_name: 'Course::Condition', as: :conditional, dependent: :destroy, diff --git a/spec/libraries/acts_as_condition_spec.rb b/spec/libraries/acts_as_condition_spec.rb index ea7a0546479..6429258d3ac 100644 --- a/spec/libraries/acts_as_condition_spec.rb +++ b/spec/libraries/acts_as_condition_spec.rb @@ -9,6 +9,10 @@ def self.columns def self.load_schema!; end + def self._default_attributes + ActiveModel::AttributeSet.new({}) + end + acts_as_condition end diff --git a/spec/libraries/acts_as_conditional_spec.rb b/spec/libraries/acts_as_conditional_spec.rb index 59d4a0edf20..33b58fc5167 100644 --- a/spec/libraries/acts_as_conditional_spec.rb +++ b/spec/libraries/acts_as_conditional_spec.rb @@ -9,6 +9,10 @@ def self.columns def self.load_schema!; end + def self._default_attributes + ActiveModel::AttributeSet.new({}) + end + attr_accessor :satisfiability_type acts_as_conditional @@ -21,6 +25,10 @@ def self.columns def self.load_schema!; end + def self._default_attributes + ActiveModel::AttributeSet.new({}) + end + acts_as_condition end diff --git a/spec/libraries/acts_as_duplication_traceable.rb b/spec/libraries/acts_as_duplication_traceable.rb index 2f8c926aa2f..a5f49ffeb4c 100644 --- a/spec/libraries/acts_as_duplication_traceable.rb +++ b/spec/libraries/acts_as_duplication_traceable.rb @@ -9,6 +9,10 @@ def self.columns def self.load_schema!; end + def self._default_attributes + ActiveModel::AttributeSet.new({}) + end + acts_as_duplication_traceable end @@ -33,6 +37,10 @@ def self.columns def self.load_schema!; end + def self._default_attributes + ActiveModel::AttributeSet.new({}) + end + acts_as_duplication_traceable def self.dependent_class diff --git a/spec/libraries/acts_as_exp_record_spec.rb b/spec/libraries/acts_as_exp_record_spec.rb index 0d13f38547e..6337c5dfacb 100644 --- a/spec/libraries/acts_as_exp_record_spec.rb +++ b/spec/libraries/acts_as_exp_record_spec.rb @@ -9,6 +9,10 @@ def self.columns def self.load_schema!; end + def self._default_attributes + ActiveModel::AttributeSet.new({}) + end + acts_as_experience_points_record end diff --git a/spec/libraries/acts_as_lesson_plan_item_spec.rb b/spec/libraries/acts_as_lesson_plan_item_spec.rb index 79387b65dce..ea3755533ea 100644 --- a/spec/libraries/acts_as_lesson_plan_item_spec.rb +++ b/spec/libraries/acts_as_lesson_plan_item_spec.rb @@ -9,6 +9,10 @@ def self.columns def self.load_schema!; end + def self._default_attributes + ActiveModel::AttributeSet.new({}) + end + acts_as_lesson_plan_item end @@ -19,6 +23,10 @@ def self.columns def self.load_schema!; end + def self._default_attributes + ActiveModel::AttributeSet.new({}) + end + acts_as_lesson_plan_item has_todo: true end diff --git a/spec/libraries/course/conditional/user_satisfiability_graph_spec.rb b/spec/libraries/course/conditional/user_satisfiability_graph_spec.rb index 93f231a8fca..f721d5faf71 100644 --- a/spec/libraries/course/conditional/user_satisfiability_graph_spec.rb +++ b/spec/libraries/course/conditional/user_satisfiability_graph_spec.rb @@ -41,6 +41,10 @@ def self.columns def self.load_schema!; end + def self._default_attributes + ActiveModel::AttributeSet.new({}) + end + def self.build(conditions, id) dummy = new dummy.conditions = conditions diff --git a/spec/libraries/filename_validator_spec.rb b/spec/libraries/filename_validator_spec.rb index 6b9eb9f4dae..ec96dea7fe4 100644 --- a/spec/libraries/filename_validator_spec.rb +++ b/spec/libraries/filename_validator_spec.rb @@ -9,6 +9,10 @@ def self.columns def self.load_schema!; end + def self._default_attributes + ActiveModel::AttributeSet.new({}) + end + attr_accessor :name validates_with FilenameValidator diff --git a/spec/libraries/has_one_many_attachments_spec.rb b/spec/libraries/has_one_many_attachments_spec.rb index 95f49bdd93d..454a07f0f29 100644 --- a/spec/libraries/has_one_many_attachments_spec.rb +++ b/spec/libraries/has_one_many_attachments_spec.rb @@ -9,6 +9,10 @@ def self.columns def self.load_schema!; end + def self._default_attributes + ActiveModel::AttributeSet.new({}) + end + has_many_attachments end @@ -19,6 +23,10 @@ def self.columns def self.load_schema!; end + def self._default_attributes + ActiveModel::AttributeSet.new({}) + end + has_one_attachment def clear_attribute_changes(attributes = changed_attributes.keys) diff --git a/spec/libraries/materials_spec.rb b/spec/libraries/materials_spec.rb index 6d3903f4815..e45fab01f13 100644 --- a/spec/libraries/materials_spec.rb +++ b/spec/libraries/materials_spec.rb @@ -9,6 +9,10 @@ def self.columns def self.load_schema!; end + def self._default_attributes + ActiveModel::AttributeSet.new({}) + end + has_one_folder end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 3fb1d38b5d8..5e54f2c6ec8 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -46,8 +46,7 @@ RSpec.configure do |config| # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures - # config.fixture_paths = ["#{::Rails.root}/spec/fixtures"] - config.file_fixture_path = "#{::Rails.root}/spec/fixtures" + config.file_fixture_path = "#{Rails.root}/spec/fixtures" # 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 @@ -76,11 +75,11 @@ if ENV['CI'] Capybara::Screenshot.s3_configuration = { s3_client_credentials: { - access_key_id: ENV['CAPYBARA_SCREENSHOT_ACCESS_KEY_ID'], - secret_access_key: ENV['CAPYBARA_SCREENSHOT_SECRET_ACCESS_KEY'], - region: ENV['CAPYBARA_SCREENSHOT_REGION'] + access_key_id: ENV.fetch('CAPYBARA_SCREENSHOT_ACCESS_KEY_ID', nil), + secret_access_key: ENV.fetch('CAPYBARA_SCREENSHOT_SECRET_ACCESS_KEY', nil), + region: ENV.fetch('CAPYBARA_SCREENSHOT_REGION', nil) }, - bucket_name: ENV['CAPYBARA_SCREENSHOT_BUCKET_NAME'] + bucket_name: ENV.fetch('CAPYBARA_SCREENSHOT_BUCKET_NAME', nil) } Capybara::Screenshot.s3_object_configuration = { diff --git a/spec/services/course/duplication/course_duplication_service_spec.rb b/spec/services/course/duplication/course_duplication_service_spec.rb index 487bd92e43a..43121956a48 100644 --- a/spec/services/course/duplication/course_duplication_service_spec.rb +++ b/spec/services/course/duplication/course_duplication_service_spec.rb @@ -21,7 +21,7 @@ describe '#duplicate_course' do it 'duplicates the logo' do - expect(File.exist?(File.join(Rails.root, 'public', new_course.logo.url))).to be true + expect(File.exist?(File.join(Rails.root, 'public', new_course.logo.store_path))).to be true end context 'when saving fails' do From cd99514be2bb4063e31a3cb0f1bedc098f7ddaf9 Mon Sep 17 00:00:00 2001 From: adi-herwana-nus Date: Sat, 21 Sep 2024 02:51:58 +0800 Subject: [PATCH 3/5] chore(ruby): upgrade to ruby 3.3.5 - fix error due to tar files being frozen strings --- .circleci/config.yml | 8 ++++---- Gemfile | 7 ++++++- Gemfile.lock | 6 +++++- lib/autoload/coursemology_docker_container.rb | 3 ++- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e160724bb4b..95abf17001b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -18,7 +18,7 @@ executors: default: false docker: - - image: cimg/ruby:3.1.4-browsers + - image: cimg/ruby:3.3.5-browsers environment: PG_HOST: localhost PG_USER: ubuntu @@ -51,8 +51,8 @@ commands: - restore_cache: name: Restore Ruby dependencies cache keys: - - v3.1.4-ruby-{{ checksum "Gemfile.lock" }} - - v3.1.4-ruby- + - v3.3.5-ruby-{{ checksum "Gemfile.lock" }} + - v3.3.5-ruby- - run: name: Install Bundler @@ -66,7 +66,7 @@ commands: paths: - ./vendor/bundle - ./.bundle - key: v3.1.4-ruby-{{ checksum "Gemfile.lock" }} + key: v3.3.5-ruby-{{ checksum "Gemfile.lock" }} rehydrate_node_deps: steps: diff --git a/Gemfile b/Gemfile index e4980dff48f..aeedf342ace 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,12 @@ # frozen_string_literal: true source 'https://rubygems.org' -ruby '3.1.4' +ruby '3.3.5' + +# These gems are included in Ruby defaults for now, +# but they will have to be included separately in future versions. +gem 'ostruct' +gem 'csv' # For Windows devs gem 'tzinfo-data', platforms: [:mswin, :mswin64] diff --git a/Gemfile.lock b/Gemfile.lock index 5a07532bc81..c5a4043b70c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -181,6 +181,7 @@ GEM coursemology-polyglot (0.3.8) activesupport (>= 4.2) crass (1.0.6) + csv (3.2.8) date (3.3.4) devise (4.9.4) bcrypt (~> 3.0) @@ -364,6 +365,7 @@ GEM mini_portile2 (~> 2.8.2) racc (~> 1.4) orm_adapter (0.5.0) + ostruct (0.6.0) parallel (1.26.3) parallel_tests (4.7.1) parallel @@ -624,6 +626,7 @@ DEPENDENCIES codecov consistency_fail coursemology-polyglot + csv devise (= 4.9.4) devise-multi_email docker-api @@ -654,6 +657,7 @@ DEPENDENCIES mimemagic (= 0.4.3) mini_magick nokogiri (>= 1.8.1) + ostruct parallel_tests pg puma @@ -697,7 +701,7 @@ DEPENDENCIES yard RUBY VERSION - ruby 3.1.4p223 + ruby 3.3.5p100 BUNDLED WITH 2.5.9 diff --git a/lib/autoload/coursemology_docker_container.rb b/lib/autoload/coursemology_docker_container.rb index c616208692f..1b756c593cf 100644 --- a/lib/autoload/coursemology_docker_container.rb +++ b/lib/autoload/coursemology_docker_container.rb @@ -202,7 +202,8 @@ def extract_test_report(report_path) tar_file = Gem::Package::TarReader.new(stream) tar_file.each do |file| test_report = file.read - return test_report.force_encoding(Encoding::UTF_8) if test_report + # this string must be mutable for force_encoding to work + return (+test_report).force_encoding(Encoding::UTF_8) if test_report end rescue Docker::Error::NotFoundError nil From bd14f1b281458100059d9b3bd36b4ba39540762b Mon Sep 17 00:00:00 2001 From: adi-herwana-nus Date: Mon, 23 Sep 2024 15:38:31 +0800 Subject: [PATCH 4/5] test: improved rspec test reliability - updated Keycloak config to allow user delete/overwrite from DB - refactored various tests/helper functions to phase out sleep stmts - improved handling of 'New Question' flow to account for page animations - added search queries to tests looking for objects in paginated lists - increased Capybara max wait time from 5s to 10s --- .circleci/config.yml | 2 +- authentication/import/coursemology_realm.json | 4 +- .../question/scribing_controller_spec.rb | 3 +- spec/factories/courses.rb | 7 ++- spec/factories/generic_announcements.rb | 5 +- spec/factories/instance_users.rb | 11 +++- spec/features/course/admin/admin_spec.rb | 3 +- .../assessment/assessment_viewing_spec.rb | 9 ++-- .../forum_post_response_management_spec.rb | 14 ++--- .../multiple_response_management_spec.rb | 52 +++++-------------- .../question/programming_management_spec.rb | 36 ++++++++----- .../question/text_response_management_spec.rb | 28 ++++------ .../voice_response_management_spec.rb | 11 ++-- spec/features/course/homepage_spec.rb | 2 + .../admin/announcement_management_spec.rb | 42 +++++++++------ .../system/admin/components_settings_spec.rb | 22 +++----- .../admin/instance/course_management_spec.rb | 52 ++++++++++++------- .../admin/instance/user_management_spec.rb | 45 ++++++++++------ spec/spec_helper.rb | 2 +- spec/support/active_job.rb | 11 ++++ spec/support/authentication_performers.rb | 9 ++-- spec/support/capybara.rb | 4 +- spec/support/frontend.rb | 16 ++++++ 23 files changed, 212 insertions(+), 178 deletions(-) create mode 100644 spec/support/frontend.rb diff --git a/.circleci/config.yml b/.circleci/config.yml index 95abf17001b..d664864f8db 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -221,7 +221,7 @@ commands: steps: - run: name: Terminate Rails server - command: pkill -SIGTERM -f puma + command: pkill -SIGINT -f puma - run: name: Wait for Rails coverage results diff --git a/authentication/import/coursemology_realm.json b/authentication/import/coursemology_realm.json index 2fc36e7bd7c..9714c2af38c 100644 --- a/authentication/import/coursemology_realm.json +++ b/authentication/import/coursemology_realm.json @@ -1821,7 +1821,7 @@ "postgres" ], "allowDatabaseToOverwriteKeycloak": [ - "false" + "true" ] } } @@ -4210,7 +4210,7 @@ "postgres" ], "allowDatabaseToOverwriteKeycloak": [ - "false" + "true" ] } } diff --git a/spec/controllers/course/assessment/question/scribing_controller_spec.rb b/spec/controllers/course/assessment/question/scribing_controller_spec.rb index 29515f93064..02d95ea1faf 100644 --- a/spec/controllers/course/assessment/question/scribing_controller_spec.rb +++ b/spec/controllers/course/assessment/question/scribing_controller_spec.rb @@ -81,10 +81,10 @@ result[:file] = fixture_file_upload(file_path, 'application/pdf') end end + let(:file_path) { 'files/one-page-document.pdf' } # "CircleCI's imagemagick installation is flaky" pending 'when the pdf is one page' do - let(:file_path) { 'files/one-page-document.pdf' } it 'creates one scribing question with a png attachment' do expect { subject }.to change { Course::Assessment::Question::Scribing.count }.by(1) @@ -99,7 +99,6 @@ # "CircleCI's imagemagick installation is flaky" pending 'when the pdf is two pages' do - let(:file_path) { 'files/two-page-document.pdf' } it 'creates one scribing question with a png attachment for each pdf page' do expect { subject }.to change { Course::Assessment::Question::Scribing.count }.by(2) diff --git a/spec/factories/courses.rb b/spec/factories/courses.rb index 7159853e1d2..e3595d85f2c 100644 --- a/spec/factories/courses.rb +++ b/spec/factories/courses.rb @@ -1,9 +1,12 @@ # frozen_string_literal: true FactoryBot.define do factory :course do + transient do + prefix { 'Course ' } + end sequence(:title) do |n| - timestamp = Time.zone.now.to_i.to_s - "Course #{timestamp + n.to_s}" + timestamp = Time.zone.now.to_i.to_s(36) + "#{prefix}#{timestamp}#{n}" end description { 'example course' } start_at { Time.zone.now } diff --git a/spec/factories/generic_announcements.rb b/spec/factories/generic_announcements.rb index ca9bdc9a50e..5ae1e3ff10d 100644 --- a/spec/factories/generic_announcements.rb +++ b/spec/factories/generic_announcements.rb @@ -1,7 +1,10 @@ # frozen_string_literal: true FactoryBot.define do factory :generic_announcement, class: System::Announcement.name do - sequence(:title) { |n| "Announcement #{n}" } + transient do + prefix { 'Announcement ' } + end + sequence(:title) { |n| "#{prefix}#{n}" } sequence(:content) { |n| "Content #{n}" } start_at { Time.zone.now } diff --git a/spec/factories/instance_users.rb b/spec/factories/instance_users.rb index a16f0da0673..596ae2e6eab 100644 --- a/spec/factories/instance_users.rb +++ b/spec/factories/instance_users.rb @@ -1,11 +1,18 @@ # frozen_string_literal: true FactoryBot.define do factory :instance_user do + transient do + user_name { nil } + end instance role { :normal } - after(:build) do |instance_user| - instance_user.user ||= build(:user, instance_users: [instance_user]) + after(:build) do |instance_user, evaluator| + instance_user.user ||= if evaluator.user_name + build(:user, name: evaluator.user_name, instance_users: [instance_user]) + else + build(:user, instance_users: [instance_user]) + end end trait :instructor do diff --git a/spec/features/course/admin/admin_spec.rb b/spec/features/course/admin/admin_spec.rb index fa52ddf2e16..72da7e2525d 100644 --- a/spec/features/course/admin/admin_spec.rb +++ b/spec/features/course/admin/admin_spec.rb @@ -47,9 +47,8 @@ find('label', text: 'Change', visible: false).click end - click_button 'Done' + find("[role='dialog']").find('button', text: 'Done').click click_button 'Save changes' - wait_for_page expect_toastify('The new course logo was successfully uploaded.') visit current_path diff --git a/spec/features/course/assessment/assessment_viewing_spec.rb b/spec/features/course/assessment/assessment_viewing_spec.rb index 5f43af4489e..a4d103c72b3 100644 --- a/spec/features/course/assessment/assessment_viewing_spec.rb +++ b/spec/features/course/assessment/assessment_viewing_spec.rb @@ -14,17 +14,14 @@ scenario 'I can access all submissions of an assessment' do assessment - visit course_assessments_path(course) - wait_for_page - - submissions_button = find_link('Submissions', visible: false) - hover_then_click submissions_button + visit course_assessments_path(course) + hover_then_click find('a[aria-label="Submissions"]') expect(current_path).to eq(course_assessment_submissions_path(course, assessment)) # Access submissions from the show assessment page visit course_assessment_path(course, assessment) - click_link 'Submissions' + hover_then_click find('a[aria-label="Submissions"]') expect(current_path).to eq(course_assessment_submissions_path(course, assessment)) end diff --git a/spec/features/course/assessment/question/forum_post_response_management_spec.rb b/spec/features/course/assessment/question/forum_post_response_management_spec.rb index b8d72ccfd95..f42eb8648db 100644 --- a/spec/features/course/assessment/question/forum_post_response_management_spec.rb +++ b/spec/features/course/assessment/question/forum_post_response_management_spec.rb @@ -14,12 +14,10 @@ scenario 'I can create a new forum post response question' do skill = create(:course_assessment_skill, course: course) - visit course_assessment_path(course, assessment) - click_on 'New Question' - new_page = window_opened_by { click_link 'Forum Post Response' } + new_page = test_new_assessment_question_flow(course, assessment, 'Forum Post Response') within_window new_page do - expect(current_path).to eq( + expect(page).to have_current_path( new_course_assessment_question_forum_post_response_path(course, assessment) ) question_attributes = attributes_for(:course_assessment_question_forum_post_response) @@ -36,7 +34,7 @@ find('li', text: skill.title).click click_button 'Save changes' - wait_for_page + expect(page).to have_current_path(course_assessment_path(course, assessment)) question_created = assessment.questions.first.specific expect(question_created.question_assessments.first.skills).to contain_exactly(skill) @@ -71,9 +69,7 @@ correct_checkbox.check click_button 'Save changes' - wait_for_page - - expect(current_path).to eq(course_assessment_path(course, assessment)) + expect(page).to have_current_path(course_assessment_path(course, assessment)) expect(forum_post.reload.title).to eq(title) expect(forum_post.reload.description).to include(description) expect(forum_post.reload.staff_only_comments).to include(staff_only_comments) @@ -86,7 +82,7 @@ correct_checkbox.uncheck click_button 'Save changes' - wait_for_page + expect(page).to have_current_path(course_assessment_path(course, assessment)) expect(forum_post.reload.has_text_response).to eq(false) end diff --git a/spec/features/course/assessment/question/multiple_response_management_spec.rb b/spec/features/course/assessment/question/multiple_response_management_spec.rb index c46739c5ebf..5c8fb9ae809 100644 --- a/spec/features/course/assessment/question/multiple_response_management_spec.rb +++ b/spec/features/course/assessment/question/multiple_response_management_spec.rb @@ -13,9 +13,7 @@ let(:user) { create(:course_manager, course: course).user } scenario 'I can switch MCQ to MRQ, and back to MCQ, for a new question' do - visit course_assessment_path(course, assessment) - click_on 'New Question' - new_mcq_page = window_opened_by { click_link 'Multiple Choice (MCQ)' } + new_mcq_page = test_new_assessment_question_flow(course, assessment, 'Multiple Choice (MCQ)') within_window new_mcq_page do click_on 'Convert to MRQ' @@ -27,9 +25,7 @@ end scenario 'I can switch MRQ to MCQ, and back to MRQ, for a new question' do - visit course_assessment_path(course, assessment) - click_on 'New Question' - new_mrq_page = window_opened_by { click_link 'Multiple Response (MRQ)' } + new_mrq_page = test_new_assessment_question_flow(course, assessment, 'Multiple Response (MRQ)') within_window new_mrq_page do click_on 'Convert to MCQ' @@ -42,9 +38,7 @@ scenario 'I can create a new multiple response question' do skill = create(:course_assessment_skill, course: course) - visit course_assessment_path(course, assessment) - click_on 'New Question' - new_mrq_page = window_opened_by { click_link 'Multiple Response (MRQ)' } + new_mrq_page = test_new_assessment_question_flow(course, assessment, 'Multiple Response (MRQ)') within_window new_mrq_page do question_attributes = attributes_for(:course_assessment_question_multiple_response) @@ -67,7 +61,7 @@ end click_button 'Save changes' - wait_for_page + expect(page).to have_current_path(course_assessment_path(course, assessment)) question_created = assessment.questions.first.specific expect(question_created).not_to be_multiple_choice @@ -77,9 +71,7 @@ end scenario 'I can create a new multiple choice question' do - visit course_assessment_path(course, assessment) - click_on 'New Question' - new_mcq_page = window_opened_by { click_link 'Multiple Choice (MCQ)' } + new_mcq_page = test_new_assessment_question_flow(course, assessment, 'Multiple Choice (MCQ)') within_window new_mcq_page do question_attributes = attributes_for(:course_assessment_question_multiple_response) @@ -99,8 +91,6 @@ end click_button 'Save changes' - wait_for_page - expect(page).to have_text('You must specify at least one correct choice.') within choice_section do @@ -109,7 +99,7 @@ end click_button 'Save changes' - wait_for_page + expect(page).to have_current_path(course_assessment_path(course, assessment)) question_created = assessment.questions.first.specific expect(question_created).to be_multiple_choice @@ -132,9 +122,7 @@ maximum_grade = 999.9 fill_in 'maximumGrade', with: maximum_grade click_button 'Save changes' - - wait_for_page - expect(current_path).to eq(course_assessment_path(course, assessment)) + expect(page).to have_current_path(course_assessment_path(course, assessment)) expect(mrq.reload.maximum_grade).to eq(maximum_grade) visit edit_path @@ -155,9 +143,7 @@ end click_button 'Save changes' - wait_for_page - - expect(current_path).to eq(course_assessment_path(course, assessment)) + expect(page).to have_current_path(course_assessment_path(course, assessment)) expect(mrq.reload.options.count).to eq(initial_options_count + options.count) # Switching in edit page @@ -165,17 +151,16 @@ visit edit_path click_button 'Convert to MCQ' find_all('button', text: 'Convert to MCQ').last.click - - wait_for_page + expect(page).to have_text('Choices') # Switch MCQ to MRQ click_button 'Convert to MRQ' find_all('button', text: 'Convert to MRQ').last.click - - wait_for_page + expect(page).to have_text('Responses') # Switching in assessment show page visit course_assessment_path(course, assessment) + expect(page).to have_current_path(course_assessment_path(course, assessment)) # Switch MRQ to MCQ click_button 'Convert to MCQ' @@ -192,15 +177,12 @@ find_all('button[aria-label="Delete choice"]').each(&:click) expect(page).to have_button('Save changes') click_button 'Save changes' - expect(page).to have_text('You must specify at least one correct choice.') find_all('button[aria-label="Undo delete choice"]').last.click expect(page).to have_button('Save changes') click_button 'Save changes' - wait_for_page - - expect(current_path).to eq(course_assessment_path(course, assessment)) + expect(page).to have_current_path(course_assessment_path(course, assessment)) expect(mrq.reload.options.count).to eq(1) end @@ -215,9 +197,7 @@ maximum_grade = 999.9 fill_in 'maximumGrade', with: maximum_grade click_button 'Save changes' - - wait_for_page - expect(current_path).to eq(course_assessment_path(course, assessment)) + expect(page).to have_current_path(course_assessment_path(course, assessment)) expect(mrq.reload.maximum_grade).to eq(maximum_grade) visit edit_path @@ -225,22 +205,18 @@ find_all('button[aria-label="Delete response"]').each(&:click) expect(page).to have_button('Save changes') click_button 'Save changes' - expect(page).to have_text('You must specify at least one response.') find_all('button[aria-label="Undo delete response"]').last.click expect(page).to have_button('Save changes') click_button 'Save changes' - wait_for_page - - expect(current_path).to eq(course_assessment_path(course, assessment)) + expect(page).to have_current_path(course_assessment_path(course, assessment)) expect(mrq.reload.options.count).to eq(1) end scenario 'I can delete a question' do mrq = create(:course_assessment_question_multiple_response, assessment: assessment) visit course_assessment_path(course, assessment) - wait_for_page within find('section', text: mrq.title) { click_button 'Delete' } click_button 'Delete question' diff --git a/spec/features/course/assessment/question/programming_management_spec.rb b/spec/features/course/assessment/question/programming_management_spec.rb index fb02c5d38c5..8cc4be9e528 100644 --- a/spec/features/course/assessment/question/programming_management_spec.rb +++ b/spec/features/course/assessment/question/programming_management_spec.rb @@ -14,9 +14,7 @@ scenario 'I can create a new question' do skill = create(:course_assessment_skill, course: course) - visit course_assessment_path(course, assessment) - click_on 'New Question' - new_page = window_opened_by { click_link 'Programming' } + new_page = test_new_assessment_question_flow(course, assessment, 'Programming') within_window new_page do expect(current_path).to eq(new_course_assessment_question_programming_path(course, assessment)) @@ -34,17 +32,19 @@ find('li', text: skill.title).click find_all('div', text: 'Language').last.click - find('li', text: attributes[:language].name).click + wait_for_animation + find('li', exact_text: attributes[:language].name).click find('div', id: 'testUi.metadata.submission').click send_keys template click_button 'Save changes' - wait_for_page + expect_toastify('Question saved.') expect(page).to have_current_path(course_assessment_path(course, assessment)) new_question = assessment.questions.first.specific.reload + expect(assessment.questions.count).to eq(1) expect(new_question.title).to eq(attributes[:title]) expect(new_question.maximum_grade).to eq(attributes[:maximum_grade]) expect(new_question.description).to include(attributes[:description]) @@ -77,7 +77,8 @@ expect(page).not_to have_field('Attempt limit') find_all('div', text: 'Language').last.click - find('li', text: attributes[:language].name).click + wait_for_animation + find('li', exact_text: attributes[:language].name).click find('span', text: 'Evaluate and test code').click expect(page).to have_text('submissions will always receive the maximum grade') @@ -101,11 +102,12 @@ end click_button 'Save changes' - wait_for_page + expect_toastify('Question saved.') expect(page).to have_current_path(course_assessment_path(course, assessment)) new_question = assessment.questions.first.specific.reload + expect(assessment.questions.count).to eq(1) expect(new_question.title).to eq(attributes[:title]) expect(new_question.maximum_grade).to eq(attributes[:maximum_grade]) expect(new_question.description).to include(attributes[:description]) @@ -130,17 +132,22 @@ end click_button 'Save changes' - wait_for_page - + expect_toastify("package wasn't successfully imported") expect(page).to have_text('error') - expect_toastify "package wasn't successfully imported" + find('.Toastify').find('svg[data-testid="CloseIcon"]').click attach_file(valid_package) do click_button 'Upload a new package' end + # TODO: close button on toastify messages doesn't work, + # but scrolling the page causes it to close eventually + page.scroll_to(find('footer')) + page.scroll_to(find('button', text: 'Upload a new package')) + expect(page).to_not have_css('.Toastify', text: "package wasn't successfully imported", wait: 60) + click_button 'Save changes' - wait_for_page + expect_toastify('Question saved.') expect(page).to have_current_path(course_assessment_path(course, assessment)) @@ -169,7 +176,7 @@ fill_in 'Maximum grade', with: maximum_grade click_button 'Save changes' - wait_for_page + expect_toastify('Question saved.') expect(page).to have_current_path(course_assessment_path(course, assessment)) expect(question.reload.maximum_grade).to eq(maximum_grade) @@ -193,6 +200,7 @@ fill_in 'Maximum grade', with: attributes[:maximum_grade] find_all('div', text: 'Language').last.click + wait_for_animation find('li', text: attributes[:language].name).click find('span', text: 'Evaluate and test code').click @@ -208,9 +216,9 @@ end click_button 'Save changes' - wait_for_page + expect_toastify('Question saved.') - new_question = assessment.questions.first.specific + new_question = assessment.questions.first.specific.reload expect(new_question.memory_limit).to eq(attributes[:memory_limit]) expect(new_question.time_limit).to eq(attributes[:time_limit]) expect(new_question.attempt_limit).to eq(attributes[:attempt_limit]) diff --git a/spec/features/course/assessment/question/text_response_management_spec.rb b/spec/features/course/assessment/question/text_response_management_spec.rb index 122b7790c2e..30d605b7aa6 100644 --- a/spec/features/course/assessment/question/text_response_management_spec.rb +++ b/spec/features/course/assessment/question/text_response_management_spec.rb @@ -17,13 +17,11 @@ scenario 'I can create a new file upload question' do skill = create(:course_assessment_skill, course: course) - visit course_assessment_path(course, assessment) - click_on 'New Question' - new_page = window_opened_by { click_link 'File Upload' } + new_page = test_new_assessment_question_flow(course, assessment, 'File Upload') within_window new_page do - expect(current_path).to eq( - new_course_assessment_question_text_response_path(course, assessment) + expect(page).to have_current_path( + new_course_assessment_question_text_response_path(course, assessment, { file_upload: true }) ) question_attributes = attributes_for(:course_assessment_question_text_response) fill_in 'title', with: question_attributes[:title] @@ -36,7 +34,7 @@ find('li', text: skill.title).click click_button 'Save changes' - wait_for_page + expect(page).to have_current_path(course_assessment_path(course, assessment)) question_created = assessment.questions.first.specific expect(question_created.question_assessments.first.skills).to contain_exactly(skill) @@ -51,12 +49,11 @@ scenario 'I can create a new text response question' do visit course_assessment_path(course, assessment) - click_on 'New Question' - new_page = window_opened_by { click_link 'Text Response' } + new_page = test_new_assessment_question_flow(course, assessment, 'Text Response') within_window new_page do file_upload_path = new_course_assessment_question_text_response_path(course, assessment) - expect(current_path).to eq(file_upload_path) + expect(page).to have_current_path(file_upload_path) question_attributes = attributes_for(:course_assessment_question_text_response) fill_in 'title', with: question_attributes[:title] @@ -65,7 +62,7 @@ fill_in 'maximumGrade', with: question_attributes[:maximum_grade] click_button 'Save changes' - wait_for_page + expect(page).to have_current_path(course_assessment_path(course, assessment)) question_created = assessment.questions.first.specific expect(question_created.title).to eq(question_attributes[:title]) @@ -100,9 +97,8 @@ fill_in 'maximumGrade', with: maximum_grade click_button 'Save changes' - wait_for_page - expect(current_path).to eq(course_assessment_path(course, assessment)) + expect(page).to have_current_path(course_assessment_path(course, assessment)) expect(question.reload.title).to eq(title) expect(question.reload.description).to include(description) expect(question.reload.staff_only_comments).to include(staff_only_comments) @@ -129,9 +125,8 @@ end click_button 'Save changes' - wait_for_page - expect(current_path).to eq(course_assessment_path(course, assessment)) + expect(page).to have_current_path(course_assessment_path(course, assessment)) expect(question.reload.max_attachments).to eq(max_attachment_default_text_response) expect(question.reload.solutions.count).to eq(solutions.count) @@ -140,17 +135,14 @@ find_all('button[aria-label="Delete solution"]').each(&:click) click_button 'Save changes' - wait_for_page - expect(current_path).to eq(course_assessment_path(course, assessment)) + expect(page).to have_current_path(course_assessment_path(course, assessment)) expect(question.reload.solutions.count).to eq(0) end scenario 'I can delete a text response question' do - skip 'Flaky tests' question = create(:course_assessment_question_text_response, assessment: assessment) visit course_assessment_path(course, assessment) - wait_for_page within find('section', text: question.title) { click_button 'Delete' } click_button 'Delete question' diff --git a/spec/features/course/assessment/question/voice_response_management_spec.rb b/spec/features/course/assessment/question/voice_response_management_spec.rb index e46f7c68442..97500ed5472 100644 --- a/spec/features/course/assessment/question/voice_response_management_spec.rb +++ b/spec/features/course/assessment/question/voice_response_management_spec.rb @@ -14,9 +14,7 @@ scenario 'I can create a new voice response question' do skill = create(:course_assessment_skill, course: course) - visit course_assessment_path(course, assessment) - click_on 'New Question' - new_voice_page = window_opened_by { click_link 'Audio Response' } + new_voice_page = test_new_assessment_question_flow(course, assessment, 'Audio Response') within_window new_voice_page do question_attributes = attributes_for(:course_assessment_question_voice_response) @@ -29,7 +27,7 @@ find('li', text: skill.title).click click_button 'Save changes' - wait_for_page + expect(page).to have_current_path(course_assessment_path(course, assessment)) question_created = assessment.questions.first.specific expect(question_created.title).to eq(question_attributes[:title]) @@ -57,9 +55,7 @@ fill_in 'maximumGrade', with: maximum_grade click_button 'Save changes' - wait_for_page - - expect(current_path).to eq(course_assessment_path(course, assessment)) + expect(page).to have_current_path(course_assessment_path(course, assessment)) expect(voice.reload.title).to eq(title) expect(voice.reload.description).to include(description) expect(voice.reload.staff_only_comments).to include(staff_only_comments) @@ -69,7 +65,6 @@ scenario 'I can delete a question' do voice = create(:course_assessment_question_voice_response, assessment: assessment) visit course_assessment_path(course, assessment) - wait_for_page within find('section', text: voice.title) { click_button 'Delete' } click_button 'Delete question' diff --git a/spec/features/course/homepage_spec.rb b/spec/features/course/homepage_spec.rb index e2336a01fd1..3bdc1bee436 100644 --- a/spec/features/course/homepage_spec.rb +++ b/spec/features/course/homepage_spec.rb @@ -106,6 +106,8 @@ scenario 'I can visit the course homepage' do visit course_path(course) + # we have at least one FE assertion to ensure below check is performed after record is updated + expect(page).to have_text(course.title) expect(course_user.reload.last_active_at).to be_within(1.hour).of(Time.zone.now) end diff --git a/spec/features/system/admin/announcement_management_spec.rb b/spec/features/system/admin/announcement_management_spec.rb index 15bcae1a7b1..e40007af61f 100644 --- a/spec/features/system/admin/announcement_management_spec.rb +++ b/spec/features/system/admin/announcement_management_spec.rb @@ -4,16 +4,28 @@ RSpec.feature 'System: Administration: Announcements' do let(:instance) { Instance.default } with_tenant(:instance) do + let!(:prefix) { "testadm-#{rand(36**12).to_s(36)}-ann-" } before do login_as(user, scope: :user) end + # For certain tests, use the search box to only show the announcements we created for this test. + # This is to prevent tests failing if there are so many announcements such that + # the ones created for this test aren't on the first page. + def search_for_announcements(query) + find( + :xpath, + # Find the announcements feed, go up to the parent, and select the input text field under it + # (the Search text field) + '//div[@id="course-announcements"]/parent::*/descendant::input[@type="text"]' + ).set('').native.send_keys(query) + end + context 'As a system administrator', js: true do let(:user) { create(:administrator) } scenario 'I can create announcements' do visit admin_announcements_path - wait_for_page find('button#new-announcement-button').click @@ -22,18 +34,20 @@ fill_in_react_ck 'textarea[name="content"]', announcement[:content] find('button.btn-submit').click - wait_for_page + + expect_toastify('New announcement posted!') expect(current_path).to eq(admin_announcements_path) + + search_for_announcements(announcement[:title]) expect(page).to have_text(announcement[:title]) expect(page).to have_text(announcement[:content]) - expect_toastify('New announcement posted!') end scenario 'I can edit announcements' do - announcement = create(:system_announcement) + announcement = create(:system_announcement, prefix: prefix) visit admin_announcements_path - wait_for_page + search_for_announcements(announcement.title) find("#announcement-edit-button-#{announcement.id}").click fill_in 'title', with: 'long string' * 100 @@ -41,13 +55,12 @@ expect_toastify('Failed to update the announcement') expect(page).to have_selector('#form-dialog-update-button') - new_title = 'New Title' + new_title = announcement.title.upcase fill_in 'title', with: new_title find('#form-dialog-update-button').click - # Commented due to flaky test - # expect_toastify('Announcement updated') - wait_for_page + expect_toastify('Announcement updated') + search_for_announcements(new_title) expect(current_path).to eq admin_announcements_path within find("#announcement-#{announcement.id}") do @@ -57,12 +70,12 @@ end scenario 'I can see all announcements' do - create_list(:system_announcement, 2) - announcements = System::Announcement.first(2) + search_prefix = "testadm-search-#{rand(36**12).to_s(36)}-ann-" + announcements = create_list(:system_announcement, 2, prefix: search_prefix) visit admin_announcements_path expect(page).to have_selector('#new-announcement-button') - wait_for_page + search_for_announcements(search_prefix) announcements.each do |announcement| expect(page).to have_selector("#announcement-#{announcement.id}") expect(page).to have_selector("#announcement-edit-button-#{announcement.id}") @@ -71,10 +84,9 @@ end scenario 'I can delete announcements' do - create(:system_announcement) - announcement = System::Announcement.first + announcement = create(:system_announcement, prefix: prefix) visit admin_announcements_path - wait_for_page + search_for_announcements(announcement.title) find("#announcement-delete-button-#{announcement.id}").click click_button('Delete') diff --git a/spec/features/system/admin/components_settings_spec.rb b/spec/features/system/admin/components_settings_spec.rb index f0fc0a5cb75..cb4e1ee0bdd 100644 --- a/spec/features/system/admin/components_settings_spec.rb +++ b/spec/features/system/admin/components_settings_spec.rb @@ -21,11 +21,10 @@ expect(page).to have_selector('tr', text: component.display_name) within find("tr#component_#{component.key}") do - checkbox = find('input', visible: false) if enabled_components.include?(component.key.to_s) - expect(checkbox).to be_checked + expect(page).to have_selector('input:checked', visible: false) else - expect(checkbox).not_to be_checked + expect(page).to have_selector('input:not(:checked)', visible: false) end end end @@ -36,22 +35,15 @@ settings = Instance::Settings::Components.new(instance) enabled_components = settings.enabled_component_ids component_to_modify = enabled_components.sample + element_to_modify = find("tr#component_#{component_to_modify}") - within find("tr#component_#{component_to_modify}") do - find('input', visible: false).click - end + element_to_modify.find('input', visible: false).click expect_toastify('Instance component setting was updated successfully.') + expect(element_to_modify).to have_selector('input:not(:checked)', visible: false) - within find("tr#component_#{component_to_modify}") do - expect(find('input', visible: false)).not_to be_checked - find('input', visible: false).click - end - + element_to_modify.find('input', visible: false).click expect_toastify('Instance component setting was updated successfully.') - - within find("tr#component_#{component_to_modify}") do - expect(find('input', visible: false)).to be_checked - end + expect(element_to_modify).to have_selector('input:checked', visible: false) end end end diff --git a/spec/features/system/admin/instance/course_management_spec.rb b/spec/features/system/admin/instance/course_management_spec.rb index c428eda43fd..e8b12cb9a3d 100644 --- a/spec/features/system/admin/instance/course_management_spec.rb +++ b/spec/features/system/admin/instance/course_management_spec.rb @@ -6,33 +6,48 @@ with_tenant(:instance) do let(:last_page) { Course.unscoped.page.total_pages } + let!(:prefix) { "testadm-#{rand(36**12).to_s(36)}-crs-" } let!(:courses) do - courses = create_list(:course, 2) + courses = create_list(:course, 2, prefix: prefix) create(:course_manager, course: courses.sample) create(:course_student, course: courses.sample) courses end + # For certain tests, use the search box to only show the courses we created for this test. + # This is to prevent tests failing if there are so many courses such that + # the ones created for this test aren't on the first page. + def search_for_courses(query) + find( + :xpath, + # Find the courses table, go up to the parent, and select the input text field under that parent + # (the Search text field) + '//div[contains(@class, "MuiTableContainer-root")]/parent::*/descendant::input[@type="text"]' + ).native.send_keys(query) + end + context 'As a Instance Administrator' do let(:admin) { create(:instance_administrator).user } before { login_as(admin, scope: :user) } scenario 'I can view all courses in the instance' do visit admin_instance_courses_path + search_for_courses(prefix) courses.each do |course| - expect(page).to have_selector("tr.course_#{course.id}", text: course.title) - expect(page). - to have_link(nil, href: "//#{course.instance.host}/courses/#{course.id}") + within find("tr.course_#{course.id}", text: course.title) do + expect(page). + to have_link(nil, href: "//#{course.instance.host}/courses/#{course.id}") - # It shows and only shows the owners - course.course_users.owner.each do |course_user| - expect(page).to have_selector('li', text: course_user.user.name) - end + # It shows and only shows the owners + course.course_users.owner.each do |course_user| + expect(page).to have_selector('li', exact_text: course_user.user.name) + end - course.course_users.reject(&:owner?).each do |course_user| - expect(page).not_to have_selector('li', text: course_user.user.name) + course.course_users.reject(&:owner?).each do |course_user| + expect(page).not_to have_selector('li', exact_text: course_user.user.name) + end end end end @@ -40,27 +55,24 @@ let!(:course_to_delete) { create(:course) } scenario 'I can delete a course' do visit admin_instance_courses_path + search_for_courses(course_to_delete.title) find("button.course-delete-#{course_to_delete.id}").click expect(page).to have_button('Delete course', disabled: true) fill_in 'confirmDeleteField', with: 'coursemology' click_button('Delete course') - wait_for_page expect_toastify("#{course_to_delete.title} was deleted.") end - let!(:course_to_search) { create(:course) } + let!(:course_to_search) { create(:course, prefix: "testadm-search-#{rand(36**12).to_s(36)}-crs-") } scenario 'I can search courses' do - skip 'Flaky tests' visit admin_instance_courses_path + search_for_courses(course_to_search.title) - find_button('Search').click - find('div[aria-label="Search"]').find('input').set(course_to_search.title) - - wait_for_field_debouncing # timeout for search debouncing - - expect(page).to have_selector('p.course_title', text: course_to_search.title) - expect(all('.course').count).to eq(1) + within find('div.MuiTableContainer-root') do + expect(page).to have_text(course_to_search.title) + expect(page.first('tbody')).to have_selector('tr', count: 1) + end end end end diff --git a/spec/features/system/admin/instance/user_management_spec.rb b/spec/features/system/admin/instance/user_management_spec.rb index 9ab72d2915e..953cd039104 100644 --- a/spec/features/system/admin/instance/user_management_spec.rb +++ b/spec/features/system/admin/instance/user_management_spec.rb @@ -6,7 +6,22 @@ with_tenant(:instance) do let(:instance_admin) { create(:instance_user, role: :administrator).user } - let!(:instance_users) { create_list(:instance_user, 2) } + let!(:prefix) { "testadm-#{rand(36**12).to_s(36)}-usr-" } + let!(:instance_users) do + (1..2).map do |i| + create(:instance_user, user_name: "#{prefix}#{i}") + end + end + + # For certain tests, use the search box to only show the users we created for this test. + # This is to prevent tests failing if there are so many users such that + # the ones created for this test aren't on the first page. + def search_for_users(query, click: true) + within find('div[aria-label="Table Toolbar"]') do + find('button[aria-label="Search"]').click if click + find('input[type="text"]').set('').native.send_keys(query) + end + end context 'As a Instance Administrator' do before { login_as(instance_admin, scope: :user) } @@ -14,6 +29,7 @@ scenario 'I can view all users in the instance' do visit admin_instance_users_path + search_for_users(prefix) instance_users.each do |instance_user| expect(page).to have_text(instance_user.user.name) expect(page).to have_selector('p.user_email', text: instance_user.user.email) @@ -36,11 +52,11 @@ expect(page).to have_selector('p.user_email', exact_text: instance_admin.email) end - # Flaky test - xscenario "I can change a user's role" do + scenario "I can change a user's role" do visit admin_instance_users_path user_to_change = instance_users.sample + search_for_users(user_to_change.user.name) within find("tr.instance_user_#{user_to_change.id}") do find('div.user_role').click @@ -54,37 +70,36 @@ scenario 'I can delete a user' do visit admin_instance_users_path + search_for_users(prefix) user_to_delete = instance_users.sample find("button.user-delete-#{user_to_delete.id}").click accept_prompt expect_toastify('User was deleted.') end + # Generate new users to search so it doesn't conflict with above scenarios scenario 'I can search users' do - skip 'Flaky tests' - user_name = 'lool' - instance_users_to_search = create_list(:user, 2, name: user_name). - map { |u| u.instance_users.first } + search_prefix = "testadm-search-#{rand(36**12).to_s(36)}-usr-" + instance_users_to_search = (1..2).map do |i| + create(:instance_user, user_name: "#{search_prefix}#{i}") + end + visit admin_instance_users_path # Search by username - click_button 'Search' - find('div[aria-label="Search"]').find('input').set(user_name) - - wait_for_field_debouncing # timeout for search debouncing + search_for_users(search_prefix) instance_users_to_search.each do |instance_user| expect(page).to have_text(instance_user.user.name) end - expect(all('.instance_user').count).to eq(2) + expect(page).to have_selector('.instance_user', count: 2) # Search by email random_instance_user = InstanceUser.order('RANDOM()').first - find('div[aria-label="Search"]').find('input').set(random_instance_user.user.email) - wait_for_field_debouncing # timeout for search debouncing + search_for_users(random_instance_user.user.email, click: false) expect(page).to have_text(random_instance_user.user.name) - expect(all('.instance_user').count).to eq(1) + expect(page).to have_selector('.instance_user', count: 1) end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index bdf79b3145d..75b917a17bb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -122,7 +122,7 @@ Capybara.configure do |config| config.server = :puma, { Silent: true } - config.default_max_wait_time = 5 + config.default_max_wait_time = 10 config.enable_aria_label = true config.server_host = Application::Application.config.x.default_host config.server_port = Application::Application.config.x.server_port diff --git a/spec/support/active_job.rb b/spec/support/active_job.rb index af329c694bf..9cb3ca1a474 100644 --- a/spec/support/active_job.rb +++ b/spec/support/active_job.rb @@ -62,6 +62,10 @@ def wait_for_job end end + # TODO: Whenever possible, rewrite tests currently using these functions to use alternative solutions. + # Sleeping for a preset time is inherently flaky as the page may not always behave the same way. + # https://thoughtbot.com/blog/write-reliable-asynchronous-integration-tests-with-capybara + # Wait for page/react lifecycle to finish loading/end def wait_for_page sleep 2.5 @@ -76,6 +80,13 @@ def wait_for_field_debouncing sleep 0.5 end + # Wait for certain animations to finish before performing further actions (e.g. clicking a list element) + # The animation may cause tests to click the wrong element if it happens at an unexpected time. + # As with all the wait_for_* methods above, we should stop using this when a better solution is found. + def wait_for_animation + sleep 1 + end + def visit_current_path tries ||= 10 visit current_path diff --git a/spec/support/authentication_performers.rb b/spec/support/authentication_performers.rb index d2835a3a1a4..d6abfba83bd 100644 --- a/spec/support/authentication_performers.rb +++ b/spec/support/authentication_performers.rb @@ -16,16 +16,15 @@ def login_as(user, _ = {}) fill_in 'Password', with: password_for(user) click_button 'Sign In' - wait_for_page + # We expect all authenticated pages should have a user menu button. + expect(page).to have_css('div[data-testid="user-menu-button"]') end def logout(*_) - # We expect all pages should have a user menu button. find('div[data-testid="user-menu-button"]').click find('li', text: 'Sign out').click - click_button 'Logout' - - wait_for_page + click_button('Logout') + expect(page).to_not have_css('div[data-testid="user-menu-button"]') end private diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 35a5598ece8..23bb1818eb1 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -87,10 +87,10 @@ def fill_in_mui_datetime(field_name, date) # Since capybara's `find` has a default timeout until the element is found, this helps # to ensure certain changes are made before continuing with the tests. def expect_toastify(message) - wait_for_page # To ensure toast is open + expect(page).to have_css('.Toastify', visible: true) container = find_all('.Toastify').first expect(container).to have_text(message) - find('p', text: message).click + container.find('p', text: message).click end # Finds a react-beautiful-dnd draggable element diff --git a/spec/support/frontend.rb b/spec/support/frontend.rb new file mode 100644 index 00000000000..c103d16a436 --- /dev/null +++ b/spec/support/frontend.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +module FrontendTestHelpers + def test_new_assessment_question_flow(course, assessment, question_type) + visit course_assessment_path(course, assessment) + click_on 'New Question' + + # Need to wait for the animation to finish, since the dropdown menu starts small and gradually scales itself up. + # If link is clicked mid-animation, it can result in going to the wrong question type (off-by-one). + wait_for_animation + window_opened_by { click_link question_type } + end +end + +RSpec.configure do |config| + config.include FrontendTestHelpers, type: :feature +end From ff3e2969f89ad4b3300bde439ded24a7c3d09277 Mon Sep 17 00:00:00 2001 From: adi-herwana-nus Date: Tue, 1 Oct 2024 10:00:21 +0800 Subject: [PATCH 5/5] test: other test improvements - added missing translations from #7542 - migrated away from deprecated codecov gem --- .circleci/config.yml | 12 ++++++------ Gemfile | 2 +- Gemfile.lock | 7 ++++--- config/initializers/coverage.rb | 4 ++-- .../zh/activerecord/course/monitoring/heartbeat.yml | 8 ++++++++ .../zh/activerecord/course/monitoring/monitor.yml | 4 ++++ 6 files changed, 25 insertions(+), 12 deletions(-) create mode 100644 config/locales/zh/activerecord/course/monitoring/heartbeat.yml diff --git a/.circleci/config.yml b/.circleci/config.yml index d664864f8db..b7f0681ac40 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -226,7 +226,7 @@ commands: - run: name: Wait for Rails coverage results no_output_timeout: 5m - command: until [ -f coverage/codecov-result.json ]; do sleep 1; done + command: until [ -f coverage/coverage.xml ]; do sleep 1; done setup_docker_layer_cache: steps: @@ -301,13 +301,9 @@ jobs: command: | zip -r test-results.zip test-results playright-report - # - codecov/upload: - # upload_name: playwright-client - # file: tests/coverage/lcov.info - - codecov/upload: upload_name: playwright-rails - file: coverage/codecov-result.json + file: coverage/coverage.xml - store_test_results: path: ~/repo/tests/results.xml @@ -343,6 +339,10 @@ jobs: mkdir ~/rspec circleci tests glob "spec/**/*_spec.rb" | circleci tests run --command="xargs bundle exec rspec --format progress --format RspecJunitFormatter -o ~/rspec/rspec.xml" --verbose --split-by=timings + - codecov/upload: + upload_name: rspec-rails + file: coverage/coverage.xml + - store_test_results: path: ~/rspec diff --git a/Gemfile b/Gemfile index aeedf342ace..2d1872bf20f 100644 --- a/Gemfile +++ b/Gemfile @@ -131,7 +131,7 @@ group :development, :test do # Code Coverage reporters gem 'simplecov' - gem 'codecov', require: false + gem 'simplecov-cobertura' gem 'dotenv-rails' end diff --git a/Gemfile.lock b/Gemfile.lock index c5a4043b70c..1fd881cc195 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -173,8 +173,6 @@ GEM marcel (~> 1.0.0) ssrf_filter (~> 1.0) childprocess (5.0.0) - codecov (0.6.0) - simplecov (>= 0.15, < 0.22) concurrent-ruby (1.3.4) connection_pool (2.4.1) consistency_fail (0.3.7) @@ -554,6 +552,9 @@ GEM docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) + simplecov-cobertura (2.1.0) + rexml + simplecov (~> 0.19) simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) slim (5.2.1) @@ -623,7 +624,6 @@ DEPENDENCIES capybara-screenshot capybara-selenium carrierwave (~> 3) - codecov consistency_fail coursemology-polyglot csv @@ -689,6 +689,7 @@ DEPENDENCIES sidekiq sidekiq-cron simplecov + simplecov-cobertura slim-rails spring stackprof diff --git a/config/initializers/coverage.rb b/config/initializers/coverage.rb index abeaa6e3f43..c1bee8f5c71 100644 --- a/config/initializers/coverage.rb +++ b/config/initializers/coverage.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true if Rails.env.test? && ENV['COLLECT_COVERAGE'] == 'true' require 'simplecov' - require 'codecov' + require 'simplecov-cobertura' - SimpleCov.formatter = SimpleCov::Formatter::Codecov + SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter SimpleCov.start('rails') do add_filter '/lib/extensions/legacy/active_record/connection_adapters/table_definition.rb' diff --git a/config/locales/zh/activerecord/course/monitoring/heartbeat.yml b/config/locales/zh/activerecord/course/monitoring/heartbeat.yml new file mode 100644 index 00000000000..a47c9b5d524 --- /dev/null +++ b/config/locales/zh/activerecord/course/monitoring/heartbeat.yml @@ -0,0 +1,8 @@ +zh: + activerecord: + errors: + models: + course/monitoring/heartbeat: + attributes: + seb_payload: + invalid_seb_payload: 'seb payload 必须为空或为有效的 JSON 对象' diff --git a/config/locales/zh/activerecord/course/monitoring/monitor.yml b/config/locales/zh/activerecord/course/monitoring/monitor.yml index 1a54f1c4af4..63a58d888d1 100644 --- a/config/locales/zh/activerecord/course/monitoring/monitor.yml +++ b/config/locales/zh/activerecord/course/monitoring/monitor.yml @@ -8,3 +8,7 @@ zh: must_be_password_protected: '评估必须受密码保护才能启用' max_interval_ms: greater_than_min_interval: '必须大于最小间隔' + blocks: + must_have_browser_authorization_and_session_protection: '必须启用浏览器授权和会话保护' + seb_config_key: + required_if_using_seb_config_key_browser_authorization: '使用 seb 配置密钥浏览器授权方法时必须提供 seb 配置密钥'