From cef65098cc6ed83ec07bdc002296bc0d25b7a5b8 Mon Sep 17 00:00:00 2001 From: yuenmichelle1 Date: Fri, 11 Oct 2024 15:03:01 -0500 Subject: [PATCH 1/3] Update queries for workflow counts (#71) * Adding Hourly Workflow Counts Realtime CAgg and change DailyWorkflowCount to Materialized Only View * initial go on using hourly classifications for workflows * remove print statement * Update count_classifications.rb * Update count_classifications.rb * remove unused var * taking care of blank case/ no entry found case * remove logs * add frames for test * adding testing for cases when end_date is before and after current day * add tests for testing period and change eriod format to match that of data pull * adding tests for the case when there are classifications from previous day * update comment on migration * update hound comments * update db.rake with new caggs * Update hourly_workflow_classification_count.rb * Update db.rake * remove redundant returns * add frozen string literal true * rubocop fix hound * adding comment on spec * rename spec to note adding counts * update migrations to be reversible --- .../hourly_workflow_classification_count.rb | 12 ++ app/queries/count_classifications.rb | 86 +++++++++++++- ...te_hourly_workflow_classification_count.rb | 33 ++++++ ...efresh_policy_for_hourly_workflow_count.rb | 16 +++ ...ention_policy_for_hourly_workflow_count.rb | 16 +++ ...assification_count_to_materialized_only.rb | 16 +++ db/schema.rb | 2 +- lib/tasks/db.rake | 11 ++ spec/queries/count_classifications_spec.rb | 112 ++++++++++++++++-- 9 files changed, 295 insertions(+), 9 deletions(-) create mode 100644 app/models/classification_counts/hourly_workflow_classification_count.rb create mode 100644 db/migrate/20240926225916_create_hourly_workflow_classification_count.rb create mode 100644 db/migrate/20240926231010_add_refresh_policy_for_hourly_workflow_count.rb create mode 100644 db/migrate/20240926231325_create_data_retention_policy_for_hourly_workflow_count.rb create mode 100644 db/migrate/20240926233924_alter_daily_workflow_classification_count_to_materialized_only.rb diff --git a/app/models/classification_counts/hourly_workflow_classification_count.rb b/app/models/classification_counts/hourly_workflow_classification_count.rb new file mode 100644 index 0000000..04cd787 --- /dev/null +++ b/app/models/classification_counts/hourly_workflow_classification_count.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module ClassificationCounts + class HourlyWorkflowClassificationCount < ApplicationRecord + self.table_name = 'hourly_classification_count_per_workflow' + attribute :classification_count, :integer + + def readonly? + true + end + end +end diff --git a/app/queries/count_classifications.rb b/app/queries/count_classifications.rb index 2abd588..d75c094 100644 --- a/app/queries/count_classifications.rb +++ b/app/queries/count_classifications.rb @@ -13,7 +13,25 @@ def call(params={}) scoped = @counts scoped = filter_by_workflow_id(scoped, params[:workflow_id]) scoped = filter_by_project_id(scoped, params[:project_id]) - filter_by_date_range(scoped, params[:start_date], params[:end_date]) + # Because of how the FE, calls out to this endpoint when querying for a project's workflow's classifications count + # And because of our use of Real Time Aggregates + # Querying the DailyClassificationCountByWorkflow becomes not as performant + # Because we are limited in resources, we do the following mitigaion for ONLY querying workflow classification counts: + # 1. Create a New HourlyClassificationCountByWorkflow which is RealTime and Create a Data Retention for this new aggregate (this should limit the amount of data the query planner has to sift through) + # 2. Turn off Real Time aggreation for the DailyClassificationCount + # 3. For workflow classification count queries that include the current date's counts, we query current date's counts via the HourlyClassificationCountByWorkflow and query the DailyClassificationCountByWorkflow for everything before the current date's + + if params[:workflow_id].present? + if end_date_includes_today?(params[:end_date]) + scoped_upto_yesterday = filter_by_date_range(scoped, params[:start_date], Date.yesterday.to_s) + scoped = include_today_to_scoped(scoped_upto_yesterday, params[:workflow_id], params[:period]) + else + scoped = filter_by_date_range(scoped, params[:start_date], params[:end_date]) + end + else + scoped = filter_by_date_range(scoped, params[:start_date], params[:end_date]) + end + scoped end private @@ -22,6 +40,72 @@ def initial_scope(relation, period) relation.select(select_and_time_bucket_by(period, 'classification')).group('period').order('period') end + def include_today_to_scoped(scoped_upto_yesterday, workflow_id, period) + period = 'year' if period.nil? + todays_classifications = current_date_workflow_classifications(workflow_id) + return scoped_upto_yesterday if todays_classifications.blank? + + if scoped_upto_yesterday.blank? + # append new entry where period is start of the period + todays_classifications[0].period = start_of_current_period(period).to_time.utc + return todays_classifications + end + + most_recent_date_from_scoped = scoped_upto_yesterday[-1].period.to_date + + # If period=week, month, or year, the current date could be part of that week, month or year; + # we check if the current date is part of the period + # if so, we add the count to the most recent period pulled from db + # if not, we append as a new entry for the current period + if today_part_of_recent_period?(most_recent_date_from_scoped, period) + add_todays_counts_to_recent_period_counts(scoped_upto_yesterday, todays_classifications) + else + todays_classifications[0].period = start_of_current_period(period).to_time.utc + append_today_to_scoped(scoped_upto_yesterday, todays_classifications) + end + end + + def start_of_current_period(period) + today = Date.today + case period + when 'day' + today + when 'week' + # Returns Monday of current week + today.at_beginning_of_week + when 'month' + today.at_beginning_of_month + when 'year' + today.at_beginning_of_year + end + end + + def today_part_of_recent_period?(most_recent_date, period) + most_recent_date == start_of_current_period(period) + end + + def append_today_to_scoped(count_records_up_to_yesterday, todays_count) + count_records_up_to_yesterday + todays_count + end + + def add_todays_counts_to_recent_period_counts(count_records_up_to_yesterday, todays_count) + current_period_counts = count_records_up_to_yesterday[-1].count + todays_count[0].count + count_records_up_to_yesterday[-1].count = current_period_counts + count_records_up_to_yesterday + end + + def current_date_workflow_classifications(workflow_id) + current_day_str = Date.today.to_s + current_hourly_classifications = ClassificationCounts::HourlyWorkflowClassificationCount.select("time_bucket('1 day', hour) AS period, SUM(classification_count)::integer AS count").group('period').order('period').where("hour >= '#{current_day_str}'") + filter_by_workflow_id(current_hourly_classifications, workflow_id) + end + + def end_date_includes_today?(end_date) + includes_today = true + includes_today = Date.parse(end_date) >= Date.today if end_date.present? + includes_today + end + def relation(params) if params[:workflow_id] ClassificationCounts::DailyWorkflowClassificationCount diff --git a/db/migrate/20240926225916_create_hourly_workflow_classification_count.rb b/db/migrate/20240926225916_create_hourly_workflow_classification_count.rb new file mode 100644 index 0000000..24f9bf0 --- /dev/null +++ b/db/migrate/20240926225916_create_hourly_workflow_classification_count.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true. + +class CreateHourlyWorkflowClassificationCount < ActiveRecord::Migration[7.0] + # we have to disable the migration transaction because creating materialized views within it is not allowed. + + # Due to how the front end pulls project stats (and workflow stats) all in one go, we hit performance issues; especially if a project has multiple workflows. + # We have discovered that having a non-realtime/materialized only continous aggregate for our daily workflow count cagg is more performant than real time. + # We plan to do the following: + # - Update the daily_classification_count_per_workflow to be materialized only (i.e. non-realtime) + # - Create a subsequent realtime cagg that buckets hourly that we will create data retention policies for. The plan is for up to 72 hours worth of hourly workflow classification counts of data. + # - Update workflow query to first query the daily counts first and the query the hourly counts for just the specific date of now. + disable_ddl_transaction! + def up + execute <<~SQL + create materialized view hourly_classification_count_per_workflow + with ( + timescaledb.continuous + ) as + select + time_bucket('1 hour', event_time) as hour, + workflow_id, + count(*) as classification_count + from classification_events where event_time > now() - INTERVAL '5 days' + group by hour, workflow_id; + SQL + end + + def down + execute <<~SQL + DROP materialized view hourly_classification_count_per_workflow; + SQL + end +end diff --git a/db/migrate/20240926231010_add_refresh_policy_for_hourly_workflow_count.rb b/db/migrate/20240926231010_add_refresh_policy_for_hourly_workflow_count.rb new file mode 100644 index 0000000..2996462 --- /dev/null +++ b/db/migrate/20240926231010_add_refresh_policy_for_hourly_workflow_count.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddRefreshPolicyForHourlyWorkflowCount < ActiveRecord::Migration[7.0] + disable_ddl_transaction! + def up + execute <<~SQL + SELECT add_continuous_aggregate_policy('hourly_classification_count_per_workflow',start_offset => INTERVAL '5 days', end_offset => INTERVAL '30 minutes', schedule_interval => INTERVAL '1 h'); + SQL + end + + def down + execute <<~SQL + SELECT remove_continuous_aggregate_policy('hourly_classification_count_per_workflow'); + SQL + end +end diff --git a/db/migrate/20240926231325_create_data_retention_policy_for_hourly_workflow_count.rb b/db/migrate/20240926231325_create_data_retention_policy_for_hourly_workflow_count.rb new file mode 100644 index 0000000..592ff11 --- /dev/null +++ b/db/migrate/20240926231325_create_data_retention_policy_for_hourly_workflow_count.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class CreateDataRetentionPolicyForHourlyWorkflowCount < ActiveRecord::Migration[7.0] + disable_ddl_transaction! + def up + execute <<~SQL + SELECT add_retention_policy('hourly_classification_count_per_workflow', drop_after => INTERVAL '3 days'); + SQL + end + + def down + execute <<~SQL + SELECT remove_retention_policy('hourly_classification_count_per_workflow'); + SQL + end +end diff --git a/db/migrate/20240926233924_alter_daily_workflow_classification_count_to_materialized_only.rb b/db/migrate/20240926233924_alter_daily_workflow_classification_count_to_materialized_only.rb new file mode 100644 index 0000000..9418a27 --- /dev/null +++ b/db/migrate/20240926233924_alter_daily_workflow_classification_count_to_materialized_only.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AlterDailyWorkflowClassificationCountToMaterializedOnly < ActiveRecord::Migration[7.0] + disable_ddl_transaction! + def up + execute <<~SQL + ALTER MATERIALIZED VIEW daily_classification_count_per_workflow set (timescaledb.materialized_only = true); + SQL + end + + def down + execute <<~SQL + ALTER MATERIALIZED VIEW daily_classification_count_per_workflow set (timescaledb.materialized_only = false); + SQL + end +end diff --git a/db/schema.rb b/db/schema.rb index 612a347..4be333d 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_03_28_183306) do +ActiveRecord::Schema[7.0].define(version: 2024_09_26_233924) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" enable_extension "timescaledb" diff --git a/lib/tasks/db.rake b/lib/tasks/db.rake index 472a835..8b0de7d 100644 --- a/lib/tasks/db.rake +++ b/lib/tasks/db.rake @@ -183,6 +183,16 @@ namespace :db do FROM classification_user_groups WHERE user_group_id IS NOT NULL GROUP BY day, user_group_id, user_id, workflow_id; SQL + + ActiveRecord::Base.connection.execute <<-SQL + CREATE MATERIALIZED VIEW IF NOT EXISTS hourly_classification_count_per_workflow + WITH (timescaledb.continuous) AS + SELECT time_bucket('1 hour', event_time) AS hour, + workflow_id, + count(*) as classification_count + FROM classification_events + GROUP BY hour, workflow_id; + SQL end desc 'Drop Continuous Aggregates Views' @@ -203,6 +213,7 @@ namespace :db do DROP MATERIALIZED VIEW IF EXISTS daily_group_classification_count_and_time_per_user CASCADE; DROP MATERIALIZED VIEW IF EXISTS daily_group_classification_count_and_time_per_user_per_project CASCADE; DROP MATERIALIZED VIEW IF EXISTS daily_group_classification_count_and_time_per_user_per_workflow CASCADE; + DROP MATERIALIZED VIEW IF EXISTS hourly_classification_count_per_workflow CASCADE; SQL end diff --git a/spec/queries/count_classifications_spec.rb b/spec/queries/count_classifications_spec.rb index 14c8c1f..f8a12fe 100644 --- a/spec/queries/count_classifications_spec.rb +++ b/spec/queries/count_classifications_spec.rb @@ -22,15 +22,14 @@ end describe 'select_and_time_bucket_by' do + let(:counts) { count_classifications.call(params) } it 'buckets counts by year by default' do - counts = count_classifications.call(params) expected_select_query = "SELECT time_bucket('1 year', day) AS period, SUM(classification_count)::integer AS count FROM \"daily_classification_count\" GROUP BY period ORDER BY period" expect(counts.to_sql).to eq(expected_select_query) end it 'buckets counts by given period' do params[:period] = 'week' - counts = count_classifications.call(params) expected_select_query = "SELECT time_bucket('1 week', day) AS period, SUM(classification_count)::integer AS count FROM \"daily_classification_count\" GROUP BY period ORDER BY period" expect(counts.to_sql).to eq(expected_select_query) end @@ -41,13 +40,13 @@ let!(:diff_workflow_event) { create(:classification_with_diff_workflow) } let!(:diff_project_event) { create(:classification_with_diff_project) } let!(:diff_time_event) { create(:classification_created_yesterday) } + let(:counts) { count_classifications.call(params) } it_behaves_like 'is filterable by workflow' it_behaves_like 'is filterable by project' it_behaves_like 'is filterable by date range' it 'returns counts of all events when no params given' do - counts = count_classifications.call(params) # because default is bucket by year and all data created in the same year, we expect counts to look something like # [] current_year = Date.today.year @@ -58,7 +57,6 @@ it 'returns counts bucketed by given period' do params[:period] = 'day' - counts = count_classifications.call(params) expect(counts.length).to eq(2) expect(counts[0].count).to eq(1) expect(counts[0].period).to eq((Date.today - 1).to_s) @@ -69,7 +67,6 @@ it 'returns counts of events with given workflow' do workflow_id = diff_workflow_event.workflow_id params[:workflow_id] = workflow_id.to_s - counts = count_classifications.call(params) expect(counts.length).to eq(1) expect(counts[0].count).to eq(1) end @@ -77,7 +74,6 @@ it 'returns counts of events with given project' do project_id = diff_project_event.project_id params[:project_id] = project_id.to_s - counts = count_classifications.call(params) expect(counts.length).to eq(1) expect(counts[0].count).to eq(1) end @@ -87,9 +83,111 @@ yesterday = Date.today - 1 params[:start_date] = last_week.to_s params[:end_date] = yesterday.to_s - counts = count_classifications.call(params) expect(counts.length).to eq(1) expect(counts[0].count).to eq(1) end + + context 'when params[:workflow_id] present' do + context 'when params[:end_date] is before current date' do + it 'returns counts from DailyWorkflowClassificationCount' do + yesterday = Date.today - 1 + params[:workflow_id] = diff_time_event.workflow_id.to_s + params[:end_date] = yesterday.to_s + expect(counts.model).to be(ClassificationCounts::DailyWorkflowClassificationCount) + expect(counts.length).to eq(1) + expect(counts[0].count).to eq(1) + end + end + + context 'when params[:end_date] includes current date' do + before do + params[:end_date] = Date.today.to_s + end + + context 'when 0 classifications up to previous day' do + context 'when 0 classifications for current day' do + it 'returns from DailyWorkflowClassificationCount' do + # Select a workflow id that has no classification + params[:workflow_id] = '100' + expect(counts.model).to be(ClassificationCounts::DailyWorkflowClassificationCount) + expect(counts.length).to eq(0) + end + end + + context 'when there are classifications for current day' do + before do + params[:workflow_id] = diff_workflow_event.workflow_id.to_s + end + + it "returns today's classifications from HourlyWorkflowClassificationCount" do + expect(counts.model).to be(ClassificationCounts::HourlyWorkflowClassificationCount) + expect(counts.length).to eq(1) + expect(counts[0].count).to eq(1) + end + + it 'returns current date when period is day' do + params[:period] = 'day' + expect(counts[0].period).to eq(Date.today.to_time.utc) + end + + it 'returns start of week when period is week' do + params[:period] = 'week' + expect(counts[0].period).to eq(Date.today.at_beginning_of_week.to_time.utc) + end + + it 'returns start of month when period is month' do + params[:period] = 'month' + expect(counts[0].period).to eq(Date.today.at_beginning_of_month.to_time.utc) + end + + it 'returns start of year when period is year' do + params[:period] = 'year' + expect(counts[0].period).to eq(Date.today.at_beginning_of_year.to_time.utc) + end + end + end + + context 'when there are classifications up to previous day' do + context 'when there are 0 classifications for current day' do + let!(:classification_created_yesterday_diff_workflow) { create(:classification_created_yesterday, workflow_id: 4, classification_id: 100) } + it 'returns from DailyWorkflowCount (scoped up to yesterday)' do + params[:workflow_id] = classification_created_yesterday_diff_workflow.workflow_id.to_s + expect(counts.model).to be(ClassificationCounts::DailyWorkflowClassificationCount) + expect(counts.length).to eq(1) + expect(counts[0].count).to eq(1) + end + end + + context 'when there are classifications for current day' do + before do + allow(Date).to receive(:today).and_return Date.new(2022, 10, 21) + params[:workflow_id] = diff_workflow_event.workflow_id.to_s + params[:period] = 'year' + end + + context 'when current day is part of the most recently pulled period' do + it 'adds the current day counts to the most recently pulled period counts' do + create(:classification_with_diff_workflow, classification_id: 1000, event_time: Date.new(2022, 1, 2)) + expect(counts.length).to eq(1) + # the 2 classifications counted is the one created in L170 as well as diff_workflow_event classification. + expect(counts[0].count).to eq(2) + expect(counts[0].period).to eq(Date.today.at_beginning_of_year) + end + end + + context 'when current day is not part of the most recently pulled period' do + it 'appends a new entry to scoped from HourlyWorkflowCount query' do + create(:classification_with_diff_workflow, classification_id: 1000, event_time: Date.new(2021, 1, 2)) + expect(counts.length).to eq(2) + counts.each { |c| expect(c.count).to eq(1) } + expect(counts[0].class).to be(ClassificationCounts::DailyWorkflowClassificationCount) + expect(counts[1].class).to be(ClassificationCounts::HourlyWorkflowClassificationCount) + expect(counts.last.period).to eq(Date.today.at_beginning_of_year) + end + end + end + end + end + end end end From 69658b419dcb7c81c42b3a588640de1621e1fc5c Mon Sep 17 00:00:00 2001 From: yuenmichelle1 Date: Thu, 17 Oct 2024 11:20:29 -0500 Subject: [PATCH 2/3] Bug fix, allow allowable periods to be case insensitive (#72) --- app/controllers/application_controller.rb | 3 ++- .../user_classification_count_controller_spec.rb | 1 + spec/support/query_params_validator.rb | 6 ++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 3e52214..2f13f43 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -78,7 +78,8 @@ def validate_date(date_param) end def validate_period - raise ValidationError, 'Invalid bucket option. Valid options for period is day, week, month, or year' unless SelectableWithTimeBucket::TIME_BUCKET_OPTIONS.keys.include? params[:period].downcase.to_sym + params[:period] = params[:period].downcase + raise ValidationError, 'Invalid bucket option. Valid options for period is day, week, month, or year' unless SelectableWithTimeBucket::TIME_BUCKET_OPTIONS.keys.include? params[:period].to_sym end def valid_date_range diff --git a/spec/controllers/user_classification_count_controller_spec.rb b/spec/controllers/user_classification_count_controller_spec.rb index 1d7b57b..0f3aa54 100644 --- a/spec/controllers/user_classification_count_controller_spec.rb +++ b/spec/controllers/user_classification_count_controller_spec.rb @@ -92,6 +92,7 @@ end context 'param validations' do + before(:each) { authenticate!(is_panoptes_admin: true) } it_behaves_like 'ensure valid query params', :query, id: 1 it 'ensures you cannot query by workflow and project_contributions' do diff --git a/spec/support/query_params_validator.rb b/spec/support/query_params_validator.rb index ff2015d..0bf8a2e 100644 --- a/spec/support/query_params_validator.rb +++ b/spec/support/query_params_validator.rb @@ -18,6 +18,12 @@ expect(response.body).to include('Invalid bucket option. Valid options for period is day, week, month, or year') end + it 'allows period param to be case-insensitive' do + params[:period] = 'MONTH' + get query, params: params + expect(response.status).to eq(200) + end + it 'ensures that we do not query by both workflow and project' do params[:workflow_id] = 1 params[:project_id] = 2 From feff7d208a14ed597f7be8c2ec6662211bed8a9f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Oct 2024 11:31:06 -0500 Subject: [PATCH 3/3] Bump actionpack from 7.0.8.4 to 7.0.8.5 (#75) Bumps [actionpack](https://github.com/rails/rails) from 7.0.8.4 to 7.0.8.5. - [Release notes](https://github.com/rails/rails/releases) - [Changelog](https://github.com/rails/rails/blob/v7.2.1.1/actionpack/CHANGELOG.md) - [Commits](https://github.com/rails/rails/compare/v7.0.8.4...v7.0.8.5) --- updated-dependencies: - dependency-name: actionpack dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 124 +++++++++++++++++++++++++-------------------------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 56f0345..226c837 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,67 +1,67 @@ GEM remote: https://rubygems.org/ specs: - actioncable (7.0.8.4) - actionpack (= 7.0.8.4) - activesupport (= 7.0.8.4) + actioncable (7.0.8.5) + actionpack (= 7.0.8.5) + activesupport (= 7.0.8.5) 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) + actionmailbox (7.0.8.5) + actionpack (= 7.0.8.5) + activejob (= 7.0.8.5) + activerecord (= 7.0.8.5) + activestorage (= 7.0.8.5) + activesupport (= 7.0.8.5) 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.0.8.5) + actionpack (= 7.0.8.5) + actionview (= 7.0.8.5) + activejob (= 7.0.8.5) + activesupport (= 7.0.8.5) 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) + actionpack (7.0.8.5) + actionview (= 7.0.8.5) + activesupport (= 7.0.8.5) rack (~> 2.0, >= 2.2.4) 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) + actiontext (7.0.8.5) + actionpack (= 7.0.8.5) + activerecord (= 7.0.8.5) + activestorage (= 7.0.8.5) + activesupport (= 7.0.8.5) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8.4) - activesupport (= 7.0.8.4) + actionview (7.0.8.5) + activesupport (= 7.0.8.5) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (7.0.8.4) - activesupport (= 7.0.8.4) + activejob (7.0.8.5) + activesupport (= 7.0.8.5) 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) - 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) + activemodel (7.0.8.5) + activesupport (= 7.0.8.5) + activerecord (7.0.8.5) + activemodel (= 7.0.8.5) + activesupport (= 7.0.8.5) + activestorage (7.0.8.5) + actionpack (= 7.0.8.5) + activejob (= 7.0.8.5) + activerecord (= 7.0.8.5) + activesupport (= 7.0.8.5) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.8.4) + activesupport (7.0.8.5) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -69,12 +69,12 @@ GEM ast (2.4.2) bootsnap (1.16.0) msgpack (~> 1.2) - builder (3.2.4) + builder (3.3.0) byebug (11.1.3) coderay (1.1.3) composite_primary_keys (14.0.6) activerecord (~> 7.0.2) - concurrent-ruby (1.3.1) + concurrent-ruby (1.3.4) crass (1.0.6) database_cleaner (2.0.2) database_cleaner-active_record (>= 2, < 3) @@ -89,7 +89,7 @@ GEM deprecate (0.0.0) diff-lcs (1.5.0) docile (1.4.0) - erubi (1.12.0) + erubi (1.13.0) factory_bot (6.2.1) activesupport (>= 5.0.0) factory_bot_rails (6.2.0) @@ -125,7 +125,7 @@ GEM faraday (~> 1.0) globalid (1.2.1) activesupport (>= 6.1) - i18n (1.14.5) + i18n (1.14.6) concurrent-ruby (~> 1.0) io-console (0.6.0) irb (1.6.4) @@ -143,7 +143,7 @@ GEM marcel (1.0.2) method_source (1.0.0) mini_mime (1.1.5) - minitest (5.23.1) + minitest (5.25.1) msgpack (1.7.0) multipart-post (2.3.0) net-imap (0.4.10) @@ -157,9 +157,9 @@ GEM net-protocol newrelic_rpm (9.5.0) nio4r (2.7.3) - nokogiri (1.16.5-x86_64-darwin) + nokogiri (1.16.7-x86_64-darwin) racc (~> 1.4) - nokogiri (1.16.5-x86_64-linux) + nokogiri (1.16.7-x86_64-linux) racc (~> 1.4) panoptes-client (1.2.0) deprecate @@ -180,26 +180,26 @@ GEM nio4r (~> 2.0) pundit (2.3.0) activesupport (>= 3.0.0) - racc (1.8.0) - rack (2.2.9) + racc (1.8.1) + rack (2.2.10) rack-cors (2.0.2) rack (>= 2.0.0) 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) + rails (7.0.8.5) + actioncable (= 7.0.8.5) + actionmailbox (= 7.0.8.5) + actionmailer (= 7.0.8.5) + actionpack (= 7.0.8.5) + actiontext (= 7.0.8.5) + actionview (= 7.0.8.5) + activejob (= 7.0.8.5) + activemodel (= 7.0.8.5) + activerecord (= 7.0.8.5) + activestorage (= 7.0.8.5) + activesupport (= 7.0.8.5) bundler (>= 1.15.0) - railties (= 7.0.8.4) + railties (= 7.0.8.5) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -207,9 +207,9 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - railties (7.0.8.4) - actionpack (= 7.0.8.4) - activesupport (= 7.0.8.4) + railties (7.0.8.5) + actionpack (= 7.0.8.5) + activesupport (= 7.0.8.5) method_source rake (>= 12.2) thor (~> 1.0)