From 7b2aa7189dbd7eade07af988ec2e4d5e7d42ae2b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Mar 2025 22:23:27 +0000 Subject: [PATCH 01/59] Bump the bundler group across 1 directory with 2 updates Bumps the bundler group with 2 updates in the / directory: [rack](https://github.com/rack/rack) and [uri](https://github.com/ruby/uri). Updates `rack` from 3.1.10 to 3.1.12 - [Release notes](https://github.com/rack/rack/releases) - [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md) - [Commits](https://github.com/rack/rack/compare/v3.1.10...v3.1.12) Updates `uri` from 0.13.1 to 0.13.2 - [Release notes](https://github.com/ruby/uri/releases) - [Commits](https://github.com/ruby/uri/compare/v0.13.1...v0.13.2) --- updated-dependencies: - dependency-name: rack dependency-type: indirect dependency-group: bundler - dependency-name: uri dependency-type: indirect dependency-group: bundler ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index dd912d48..135bdc3a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -310,7 +310,7 @@ GEM rspec-mocks (~> 3.12) rspec-support (~> 3.12) racc (1.8.1) - rack (3.1.10) + rack (3.1.12) rack-accept (0.4.5) rack (>= 0.4) rack-protection (4.0.0) @@ -501,7 +501,7 @@ GEM uber (0.1.0) unaccent (0.4.0) unicode-display_width (2.5.0) - uri (0.13.1) + uri (0.13.2) useragent (0.16.11) warden (1.2.9) rack (>= 2.0.9) From 1c2fc2e65b239518b747d2a854e4423cfab4c168 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 11 Mar 2025 12:45:16 -0400 Subject: [PATCH 02/59] Simplify HTML structure in judging results partial view Remove unnecessary div wrapper in contest instance judging results template for cleaner markup --- app/views/contest_instances/_judging_results.html.erb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/contest_instances/_judging_results.html.erb b/app/views/contest_instances/_judging_results.html.erb index 33e87ec2..cb5e440a 100644 --- a/app/views/contest_instances/_judging_results.html.erb +++ b/app/views/contest_instances/_judging_results.html.erb @@ -1,5 +1,5 @@
-
+ <% if @contest_instance.judging_rounds.any? %> <% @contest_instance.judging_rounds.order(:round_number).each do |round| %>
@@ -103,5 +103,4 @@ <% else %>

No judging rounds have been created yet.

<% end %> -
From e4ad254bc65205daf9aef1024954b01d09441e8e Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 11 Mar 2025 12:45:31 -0400 Subject: [PATCH 03/59] Refactor manage judges partial for improved readability and structure Streamline the HTML structure of the contest instance judges management view by: - Removing unnecessary nested divs - Simplifying layout and spacing - Maintaining existing visual hierarchy and information display --- .../contest_instances/_manage_judges.html.erb | 159 +++++++++--------- 1 file changed, 75 insertions(+), 84 deletions(-) diff --git a/app/views/contest_instances/_manage_judges.html.erb b/app/views/contest_instances/_manage_judges.html.erb index 22195a9d..8f2d695f 100644 --- a/app/views/contest_instances/_manage_judges.html.erb +++ b/app/views/contest_instances/_manage_judges.html.erb @@ -1,93 +1,84 @@ - <%# Judging Information Section - Kept as a distinct card %> -
-
- -

Total Judging Rounds: - <%= contest_instance.actual_judging_rounds_count %>

-
- <%= render 'management_links' %> +
+

Total Judging Rounds:<%= contest_instance.actual_judging_rounds_count %>

+ <%= render 'management_links' %> +
+ <% contest_instance.judging_rounds.order(:round_number).each do |round| %> +
+
+
Round <%= round.round_number %>
+ + <%= round.active ? 'Active' : (round.completed ? 'Completed' : 'Pending') %> + +
+
+
+
+ Dates: + <%= format_datetime(round.start_date) %> - <%= format_datetime(round.end_date) %> +
+
-
- <% contest_instance.judging_rounds.order(:round_number).each do |round| %> -
-
-
Round <%= round.round_number %>
- - <%= round.active ? 'Active' : (round.completed ? 'Completed' : 'Pending') %> - +
+
+
+ Judges Assigned: +
+ <% if round.judges.any? %> + <% round.judges.each do |judge| %> + + <%= judge.display_name_and_uid %> + + <% end %> + <% else %> + No judges assigned to this round + <% end %>
-
-
-
- Dates: - <%= format_datetime(round.start_date) %> - <%= format_datetime(round.end_date) %> -
-
- -
-
-
- Judges Assigned: -
- <% if round.judges.any? %> - <% round.judges.each do |judge| %> - - <%= judge.display_name_and_uid %> - - <% end %> - <% else %> - No judges assigned to this round - <% end %> -
-
-
-
- -
-
- Minimum entries to be evaluated per judge: - <%= round.required_entries_count %> -
-
+
+
+
-
- Comment requirements: -
-

Internal: - <%= round.require_internal_comments ? 'Required' : 'Optional' %> - <% if round.require_internal_comments && round.min_internal_comment_words > 0 %> - (minimum <%= pluralize(round.min_internal_comment_words, 'word') %>) - <% end %> -

-
-
-

External: - <%= round.require_external_comments ? 'Required' : 'Optional' %> - <% if round.require_external_comments && round.min_external_comment_words > 0 %> - (minimum <%= pluralize(round.min_external_comment_words, 'word') %>) - <% end %> -

-
-
-
+
+
+ Minimum entries to be evaluated per judge: + <%= round.required_entries_count %> +
+
- <% if round.special_instructions.present? %> -
-
-
- Instructions:
- <%= simple_format(round.special_instructions) %> -
-
-
+
+ Comment requirements: +
+

Internal: + <%= round.require_internal_comments ? 'Required' : 'Optional' %> + <% if round.require_internal_comments && round.min_internal_comment_words > 0 %> + (minimum <%= pluralize(round.min_internal_comment_words, 'word') %>) <% end %> -

+

+
+
+

External: + <%= round.require_external_comments ? 'Required' : 'Optional' %> + <% if round.require_external_comments && round.min_external_comment_words > 0 %> + (minimum <%= pluralize(round.min_external_comment_words, 'word') %>) + <% end %> +

- <% end %>
+ + <% if round.special_instructions.present? %> +
+
+
+ Instructions:
+ <%= simple_format(round.special_instructions) %> +
+
+
+ <% end %>
-
+ <% end %> +
+
From de6599641c0c64fe43c1f77499849510657f6f7a Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 11 Mar 2025 17:25:54 -0400 Subject: [PATCH 04/59] Add contact email validation to Container model Enhance Container model with: - New contact_email attribute - Presence validation for contact email - Email format validation using URI::MailTo::EMAIL_REGEXP - Updated strong parameters in ContainersController to permit contact_email --- app/controllers/containers_controller.rb | 2 +- app/models/container.rb | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/controllers/containers_controller.rb b/app/controllers/containers_controller.rb index a4b1f7f3..aa76f1d7 100644 --- a/app/controllers/containers_controller.rb +++ b/app/controllers/containers_controller.rb @@ -110,7 +110,7 @@ def authorize_index end def container_params - params.require(:container).permit(:name, :description, :notes, :department_id, :visibility_id, + params.require(:container).permit(:name, :description, :notes, :contact_email, :department_id, :visibility_id, assignments_attributes: %i[id user_id role_id _destroy]) end end diff --git a/app/models/container.rb b/app/models/container.rb index 27120add..9ac37f82 100644 --- a/app/models/container.rb +++ b/app/models/container.rb @@ -5,6 +5,7 @@ # Table name: containers # # id :bigint not null, primary key +# contact_email :string(255) # name :string(255) # notes :text(65535) # created_at :datetime not null @@ -40,6 +41,8 @@ class Container < ApplicationRecord validates :name, presence: true, uniqueness: true validates :department_id, presence: { message: 'You must select a department' } validates :visibility_id, presence: { message: 'You must select a visibility option' } + validates :contact_email, presence: { message: 'You must enter a contact email' } + validates :contact_email, format: { with: URI::MailTo::EMAIL_REGEXP, message: 'You must enter a valid email address' } scope :visible, -> { joins(:visibility).where(visibilities: { kind: 'Public' }) } # Only show containers with 'Public' visibility From a8c75b875836ea54ad305d38f505c3bec264e1c2 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 11 Mar 2025 17:26:17 -0400 Subject: [PATCH 05/59] Add contact email field to Container form Update the Container form to include: - New contact_email input field - Helpful hint explaining the purpose of the contact email - Maintain existing required fields for name and department --- app/views/containers/_form.html.erb | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/app/views/containers/_form.html.erb b/app/views/containers/_form.html.erb index 3471a71f..142b67fc 100644 --- a/app/views/containers/_form.html.erb +++ b/app/views/containers/_form.html.erb @@ -6,16 +6,18 @@
<%= f.input :name, required: true %> <%= f.input :description, as: :rich_text_area %> - - <%= f.input :department_id, - collection: Department.all, - label_method: :name, - value_method: :id, + <%= f.input :contact_email, + hint: "This email will be displayed in notifications to applicants as the contact for questions about contests in this collection." %> + + <%= f.input :department_id, + collection: Department.all, + label_method: :name, + value_method: :id, required: true %> - - <%= f.input :visibility_id, - collection: Visibility.all, - label_method: :kind, + + <%= f.input :visibility_id, + collection: Visibility.all, + label_method: :kind, value_method: :id, hint: safe_join([ content_tag(:strong, "Public"), " visibility means that this collection of contests will be visible in the applicant's dashboard. ", From c969cf387413d5455ae10c3e4f92c237ba375e10 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 11 Mar 2025 17:26:41 -0400 Subject: [PATCH 06/59] Add tracking columns for emails and contact information Add database columns to support email tracking and contact details: - `emails_sent_count` to JudgingRounds for tracking email communications - `contact_email` to Containers for storing additional contact information --- ...20250311192043_add_emails_sent_count_to_judging_rounds.rb | 5 +++++ db/migrate/20250311193603_add_contact_email_to_containers.rb | 5 +++++ db/schema.rb | 4 +++- 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20250311192043_add_emails_sent_count_to_judging_rounds.rb create mode 100644 db/migrate/20250311193603_add_contact_email_to_containers.rb diff --git a/db/migrate/20250311192043_add_emails_sent_count_to_judging_rounds.rb b/db/migrate/20250311192043_add_emails_sent_count_to_judging_rounds.rb new file mode 100644 index 00000000..08a40fe1 --- /dev/null +++ b/db/migrate/20250311192043_add_emails_sent_count_to_judging_rounds.rb @@ -0,0 +1,5 @@ +class AddEmailsSentCountToJudgingRounds < ActiveRecord::Migration[7.2] + def change + add_column :judging_rounds, :emails_sent_count, :integer, default: 0, null: false + end +end diff --git a/db/migrate/20250311193603_add_contact_email_to_containers.rb b/db/migrate/20250311193603_add_contact_email_to_containers.rb new file mode 100644 index 00000000..817dbc26 --- /dev/null +++ b/db/migrate/20250311193603_add_contact_email_to_containers.rb @@ -0,0 +1,5 @@ +class AddContactEmailToContainers < ActiveRecord::Migration[7.2] + def change + add_column :containers, :contact_email, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index abae56cb..aecd7dfa 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.2].define(version: 2025_01_08_193135) do +ActiveRecord::Schema[7.2].define(version: 2025_03_11_193603) do create_table "action_text_rich_texts", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.string "name", null: false t.text "body", size: :long @@ -147,6 +147,7 @@ t.bigint "visibility_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "contact_email" t.index ["department_id"], name: "index_containers_on_department_id" t.index ["visibility_id"], name: "index_containers_on_visibility_id" end @@ -269,6 +270,7 @@ t.integer "min_external_comment_words", default: 0, null: false t.text "special_instructions" t.integer "required_entries_count", default: 0, null: false + t.integer "emails_sent_count", default: 0, null: false t.index ["contest_instance_id", "round_number"], name: "index_judging_rounds_on_contest_instance_id_and_round_number", unique: true t.index ["contest_instance_id"], name: "index_judging_rounds_on_contest_instance_id" end From 878046d1aa886f1c53d535d8fb3972b89a9195c6 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 11 Mar 2025 17:27:03 -0400 Subject: [PATCH 07/59] Configure inline Active Job adapter for development email processing Set up inline queue adapter for Active Job in development environment to ensure immediate email delivery and improve testing workflow with letter_opener_web --- config/environments/development.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/environments/development.rb b/config/environments/development.rb index 148d0bba..7715d246 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -51,6 +51,10 @@ config.action_mailer.delivery_method = :letter_opener_web config.action_mailer.perform_deliveries = true + # Use the inline adapter for Active Job in development so emails sent with deliver_later + # will be processed immediately and show up in letter_opener_web + config.active_job.queue_adapter = :inline + # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log From 5a608cb39523ed9171462591b63ee5ff1bd59017 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 11 Mar 2025 17:27:44 -0400 Subject: [PATCH 08/59] Add ResultsMailer for entry evaluation notifications Implement comprehensive email notification system for contest entries: - Create ResultsMailer with entry_evaluation_notification method - Add HTML and plain text email templates - Support dynamic content including entry details, rankings, and judge comments - Implement flexible contact email fallback mechanism - Style email templates with responsive design and clear visual hierarchy --- app/mailers/results_mailer.rb | 36 +++++ .../entry_evaluation_notification.html.erb | 141 ++++++++++++++++++ .../entry_evaluation_notification.text.erb | 51 +++++++ 3 files changed, 228 insertions(+) create mode 100644 app/mailers/results_mailer.rb create mode 100644 app/views/mailers/results_mailer/entry_evaluation_notification.html.erb create mode 100644 app/views/mailers/results_mailer/entry_evaluation_notification.text.erb diff --git a/app/mailers/results_mailer.rb b/app/mailers/results_mailer.rb new file mode 100644 index 00000000..ae174bde --- /dev/null +++ b/app/mailers/results_mailer.rb @@ -0,0 +1,36 @@ +class ResultsMailer < ApplicationMailer + def entry_evaluation_notification(entry, round) + @entry = entry + @round = round + @profile = entry.profile + @user = @profile.user + @contest_instance = entry.contest_instance + @contest_description = @contest_instance.contest_description + @container = @contest_description.container + + # Get the contact email from the container, with fallbacks if not present + @contact_email = @container.contact_email.presence || + Rails.application.credentials.dig(:mailer, :default_contact_email) || + Rails.application.credentials.dig(:devise, :mailer_sender) || + 'contests@example.com' + + # Get all rankings for this entry in this round + @rankings = EntryRanking.where(entry: @entry, judging_round: @round) + + # Calculate the average rank + @avg_rank = @round.average_rank_for_entry(@entry) + + # Check if the entry was selected for the next round + @selected_for_next_round = @rankings.any?(&:selected_for_next_round?) + + # Only include external comments that are meant to be shared with applicants + @external_comments = @rankings.map(&:external_comments).compact.reject(&:empty?) + + subject = "Evaluation Results for \"#{@entry.title}\" - #{@contest_description.name}" + + mail( + to: @user.email, + subject: subject + ) + end +end diff --git a/app/views/mailers/results_mailer/entry_evaluation_notification.html.erb b/app/views/mailers/results_mailer/entry_evaluation_notification.html.erb new file mode 100644 index 00000000..31f2605b --- /dev/null +++ b/app/views/mailers/results_mailer/entry_evaluation_notification.html.erb @@ -0,0 +1,141 @@ + + + + + + + +
+
+ + +

<%= @contest_description.name %>

+
+ +

Dear <%= @user.display_name_or_first_name_last_name %>,

+ +

Thank you for your submission to the <%= @contest_description.name %>. We appreciate your participation and the effort you put into your work.

+ +
+

Your Entry Details

+

Title: <%= @entry.title %>

+

Submitted: <%= @entry.created_at.strftime("%B %d, %Y") %>

+ <% if @entry.category.present? %> +

Category: <%= @entry.category.kind %>

+ <% end %> + <% if @entry.pen_name.present? %> +

Pen Name: <%= @entry.pen_name %>

+ <% end %> +
+ +

Evaluation Results - Round <%= @round.round_number %>

+ + <% if @avg_rank.present? %> +

Your submission received an average ranking of <%= @avg_rank %> from our judging panel.

+ <% end %> + + <% if @selected_for_next_round && @contest_instance.judging_rounds.exists?(round_number: @round.round_number + 1) %> +
+

Congratulations! Your entry has been selected to advance to Round <%= @round.round_number + 1 %>.

+
+ <% elsif @round.round_number == @contest_instance.judging_rounds.maximum(:round_number) %> + <% if @selected_for_next_round %> +
+

Congratulations! Your entry has been selected as a finalist.

+
+ <% else %> +
+

We regret to inform you that your entry was not selected as a finalist.

+
+ <% end %> + <% elsif !@selected_for_next_round %> +
+

We regret to inform you that your entry was not selected to advance to the next round of judging.

+
+ <% end %> + + <% if @external_comments.any? %> +

Feedback from Judges

+

Our judges have provided the following feedback on your submission:

+ + <% @external_comments.each do |comment| %> +
+ <%= simple_format(comment) %> +
+ <% end %> + <% end %> + +

We value your creativity and hope you found this competition to be a rewarding experience. Your participation contributes to the vibrant artistic and academic community at our university.

+ + <% if @contact_email.present? %> +

If you have any questions about the judging process or results, please contact <%= @contact_email %>.

+ <% end %> + + +
+ + diff --git a/app/views/mailers/results_mailer/entry_evaluation_notification.text.erb b/app/views/mailers/results_mailer/entry_evaluation_notification.text.erb new file mode 100644 index 00000000..6c9ad2cf --- /dev/null +++ b/app/views/mailers/results_mailer/entry_evaluation_notification.text.erb @@ -0,0 +1,51 @@ +<%= @contest_description.name %> + +Dear <%= @user.display_name_or_first_name_last_name %>, + +Thank you for your submission to the <%= @contest_description.name %>. We appreciate your participation and the effort you put into your work. + +YOUR ENTRY DETAILS +================= +Title: <%= @entry.title %> +Submitted: <%= @entry.created_at.strftime("%B %d, %Y") %> +<% if @entry.category.present? %>Category: <%= @entry.category.kind %><% end %> +<% if @entry.pen_name.present? %>Pen Name: <%= @entry.pen_name %><% end %> + +EVALUATION RESULTS - ROUND <%= @round.round_number %> +================= +<% if @avg_rank.present? %> +Your submission received an average ranking of <%= @avg_rank %> from our judging panel. +<% end %> + +<% if @selected_for_next_round && @contest_instance.judging_rounds.exists?(round_number: @round.round_number + 1) %> +CONGRATULATIONS! Your entry has been selected to advance to Round <%= @round.round_number + 1 %>. +<% elsif @round.round_number == @contest_instance.judging_rounds.maximum(:round_number) %> +<% if @selected_for_next_round %> +CONGRATULATIONS! Your entry has been selected as a finalist. +<% else %> +We regret to inform you that your entry was not selected as a finalist. +<% end %> +<% elsif !@selected_for_next_round %> +We regret to inform you that your entry was not selected to advance to the next round of judging. +<% end %> + +<% if @external_comments.any? %> +FEEDBACK FROM JUDGES +================= +Our judges have provided the following feedback on your submission: + +<% @external_comments.each do |comment| %> +* <%= comment.gsub(/\n/, "\n ") %> + +<% end %> +<% end %> + +We value your creativity and hope you found this competition to be a rewarding experience. Your participation contributes to the vibrant artistic and academic community at our university. + +<% if @contact_email.present? %> +If you have any questions about the judging process or results, please contact <%= @contact_email %>. +<% end %> + +--- +This is an automated email. Please do not reply to this message. +© <%= Date.today.year %> <%= @container.name %> | All Rights Reserved From 9ff083751e4878258f2ac5436f7f1fa689bb8d51 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 11 Mar 2025 17:27:53 -0400 Subject: [PATCH 09/59] Add complete? method to JudgingRound for view compatibility Introduce an alias method to match view expectations and improve code readability by providing a more intuitive method name for checking round completion status --- app/models/judging_round.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/models/judging_round.rb b/app/models/judging_round.rb index fdc3fd1b..1cd1d65a 100644 --- a/app/models/judging_round.rb +++ b/app/models/judging_round.rb @@ -5,6 +5,7 @@ # id :bigint not null, primary key # active :boolean default(FALSE), not null # completed :boolean default(FALSE), not null +# emails_sent_count :integer default(0), not null # end_date :datetime # min_external_comment_words :integer default(0), not null # min_internal_comment_words :integer default(0), not null @@ -48,6 +49,11 @@ class JudgingRound < ApplicationRecord before_create :set_active_by_default + # Alias for completed? to match what's used in views + def complete? + completed? + end + def activate! return false unless valid? From 8a74459d29f7e500aa67721396efdc339550bc0e Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 11 Mar 2025 17:28:22 -0400 Subject: [PATCH 10/59] Add send_round_results route for ContestInstance Extend contest instance routes with a custom member route to support sending round results, enabling more granular control over result communication processes --- config/routes.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/routes.rb b/config/routes.rb index d05e9120..0533a2b2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -34,6 +34,9 @@ resources :containers do resources :contest_descriptions do resources :contest_instances do + member do + post 'send_round_results' + end resources :judging_rounds do member do patch :activate From 87ac3461c8a4145445029ec01dae4d515124fb0f Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 11 Mar 2025 17:28:59 -0400 Subject: [PATCH 11/59] Implement send_round_results action for ContestInstances Add functionality to send evaluation results emails for a specific judging round: - Implement send_round_results method in ContestInstancesController - Add authorization check for managing contest instances - Validate round completion before sending emails - Support both immediate and background email delivery based on environment - Track and increment emails sent count for the judging round - Provide user-friendly redirect with success notification --- .../contest_instances_controller.rb | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/app/controllers/contest_instances_controller.rb b/app/controllers/contest_instances_controller.rb index 4d3e727f..045c718d 100644 --- a/app/controllers/contest_instances_controller.rb +++ b/app/controllers/contest_instances_controller.rb @@ -1,7 +1,7 @@ class ContestInstancesController < ApplicationController before_action :set_container before_action :set_contest_description - before_action :set_contest_instance, only: %i[show edit update destroy] + before_action :set_contest_instance, only: %i[show edit update destroy send_round_results] before_action :authorize_container_access # GET /contest_instances @@ -78,6 +78,47 @@ def destroy end end + # POST /containers/:container_id/contest_descriptions/:contest_description_id/contest_instances/:id/send_round_results + def send_round_results + authorize @contest_instance, :manage? + + round_id = params[:round_id] + judging_round = @contest_instance.judging_rounds.find_by(id: round_id) + + if judging_round.nil? + redirect_to container_contest_description_contest_instance_path(@container, @contest_description, @contest_instance), + alert: 'Judging round not found.' + return + end + + if !judging_round.completed? + redirect_to container_contest_description_contest_instance_path(@container, @contest_description, @contest_instance), + alert: 'Cannot send results for an incomplete judging round.' + return + end + + # Get all entries for this round + entries = judging_round.entries.uniq + + email_count = 0 + + # Send an email for each entry + entries.each do |entry| + if Rails.env.development? + ResultsMailer.entry_evaluation_notification(entry, judging_round).deliver_now + else + ResultsMailer.entry_evaluation_notification(entry, judging_round).deliver_later + end + email_count += 1 + end + + # Increment the emails sent counter for this round + judging_round.increment!(:emails_sent_count) + + redirect_to container_contest_description_contest_instance_path(@container, @contest_description, @contest_instance), + notice: "Successfully queued #{email_count} evaluation result emails for round #{judging_round.round_number}. This is email batch ##{judging_round.emails_sent_count}." + end + private def authorize_container_access From 5b0646e7f5c472b45ef3b9aeb6fdd26105180206 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 11 Mar 2025 17:29:20 -0400 Subject: [PATCH 12/59] Enhance judging results view with improved styling and email tracking Update the judging results partial to: - Add card styling for each judging round - Include email send button with tooltip and contact email display - Show email sent count badge for tracking round communications - Maintain existing functionality for displaying entry rankings and comments --- .../_judging_results.html.erb | 212 ++++++++++-------- 1 file changed, 122 insertions(+), 90 deletions(-) diff --git a/app/views/contest_instances/_judging_results.html.erb b/app/views/contest_instances/_judging_results.html.erb index cb5e440a..8504c526 100644 --- a/app/views/contest_instances/_judging_results.html.erb +++ b/app/views/contest_instances/_judging_results.html.erb @@ -1,106 +1,138 @@
+ <% if @contest_instance.judging_rounds.any? %> + <% @contest_instance.judging_rounds.order(:round_number).each do |round| %> +
+

Round <%= round.round_number %>

+
+
+ <%= button_to send_round_results_container_contest_description_contest_instance_path( + @container, + @contest_description, + @contest_instance, + round_id: round.id + ), + method: :post, + class: "btn btn-sm btn-info me-3", + disabled: !round.complete?, + data: { + confirm: "Are you sure you want to send evaluation results to all applicants for round #{round.round_number}?" + } do %> + + Email round <%= round.round_number %> results + <% end %> +
- <% if @contest_instance.judging_rounds.any? %> - <% @contest_instance.judging_rounds.order(:round_number).each do |round| %> -
-

Round <%= round.round_number %>

-
- - - - - - - - - - - - - <% entries_with_avg_rank = round.entries.distinct.map do |entry| - avg_rank = entry.entry_rankings.where(judging_round: round).average(:rank) - [entry, avg_rank || Float::INFINITY] - end.sort_by { |_, avg_rank| avg_rank } %> + <% if round.emails_sent_count > 0 %> + + + Emails sent: <%= round.emails_sent_count %> time<%= 's' if round.emails_sent_count > 1 %> + + <% end %> + +
+
Entry IDTitleAverage RankIndividual RankingsCommentsSelected for Next Round
+ + + + + + + + + + + + <% entries_with_avg_rank = round.entries.distinct.map do |entry| + avg_rank = entry.entry_rankings.where(judging_round: round).average(:rank) + [entry, avg_rank || Float::INFINITY] + end.sort_by { |_, avg_rank| avg_rank } %> - <% entries_with_avg_rank.each do |entry, _| %> - - - - - - + + + + + - - - <% end %> - -
Entry IDTitleAverage RankIndividual RankingsCommentsSelected for Next Round
<%= entry.id %><%= entry.title %> - <%= entry.entry_rankings.where(judging_round: round).average(:rank)&.round(2) || 'No rankings' %> - -
- <% rankings = entry.entry_rankings.where(judging_round: round) %> - <% if rankings.any? %> - <% rankings.each do |ranking| %> -
-
- <%= content_tag(:span, - display_email(ranking.user.email), - data: { - 'bs-toggle': 'tooltip', - 'bs-html': 'true', - 'bs-title': "#{h(ranking.user.display_name_or_first_name_last_name)}
#{h(display_email(ranking.user.email))}" - } - ) %> -
- <%= ranking.rank || 'Not ranked' %> -
- <% end %> - <% else %> -
No rankings yet
- <% end %> -
-
-
- <% rankings = entry.entry_rankings.where(judging_round: round) %> - <% has_comments = false %> + <% entries_with_avg_rank.each do |entry, _| %> +
<%= entry.id %><%= entry.title %> + <%= entry.entry_rankings.where(judging_round: round).average(:rank)&.round(2) || 'No rankings' %> + +
+ <% rankings = entry.entry_rankings.where(judging_round: round) %> + <% if rankings.any? %> <% rankings.each do |ranking| %> - <% if ranking.external_comments.present? || ranking.internal_comments.present? %> - <% has_comments = true %> -
- <%= content_tag(:span, +
+
+ <%= content_tag(:span, display_email(ranking.user.email), data: { 'bs-toggle': 'tooltip', 'bs-html': 'true', 'bs-title': "#{h(ranking.user.display_name_or_first_name_last_name)}
#{h(display_email(ranking.user.email))}" } - ) %>
- <% if ranking.external_comments.present? %> -
- External: - <%= ranking.external_comments %> -
- <% end %> - <% if ranking.internal_comments.present? %> -
- Internal: - <%= ranking.internal_comments %> -
- <% end %> + ) %>
- <% end %> + <%= ranking.rank || 'Not ranked' %> +
<% end %> - <% unless has_comments %> -
No comments yet
+ <% else %> +
No rankings yet
+ <% end %> +
+
+
+ <% rankings = entry.entry_rankings.where(judging_round: round) %> + <% has_comments = false %> + <% rankings.each do |ranking| %> + <% if ranking.external_comments.present? || ranking.internal_comments.present? %> + <% has_comments = true %> +
+ <%= content_tag(:span, + display_email(ranking.user.email), + data: { + 'bs-toggle': 'tooltip', + 'bs-html': 'true', + 'bs-title': "#{h(ranking.user.display_name_or_first_name_last_name)}
#{h(display_email(ranking.user.email))}" + } + ) %>
+ <% if ranking.external_comments.present? %> +
+ External: + <%= ranking.external_comments %> +
+ <% end %> + <% if ranking.internal_comments.present? %> +
+ Internal: + <%= ranking.internal_comments %> +
+ <% end %> +
<% end %> -
-
-
- <%= boolean_to_yes_no(entry.entry_rankings.where(judging_round: round, selected_for_next_round: true).exists?) %> -
-
+ <% end %> + <% unless has_comments %> +
No comments yet
+ <% end %> +
+
+ + + <%= boolean_to_yes_no(entry.entry_rankings.where(judging_round: round, selected_for_next_round: true).exists?) %> + + + <% end %> + +
- <% end %> - <% else %> -

No judging rounds have been created yet.

+
<% end %> + <% else %> +

No judging rounds have been created yet.

+ <% end %>
From ce1bcb8055c83a0f502d380526051d51f4ccd7ca Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 11 Mar 2025 17:29:40 -0400 Subject: [PATCH 13/59] Add authorization method for sending round results in ContestInstancePolicy Extend ContestInstancePolicy with a new authorization method `send_round_results?` to support access control for sending contest round evaluation results, maintaining consistent permission checks with existing methods --- app/policies/contest_instance_policy.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/policies/contest_instance_policy.rb b/app/policies/contest_instance_policy.rb index 0cf147a5..fffa44d5 100644 --- a/app/policies/contest_instance_policy.rb +++ b/app/policies/contest_instance_policy.rb @@ -58,7 +58,11 @@ def activate? user&.has_container_role?(record.contest_description.container) || axis_mundi? end - def deactivate? + def deactivate? + user&.has_container_role?(record.contest_description.container) || axis_mundi? + end + + def send_round_results? user&.has_container_role?(record.contest_description.container) || axis_mundi? end end From 45d3eaaa1e3242d9151932348f012a85a0673017 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 11 Mar 2025 17:29:49 -0400 Subject: [PATCH 14/59] Add emails_sent_count and contact_email to model specs and factories Update test factories and model specs for Containers and JudgingRounds to reflect new database columns: - Add contact_email to Container factory and spec - Add emails_sent_count to JudgingRound factory and spec --- spec/factories/containers.rb | 1 + spec/factories/judging_rounds.rb | 1 + spec/models/container_spec.rb | 1 + spec/models/judging_round_spec.rb | 1 + 4 files changed, 4 insertions(+) diff --git a/spec/factories/containers.rb b/spec/factories/containers.rb index 5e8eecb3..8afb5917 100644 --- a/spec/factories/containers.rb +++ b/spec/factories/containers.rb @@ -3,6 +3,7 @@ # Table name: containers # # id :bigint not null, primary key +# contact_email :string(255) # name :string(255) # notes :text(65535) # created_at :datetime not null diff --git a/spec/factories/judging_rounds.rb b/spec/factories/judging_rounds.rb index 73632fa9..4cdfcbef 100644 --- a/spec/factories/judging_rounds.rb +++ b/spec/factories/judging_rounds.rb @@ -5,6 +5,7 @@ # id :bigint not null, primary key # active :boolean default(FALSE), not null # completed :boolean default(FALSE), not null +# emails_sent_count :integer default(0), not null # end_date :datetime # min_external_comment_words :integer default(0), not null # min_internal_comment_words :integer default(0), not null diff --git a/spec/models/container_spec.rb b/spec/models/container_spec.rb index 73903c74..83aef2a5 100644 --- a/spec/models/container_spec.rb +++ b/spec/models/container_spec.rb @@ -3,6 +3,7 @@ # Table name: containers # # id :bigint not null, primary key +# contact_email :string(255) # name :string(255) # notes :text(65535) # created_at :datetime not null diff --git a/spec/models/judging_round_spec.rb b/spec/models/judging_round_spec.rb index 19b31bde..0bdef978 100644 --- a/spec/models/judging_round_spec.rb +++ b/spec/models/judging_round_spec.rb @@ -5,6 +5,7 @@ # id :bigint not null, primary key # active :boolean default(FALSE), not null # completed :boolean default(FALSE), not null +# emails_sent_count :integer default(0), not null # end_date :datetime # min_external_comment_words :integer default(0), not null # min_internal_comment_words :integer default(0), not null From c9467348ae6a03b515b84666bd3c5db9929984ba Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 11 Mar 2025 17:33:28 -0400 Subject: [PATCH 15/59] Refactor judging results view to use Stimulus confirm controller Update the judging results partial to leverage Stimulus for confirmation dialog: - Replace inline `confirm` attribute with Stimulus `confirm` controller - Use `confirm_message_value` to dynamically set confirmation text - Maintain existing email send button functionality and styling --- app/views/contest_instances/_judging_results.html.erb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/contest_instances/_judging_results.html.erb b/app/views/contest_instances/_judging_results.html.erb index 8504c526..fb2e7378 100644 --- a/app/views/contest_instances/_judging_results.html.erb +++ b/app/views/contest_instances/_judging_results.html.erb @@ -19,7 +19,8 @@ class: "btn btn-sm btn-info me-3", disabled: !round.complete?, data: { - confirm: "Are you sure you want to send evaluation results to all applicants for round #{round.round_number}?" + controller: "confirm", + confirm_message_value: "Are you sure you want to send evaluation results to all applicants for round #{round.round_number}?" } do %> Email round <%= round.round_number %> results From d6b03fecbad0ab11b7cfb9b39b0b896dc17fbab7 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Wed, 12 Mar 2025 09:31:52 -0400 Subject: [PATCH 16/59] Update authorization check in ContestInstancesController for sending round results Refactor the authorization method in the send_round_results action to use the new `send_round_results?` method, ensuring consistent permission checks in line with the updated ContestInstancePolicy. --- app/controllers/contest_instances_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/contest_instances_controller.rb b/app/controllers/contest_instances_controller.rb index 045c718d..d8475374 100644 --- a/app/controllers/contest_instances_controller.rb +++ b/app/controllers/contest_instances_controller.rb @@ -80,7 +80,7 @@ def destroy # POST /containers/:container_id/contest_descriptions/:contest_description_id/contest_instances/:id/send_round_results def send_round_results - authorize @contest_instance, :manage? + authorize @contest_instance, :send_round_results? round_id = params[:round_id] judging_round = @contest_instance.judging_rounds.find_by(id: round_id) From 2034d2bb3c763b098713920ba92897383e51de29 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Wed, 12 Mar 2025 11:05:56 -0400 Subject: [PATCH 17/59] Configure test environment for SSL and ActiveJob settings - Add commented configuration for forcing SSL in the test environment. - Set ActiveJob to run synchronously in the test environment for immediate job execution during tests. --- config/environments/test.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/environments/test.rb b/config/environments/test.rb index e44d6803..a44ad823 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -45,6 +45,12 @@ # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Configure ActiveJob to run synchronously in test environment + config.active_job.queue_adapter = :inline + # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr From b6ddd2cfcfeea3b3f370ddb0855783a9d01fc88a Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Wed, 12 Mar 2025 11:06:26 -0400 Subject: [PATCH 18/59] Add tests for ContestInstancesController and ResultsMailer - Introduce comprehensive RSpec tests for the send_round_results action in ContestInstancesController, covering valid and invalid scenarios, including email sending and authorization checks. - Create a new ResultsMailer spec to validate the entry_evaluation_notification method, ensuring correct email content and headers. - Update container factory to include contact_email and enhance container and judging round specs to reflect new functionality. - Add system tests for judging results email interactions, verifying UI behavior and email sending functionality. --- .../contest_instances_controller_spec.rb | 126 ++++++++++++++++++ spec/factories/containers.rb | 1 + spec/mailers/results_mailer_spec.rb | 126 ++++++++++++++++++ spec/models/container_spec.rb | 3 +- spec/models/judging_round_spec.rb | 51 +++++++ spec/system/judging_results_email_spec.rb | 123 +++++++++++++++++ 6 files changed, 429 insertions(+), 1 deletion(-) create mode 100644 spec/controllers/contest_instances_controller_spec.rb create mode 100644 spec/mailers/results_mailer_spec.rb create mode 100644 spec/system/judging_results_email_spec.rb diff --git a/spec/controllers/contest_instances_controller_spec.rb b/spec/controllers/contest_instances_controller_spec.rb new file mode 100644 index 00000000..185577e7 --- /dev/null +++ b/spec/controllers/contest_instances_controller_spec.rb @@ -0,0 +1,126 @@ +require 'rails_helper' + +RSpec.describe ContestInstancesController, type: :controller do + # Add existing specs if there are any... + + describe 'POST #send_round_results' do + let(:department) { create(:department) } + let(:user) { create(:user, :axis_mundi) } # Admin user with full privileges + let(:container) { create(:container, department: department) } + let(:contest_description) { create(:contest_description, container: container) } + let(:contest_instance) { create(:contest_instance, contest_description: contest_description) } + + before do + sign_in user + end + + context 'with valid judging round' do + let(:judging_round) { create(:judging_round, contest_instance: contest_instance, completed: true) } + let(:profile1) { create(:profile) } + let(:profile2) { create(:profile) } + let(:entry1) { create(:entry, contest_instance: contest_instance, profile: profile1) } + let(:entry2) { create(:entry, contest_instance: contest_instance, profile: profile2) } + let!(:entry_ranking1) { create(:entry_ranking, :with_assigned_judge, entry: entry1, judging_round: judging_round) } + let!(:entry_ranking2) { create(:entry_ranking, :with_assigned_judge, entry: entry2, judging_round: judging_round) } + + before do + # Add entries to the judging round + allow(judging_round).to receive(:entries).and_return([ entry1, entry2 ]) + allow(judging_round.entries).to receive(:uniq).and_return([ entry1, entry2 ]) + + # Configure ActiveJob to use inline adapter for testing + ActiveJob::Base.queue_adapter = :inline + end + + it 'sends emails for each entry' do + expect(ResultsMailer).to receive(:entry_evaluation_notification).with(entry1, judging_round).and_call_original + expect(ResultsMailer).to receive(:entry_evaluation_notification).with(entry2, judging_round).and_call_original + + expect { + post :send_round_results, params: { + container_id: container.id, + contest_description_id: contest_description.id, + id: contest_instance.id, + round_id: judging_round.id + } + }.to change { ActionMailer::Base.deliveries.count }.by(2) + end + + it 'increments the emails_sent_count for the judging round' do + expect { + post :send_round_results, params: { + container_id: container.id, + contest_description_id: contest_description.id, + id: contest_instance.id, + round_id: judging_round.id + } + }.to change { judging_round.reload.emails_sent_count }.by(1) + end + + it 'redirects with a success notice' do + post :send_round_results, params: { + container_id: container.id, + contest_description_id: contest_description.id, + id: contest_instance.id, + round_id: judging_round.id + } + + expect(response).to redirect_to(container_contest_description_contest_instance_path(container, contest_description, contest_instance)) + expect(flash[:notice]).to include("Successfully queued 2 evaluation result emails") + expect(flash[:notice]).to include("email batch #1") + end + end + + context 'with non-existent judging round' do + it 'redirects with an alert' do + post :send_round_results, params: { + container_id: container.id, + contest_description_id: contest_description.id, + id: contest_instance.id, + round_id: 9999 # Non-existent round + } + + expect(response).to redirect_to(container_contest_description_contest_instance_path(container, contest_description, contest_instance)) + expect(flash[:alert]).to eq('Judging round not found.') + end + end + + context 'with incomplete judging round' do + let(:judging_round) { create(:judging_round, contest_instance: contest_instance, completed: false) } + + it 'redirects with an alert' do + post :send_round_results, params: { + container_id: container.id, + contest_description_id: contest_description.id, + id: contest_instance.id, + round_id: judging_round.id + } + + expect(response).to redirect_to(container_contest_description_contest_instance_path(container, contest_description, contest_instance)) + expect(flash[:alert]).to eq('Cannot send results for an incomplete judging round.') + end + end + + context 'with unauthorized user' do + let(:regular_user) { create(:user) } + let(:judging_round) { create(:judging_round, contest_instance: contest_instance, completed: true) } + + before do + sign_out user + sign_in regular_user + end + + it 'does not allow access to the action' do + post :send_round_results, params: { + container_id: container.id, + contest_description_id: contest_description.id, + id: contest_instance.id, + round_id: judging_round.id + } + + expect(response).to redirect_to(root_path) + expect(flash[:alert]).to match(/not authorized/i) + end + end + end +end diff --git a/spec/factories/containers.rb b/spec/factories/containers.rb index 8afb5917..855bc2f0 100644 --- a/spec/factories/containers.rb +++ b/spec/factories/containers.rb @@ -28,5 +28,6 @@ department visibility notes { "Notes for #{name}" } + contact_email { "contact@example.com" } end end diff --git a/spec/mailers/results_mailer_spec.rb b/spec/mailers/results_mailer_spec.rb new file mode 100644 index 00000000..24b973e3 --- /dev/null +++ b/spec/mailers/results_mailer_spec.rb @@ -0,0 +1,126 @@ +require 'rails_helper' + +RSpec.describe ResultsMailer, type: :mailer do + describe '#entry_evaluation_notification' do + # Create all required test data + let(:user) { create(:user, email: 'applicant@example.com', first_name: 'John', last_name: 'Applicant') } + let(:profile) { create(:profile, user: user) } + let(:department) { create(:department) } + let(:container) { create(:container, name: 'Test Container', department: department, contact_email: 'contact@example.com') } + let(:contest_description) { create(:contest_description, name: 'Test Contest', container: container) } + let(:contest_instance) { create(:contest_instance, contest_description: contest_description, date_open: 2.months.ago, date_closed: 1.month.ago) } + let(:category) { create(:category, kind: 'Poetry') } + let(:entry) { create(:entry, title: 'Test Entry', profile: profile, contest_instance: contest_instance, category: category, pen_name: 'Writer Pen') } + # Create a second entry for the second ranking + let(:entry2) { create(:entry, title: 'Second Entry', profile: profile, contest_instance: contest_instance, category: category, pen_name: 'Writer Pen') } + let(:judging_round) { create(:judging_round, contest_instance: contest_instance, round_number: 1, completed: true, + start_date: 3.weeks.ago, end_date: 2.weeks.ago) } + let(:judge) { create(:user, :with_judge_role) } + + # Create the judging assignment to link the judge to the contest + let!(:judging_assignment) { create(:judging_assignment, user: judge, contest_instance: contest_instance) } + # Assign judge to the round + let!(:round_judge_assignment) { create(:round_judge_assignment, user: judge, judging_round: judging_round) } + + # Create test rankings - using different entries + let!(:ranking1) { create(:entry_ranking, entry: entry, user: judge, judging_round: judging_round, rank: 1, external_comments: 'Great work!', selected_for_next_round: true) } + let!(:ranking2) { create(:entry_ranking, entry: entry2, user: judge, judging_round: judging_round, rank: 2, external_comments: 'Impressive submission.') } + + let(:mail) { described_class.entry_evaluation_notification(entry, judging_round) } + + it 'renders the headers' do + expect(mail.subject).to eq("Evaluation Results for \"Test Entry\" - Test Contest") + expect(mail.to).to eq([ 'applicant@example.com' ]) + expect(mail.from).to eq([ Rails.application.credentials.dig(:devise, :mailer_sender) || 'from@example.com' ]) + end + + it 'renders the entry details in the body' do + expect(mail.body.encoded).to include('Test Entry') + expect(mail.body.encoded).to include('Category: Poetry') + expect(mail.body.encoded).to include('Pen Name: Writer Pen') + end + + it 'includes the average ranking' do + # We'll use only ranking1 since it's the one for our entry + expect(mail.body.encoded).to include('average ranking of 1') + end + + it 'includes the external comments' do + expect(mail.body.encoded).to include('Great work!') + expect(mail.body.encoded).not_to include('Impressive submission.') # This is for entry2 + end + + it 'indicates the entry was selected for the next round' do + expect(mail.body.encoded).to include('Congratulations') + expect(mail.body.encoded).to include('has been selected') + end + + it 'includes the contact email' do + expect(mail.body.encoded).to include('contact@example.com') + end + + context 'when container has no contact email' do + # Use allow to bypass the validation rather than trying to create an invalid container + before do + # Mock the container's contact_email method to return nil or empty string + allow_any_instance_of(Container).to receive(:contact_email).and_return(nil) + end + + it 'falls back to the default contact email from credentials' do + default_email = Rails.application.credentials.dig(:mailer, :default_contact_email) + + if default_email + expect(mail.body.encoded).to include(default_email) + else + default_sender = Rails.application.credentials.dig(:devise, :mailer_sender) || 'contests@example.com' + expect(mail.body.encoded).to include(default_sender) + end + end + end + + context 'when entry was not selected for next round' do + # Use the same approach with the judge + before do + # Update the existing ranking to be not selected for next round + ranking1.update_column(:selected_for_next_round, false) + end + + it 'indicates the entry was not selected' do + expect(mail.body.encoded).to include('regret to inform you') + expect(mail.body.encoded).to include('not selected') + end + end + + context 'when there are no external comments' do + before do + # Update the existing ranking to have no external comments + ranking1.update_column(:external_comments, '') + end + + it 'does not include the feedback section' do + expect(mail.body.encoded).not_to include('Feedback from Judges') + end + end + + context 'when it is the final round' do + # Make sure the final round dates come after the first round end date + let(:final_round) { create(:judging_round, contest_instance: contest_instance, round_number: 2, completed: true, + start_date: 1.week.ago, end_date: 2.days.ago) } + + before do + # Assign the same judge to the final round + create(:round_judge_assignment, user: judge, judging_round: final_round) + # Create a ranking in the final round + create(:entry_ranking, entry: entry, user: judge, judging_round: final_round, rank: 1, selected_for_next_round: true) + # Allow the method call that checks for maximum round number + allow(contest_instance.judging_rounds).to receive(:maximum).with(:round_number).and_return(2) + # Use the final round for the mailer + @mail = described_class.entry_evaluation_notification(entry, final_round) + end + + it 'indicates the entry is a finalist' do + expect(@mail.body.encoded).to include('selected as a finalist') + end + end + end +end diff --git a/spec/models/container_spec.rb b/spec/models/container_spec.rb index 83aef2a5..005c55bd 100644 --- a/spec/models/container_spec.rb +++ b/spec/models/container_spec.rb @@ -101,7 +101,8 @@ name: 'Test Container', department:, visibility:, - creator: user + creator: user, + contact_email: 'test@example.com' ) end diff --git a/spec/models/judging_round_spec.rb b/spec/models/judging_round_spec.rb index 0bdef978..b92d4e91 100644 --- a/spec/models/judging_round_spec.rb +++ b/spec/models/judging_round_spec.rb @@ -208,4 +208,55 @@ end end end + + describe 'email tracking functionality' do + let(:contest_instance) { create(:contest_instance) } + let!(:first_round) do + create(:judging_round, + contest_instance: contest_instance, + round_number: 1, + start_date: contest_instance.date_closed + 1.day, + end_date: contest_instance.date_closed + 2.days) + end + + it 'defaults emails_sent_count to 0' do + expect(first_round.emails_sent_count).to eq(0) + end + + it 'increments emails_sent_count correctly' do + expect { + first_round.increment!(:emails_sent_count) + }.to change { first_round.reload.emails_sent_count }.from(0).to(1) + + expect { + first_round.increment!(:emails_sent_count) + }.to change { first_round.reload.emails_sent_count }.from(1).to(2) + end + + context 'with multiple rounds' do + let!(:second_round) do + create(:judging_round, + contest_instance: contest_instance, + round_number: 2, + start_date: first_round.end_date + 1.hour, + emails_sent_count: 0) + end + + before do + # Set first round to have some emails sent for testing + first_round.update!(emails_sent_count: 2) + end + + it 'tracks emails_sent_count independently for each round' do + expect(first_round.reload.emails_sent_count).to eq(2) + expect(second_round.emails_sent_count).to eq(0) + + second_round.increment!(:emails_sent_count) + expect(second_round.reload.emails_sent_count).to eq(1) + + # Ensure first round count remains unchanged + expect(first_round.reload.emails_sent_count).to eq(2) + end + end + end end diff --git a/spec/system/judging_results_email_spec.rb b/spec/system/judging_results_email_spec.rb new file mode 100644 index 00000000..fc9d45e9 --- /dev/null +++ b/spec/system/judging_results_email_spec.rb @@ -0,0 +1,123 @@ +require 'rails_helper' + +RSpec.describe 'Judging Results Email', type: :system do + let(:department) { create(:department, name: 'Test Department') } + let(:admin_user) { create(:user, :axis_mundi) } + let(:container) { create(:container, name: 'Test Container', department: department, contact_email: 'admin@example.com') } + let(:contest_description) { create(:contest_description, name: 'Test Contest', container: container) } + let(:contest_instance) { create(:contest_instance, contest_description: contest_description, date_open: 4.months.ago, date_closed: 3.months.ago) } + + # Create applicant user and entry + let(:applicant) { create(:user, email: 'applicant@example.com') } + let(:profile) { create(:profile, user: applicant) } + let(:entry) { create(:entry, title: 'Test Entry', profile: profile, contest_instance: contest_instance) } + + # Create judge and rankings + let(:judge) { + judge = create(:user, :with_judge_role) + create(:judging_assignment, user: judge, contest_instance: contest_instance, active: true) + judge + } + + # Create judging rounds + let(:judging_round) { create(:judging_round, + contest_instance: contest_instance, + round_number: 1, + start_date: 50.days.ago, + end_date: 30.days.ago, + completed: true) } + + let(:incomplete_round) { create(:judging_round, + contest_instance: contest_instance, + round_number: 2, + start_date: 20.days.ago, + end_date: 10.days.ago, + completed: false) } + + before do + # Create rankings + create(:entry_ranking, + entry: entry, + judging_round: judging_round, + user: judge, + rank: 2, + external_comments: 'Good submission!', + selected_for_next_round: true) + + create(:entry_ranking, + entry: entry, + judging_round: incomplete_round, + user: judge) + + # Assign the judge to the rounds + create(:round_judge_assignment, judging_round: judging_round, user: judge) + create(:round_judge_assignment, judging_round: incomplete_round, user: judge) + + # Set up email counter + judging_round.update(emails_sent_count: 1) + + # Sign in as admin + login_as(admin_user, scope: :user) + + # Mock the mailer to avoid actually sending emails in tests + allow(ResultsMailer).to receive(:entry_evaluation_notification).and_return(double(deliver_now: true, deliver_later: true)) + end + + it 'shows completed round email counter and enables/disables buttons correctly', :js do + # Visit the contest instance page + visit container_contest_description_contest_instance_path(container, contest_description, contest_instance) + + # Click the Judging Results tab + execute_script("document.getElementById('judging-results-tab').click()") + sleep 1 + + # Verify we can see both rounds + expect(page).to have_content('Round 1') + expect(page).to have_content('Round 2') + + # Get all buttons with email text + buttons = page.all('button', text: /Email round \d+ results/) + + # Check there are 2 buttons (one for each round) + expect(buttons.size).to eq(2) + + # First button should be for round 1 and enabled + expect(buttons[0].text).to include('Email round 1 results') + expect(buttons[0]['disabled']).to eq('false').or eq(false).or be_nil + + # Second button should be for round 2 and disabled + expect(buttons[1].text).to include('Email round 2 results') + expect(buttons[1]['disabled']).to eq('true').or eq(true) + + # Check that the badge is displayed for round 1 + expect(page).to have_content('Emails sent: 1 time') + end + + it 'sends emails and increments counter when button is clicked', :js do + # Visit the contest instance page + visit container_contest_description_contest_instance_path(container, contest_description, contest_instance) + + # Click the Judging Results tab + execute_script("document.getElementById('judging-results-tab').click()") + sleep 1 + + # Initial badge count + expect(page).to have_content('Emails sent: 1 time') + + # Click the email button for round 1 + accept_confirm do + click_button 'Email round 1 results' + end + + # Verify success message appears + expect(page).to have_content('Successfully queued 1 evaluation result emails') + + # Refresh the page to ensure we're seeing the latest data + visit current_path + execute_script("document.getElementById('judging-results-tab').click()") + sleep 1 + + # Verify counter increased + expect(page).to have_content('Emails sent: 2 times') + end +end From e75dea6fa4c854da3ec24b7f8f6d4ff710a3bd49 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Wed, 12 Mar 2025 11:06:58 -0400 Subject: [PATCH 19/59] Update environment check in ContestInstancesController for email notifications - Change the environment check from `development?` to `local?` in the email notification logic within the `send_round_results` action, ensuring proper email delivery behavior in local development settings. --- app/controllers/contest_instances_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/contest_instances_controller.rb b/app/controllers/contest_instances_controller.rb index d8475374..d4fef4db 100644 --- a/app/controllers/contest_instances_controller.rb +++ b/app/controllers/contest_instances_controller.rb @@ -104,7 +104,7 @@ def send_round_results # Send an email for each entry entries.each do |entry| - if Rails.env.development? + if Rails.env.local? ResultsMailer.entry_evaluation_notification(entry, judging_round).deliver_now else ResultsMailer.entry_evaluation_notification(entry, judging_round).deliver_later From 34d63d9914c75d949916ba02cf60404bc1c01ec9 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Wed, 12 Mar 2025 13:25:54 -0400 Subject: [PATCH 20/59] Enhance ResultsMailer to include judge information with feedback comments - Update ResultsMailer to structure external comments with associated judge names for clarity in email notifications. - Modify entry_evaluation_notification views to display judge names alongside their feedback in both HTML and text formats, improving the recipient's understanding of the feedback context. --- app/mailers/results_mailer.rb | 10 +++++++++- .../entry_evaluation_notification.html.erb | 12 +++++++++--- .../entry_evaluation_notification.text.erb | 7 ++++--- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/app/mailers/results_mailer.rb b/app/mailers/results_mailer.rb index ae174bde..797e0b7d 100644 --- a/app/mailers/results_mailer.rb +++ b/app/mailers/results_mailer.rb @@ -24,7 +24,15 @@ def entry_evaluation_notification(entry, round) @selected_for_next_round = @rankings.any?(&:selected_for_next_round?) # Only include external comments that are meant to be shared with applicants - @external_comments = @rankings.map(&:external_comments).compact.reject(&:empty?) + # and include the judge information with each comment + @external_comments_with_judges = @rankings.map do |ranking| + if ranking.external_comments.present? && !ranking.external_comments.empty? + { + comment: ranking.external_comments, + judge: ranking.user.display_name_or_first_name_last_name + } + end + end.compact subject = "Evaluation Results for \"#{@entry.title}\" - #{@contest_description.name}" diff --git a/app/views/mailers/results_mailer/entry_evaluation_notification.html.erb b/app/views/mailers/results_mailer/entry_evaluation_notification.html.erb index 31f2605b..ee251279 100644 --- a/app/views/mailers/results_mailer/entry_evaluation_notification.html.erb +++ b/app/views/mailers/results_mailer/entry_evaluation_notification.html.erb @@ -49,6 +49,11 @@ background-color: #f9f9f9; border-left: 3px solid #1a5a96; } + .judge-name { + color: #1a5a96; + margin-bottom: 5px; + font-size: 0.9em; + } .advance-notice { background-color: #d4edda; color: #155724; @@ -115,13 +120,14 @@
<% end %> - <% if @external_comments.any? %> + <% if @external_comments_with_judges.any? %>

Feedback from Judges

Our judges have provided the following feedback on your submission:

- <% @external_comments.each do |comment| %> + <% @external_comments_with_judges.each do |comment_data| %>
- <%= simple_format(comment) %> +

From Judge: <%= comment_data[:judge] %>

+ <%= simple_format(comment_data[:comment]) %>
<% end %> <% end %> diff --git a/app/views/mailers/results_mailer/entry_evaluation_notification.text.erb b/app/views/mailers/results_mailer/entry_evaluation_notification.text.erb index 6c9ad2cf..deae4ae3 100644 --- a/app/views/mailers/results_mailer/entry_evaluation_notification.text.erb +++ b/app/views/mailers/results_mailer/entry_evaluation_notification.text.erb @@ -29,13 +29,14 @@ We regret to inform you that your entry was not selected as a finalist. We regret to inform you that your entry was not selected to advance to the next round of judging. <% end %> -<% if @external_comments.any? %> +<% if @external_comments_with_judges.any? %> FEEDBACK FROM JUDGES ================= Our judges have provided the following feedback on your submission: -<% @external_comments.each do |comment| %> -* <%= comment.gsub(/\n/, "\n ") %> +<% @external_comments_with_judges.each do |comment_data| %> +* From Judge: <%= comment_data[:judge] %> + <%= comment_data[:comment].gsub(/\n/, "\n ") %> <% end %> <% end %> From ecbdfd2c1761ed7a832c7ea0ad844c2e15eb0201 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Wed, 12 Mar 2025 13:57:28 -0400 Subject: [PATCH 21/59] Add email preferences action to ContestInstancesController - Introduce email_preferences method to handle user preferences for email notifications related to judging rounds. - Implement checks for valid judging round and its completion status before proceeding with email preferences. - Update judging round attributes based on user input for average ranking and advancement status. - Ensure proper redirection with alert messages for invalid scenarios. --- .../contest_instances_controller.rb | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/app/controllers/contest_instances_controller.rb b/app/controllers/contest_instances_controller.rb index d4fef4db..7ada60d4 100644 --- a/app/controllers/contest_instances_controller.rb +++ b/app/controllers/contest_instances_controller.rb @@ -78,7 +78,26 @@ def destroy end end - # POST /containers/:container_id/contest_descriptions/:contest_description_id/contest_instances/:id/send_round_results + def email_preferences + set_contest_instance + authorize @contest_instance, :send_round_results? + + round_id = params[:round_id] + @judging_round = @contest_instance.judging_rounds.find_by(id: round_id) + + if @judging_round.nil? + redirect_to container_contest_description_contest_instance_path(@container, @contest_description, @contest_instance), + alert: 'Judging round not found.' + return + end + + if !@judging_round.completed? + redirect_to container_contest_description_contest_instance_path(@container, @contest_description, @contest_instance), + alert: 'Cannot send results for an incomplete judging round.' + nil + end + end + def send_round_results authorize @contest_instance, :send_round_results? @@ -97,6 +116,14 @@ def send_round_results return end + # Update email preferences if they were provided + if params[:include_average_ranking].present? || params[:include_advancement_status].present? + judging_round.update( + include_average_ranking: params[:include_average_ranking] == '1', + include_advancement_status: params[:include_advancement_status] == '1' + ) + end + # Get all entries for this round entries = judging_round.entries.uniq From c849f6532ba2acf0fa72b92fe3367795fe512c4a Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Wed, 12 Mar 2025 13:57:38 -0400 Subject: [PATCH 22/59] Add advancement status and average ranking attributes to JudgingRound model - Introduce new boolean attributes `include_advancement_status` and `include_average_ranking` to the JudgingRound model, defaulting to false. - These attributes will facilitate user preferences for judging round configurations related to advancement and ranking calculations. --- app/models/judging_round.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/judging_round.rb b/app/models/judging_round.rb index 1cd1d65a..e7c74c29 100644 --- a/app/models/judging_round.rb +++ b/app/models/judging_round.rb @@ -7,6 +7,8 @@ # completed :boolean default(FALSE), not null # emails_sent_count :integer default(0), not null # end_date :datetime +# include_advancement_status :boolean default(FALSE) +# include_average_ranking :boolean default(FALSE) # min_external_comment_words :integer default(0), not null # min_internal_comment_words :integer default(0), not null # require_external_comments :boolean default(FALSE), not null From 5f2fc17bf847067007912e8bbb1da849be4eff88 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Wed, 12 Mar 2025 13:58:14 -0400 Subject: [PATCH 23/59] Refactor judging results view to use link_to for email preferences - Replace button_to with link_to for the email preferences action in the judging results partial. - Remove unnecessary method and data attributes, simplifying the button's functionality while maintaining the disabled state based on round completion. --- app/views/contest_instances/_judging_results.html.erb | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/views/contest_instances/_judging_results.html.erb b/app/views/contest_instances/_judging_results.html.erb index fb2e7378..28331c5d 100644 --- a/app/views/contest_instances/_judging_results.html.erb +++ b/app/views/contest_instances/_judging_results.html.erb @@ -9,19 +9,14 @@ data-bs-placement="top" data-bs-html="true" title="Email results to applicants for round <%= round.round_number %>
Contact: <%= h(@container.contact_email.presence || 'Not set') %>"> - <%= button_to send_round_results_container_contest_description_contest_instance_path( + <%= link_to email_preferences_container_contest_description_contest_instance_path( @container, @contest_description, @contest_instance, round_id: round.id ), - method: :post, class: "btn btn-sm btn-info me-3", - disabled: !round.complete?, - data: { - controller: "confirm", - confirm_message_value: "Are you sure you want to send evaluation results to all applicants for round #{round.round_number}?" - } do %> + disabled: !round.complete? do %> Email round <%= round.round_number %> results <% end %> From c82e3f54fc11ac08c66e0d48c3fdf1a845122454 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Wed, 12 Mar 2025 13:58:33 -0400 Subject: [PATCH 24/59] Add email preferences view for judging round results - Create a new view for email preferences in contest instances, allowing users to select options for including average ranking and advancement status in evaluation result emails. - Implement form with checkboxes for user preferences and a confirmation dialog for sending emails, enhancing user experience and control over email content. --- .../email_preferences.html.erb | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 app/views/contest_instances/email_preferences.html.erb diff --git a/app/views/contest_instances/email_preferences.html.erb b/app/views/contest_instances/email_preferences.html.erb new file mode 100644 index 00000000..4faed82a --- /dev/null +++ b/app/views/contest_instances/email_preferences.html.erb @@ -0,0 +1,49 @@ +

Email Results Preferences

+ +
+
+

Round <%= @judging_round.round_number %> Email Content Options

+
+
+

+ Select which information to include in the evaluation result emails for this round: +

+ + <%= form_with url: send_round_results_container_contest_description_contest_instance_path( + @container, + @contest_description, + @contest_instance, + round_id: @judging_round.id + ), + method: :post, + class: "email-preferences-form", + data: { + controller: "confirm", + confirm_message_value: "Are you sure you want to send evaluation results to all applicants for round #{@judging_round.round_number}?" + } do |f| %> + +
+ <%= f.check_box :include_average_ranking, + id: "include_average_ranking", + class: "form-check-input", + checked: @judging_round.include_average_ranking %> + <%= f.label :include_average_ranking, "Include average ranking information", class: "form-check-label" %> +
When checked, emails will include the average ranking each entry received from judges.
+
+ +
+ <%= f.check_box :include_advancement_status, + id: "include_advancement_status", + class: "form-check-input", + checked: @judging_round.include_advancement_status %> + <%= f.label :include_advancement_status, "Include advancement status information", class: "form-check-label" %> +
When checked, emails will include whether the entry was selected to advance to the next round or was selected as a finalist.
+
+ +
+ <%= f.submit "Send Emails", class: "btn btn-primary me-2" %> + <%= link_to "Cancel", container_contest_description_contest_instance_path(@container, @contest_description, @contest_instance), class: "btn btn-secondary" %> +
+ <% end %> +
+
From 94b0915e3002929780a4cca50e0b8a0370132daa Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Wed, 12 Mar 2025 13:59:07 -0400 Subject: [PATCH 25/59] Update entry evaluation notification view to incorporate user preferences for average ranking and advancement status - Modify the entry_evaluation_notification view to conditionally display average ranking and advancement status based on the new attributes `include_average_ranking` and `include_advancement_status` from the JudgingRound model. - Enhance messaging for finalists and non-finalists to provide clearer communication regarding advancement in the judging process. --- .../entry_evaluation_notification.html.erb | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/app/views/mailers/results_mailer/entry_evaluation_notification.html.erb b/app/views/mailers/results_mailer/entry_evaluation_notification.html.erb index ee251279..ea7009c6 100644 --- a/app/views/mailers/results_mailer/entry_evaluation_notification.html.erb +++ b/app/views/mailers/results_mailer/entry_evaluation_notification.html.erb @@ -96,28 +96,30 @@

Evaluation Results - Round <%= @round.round_number %>

- <% if @avg_rank.present? %> + <% if @avg_rank.present? && @round.include_average_ranking %>

Your submission received an average ranking of <%= @avg_rank %> from our judging panel.

<% end %> - <% if @selected_for_next_round && @contest_instance.judging_rounds.exists?(round_number: @round.round_number + 1) %> -
-

Congratulations! Your entry has been selected to advance to Round <%= @round.round_number + 1 %>.

-
- <% elsif @round.round_number == @contest_instance.judging_rounds.maximum(:round_number) %> - <% if @selected_for_next_round %> + <% if @round.include_advancement_status %> + <% if @selected_for_next_round && @contest_instance.judging_rounds.exists?(round_number: @round.round_number + 1) %>
-

Congratulations! Your entry has been selected as a finalist.

+

Congratulations! Your entry has been selected to advance to Round <%= @round.round_number + 1 %>.

- <% else %> + <% elsif @round.round_number == @contest_instance.judging_rounds.maximum(:round_number) %> + <% if @selected_for_next_round %> +
+

Congratulations! Your entry has been selected as a finalist.

+
+ <% else %> +
+

We regret to inform you that your entry was not selected as a finalist.

+
+ <% end %> + <% elsif !@selected_for_next_round %>
-

We regret to inform you that your entry was not selected as a finalist.

+

We regret to inform you that your entry was not selected to advance to the next round of judging.

<% end %> - <% elsif !@selected_for_next_round %> -
-

We regret to inform you that your entry was not selected to advance to the next round of judging.

-
<% end %> <% if @external_comments_with_judges.any? %> From c1dae84048e923b51e91eefae2e61a1d2f6a52b4 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Wed, 12 Mar 2025 13:59:14 -0400 Subject: [PATCH 26/59] Update entry evaluation notification view to conditionally display advancement status - Modify the entry_evaluation_notification view to include conditional rendering for advancement status based on the `include_advancement_status` attribute from the JudgingRound model. - Ensure that the messaging for selected and non-selected entries is displayed only when the user preferences allow it, enhancing clarity in communication regarding entry outcomes. --- .../results_mailer/entry_evaluation_notification.text.erb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/mailers/results_mailer/entry_evaluation_notification.text.erb b/app/views/mailers/results_mailer/entry_evaluation_notification.text.erb index deae4ae3..1f585ed2 100644 --- a/app/views/mailers/results_mailer/entry_evaluation_notification.text.erb +++ b/app/views/mailers/results_mailer/entry_evaluation_notification.text.erb @@ -13,10 +13,11 @@ Submitted: <%= @entry.created_at.strftime("%B %d, %Y") %> EVALUATION RESULTS - ROUND <%= @round.round_number %> ================= -<% if @avg_rank.present? %> +<% if @avg_rank.present? && @round.include_average_ranking %> Your submission received an average ranking of <%= @avg_rank %> from our judging panel. <% end %> +<% if @round.include_advancement_status %> <% if @selected_for_next_round && @contest_instance.judging_rounds.exists?(round_number: @round.round_number + 1) %> CONGRATULATIONS! Your entry has been selected to advance to Round <%= @round.round_number + 1 %>. <% elsif @round.round_number == @contest_instance.judging_rounds.maximum(:round_number) %> @@ -28,6 +29,7 @@ We regret to inform you that your entry was not selected as a finalist. <% elsif !@selected_for_next_round %> We regret to inform you that your entry was not selected to advance to the next round of judging. <% end %> +<% end %> <% if @external_comments_with_judges.any? %> FEEDBACK FROM JUDGES From 893d0fab65b0b0cc3f45df09602d6a7af4409a62 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Wed, 12 Mar 2025 13:59:24 -0400 Subject: [PATCH 27/59] Add email preferences route for contest instances - Introduce a new route for the email_preferences action within contest instances, allowing users to access their email notification settings related to judging rounds. - This addition enhances the routing structure, facilitating better user interaction with email preferences in the application. --- config/routes.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/routes.rb b/config/routes.rb index 0533a2b2..ba99d598 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -35,6 +35,7 @@ resources :contest_descriptions do resources :contest_instances do member do + get 'email_preferences' post 'send_round_results' end resources :judging_rounds do From cb4e73887ebf3726ce915d694364d75021d328de Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Wed, 12 Mar 2025 13:59:34 -0400 Subject: [PATCH 28/59] Add email preferences columns to JudgingRounds table - Introduce two new boolean columns, `include_average_ranking` and `include_advancement_status`, to the JudgingRounds table via a migration. - These columns will allow users to specify their preferences for including average ranking and advancement status in evaluation result emails, enhancing user control over email content. --- ...0250312173602_add_email_preferences_to_judging_rounds.rb | 6 ++++++ db/schema.rb | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20250312173602_add_email_preferences_to_judging_rounds.rb diff --git a/db/migrate/20250312173602_add_email_preferences_to_judging_rounds.rb b/db/migrate/20250312173602_add_email_preferences_to_judging_rounds.rb new file mode 100644 index 00000000..a5562bda --- /dev/null +++ b/db/migrate/20250312173602_add_email_preferences_to_judging_rounds.rb @@ -0,0 +1,6 @@ +class AddEmailPreferencesToJudgingRounds < ActiveRecord::Migration[7.2] + def change + add_column :judging_rounds, :include_average_ranking, :boolean, default: false + add_column :judging_rounds, :include_advancement_status, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index aecd7dfa..64c25b4d 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.2].define(version: 2025_03_11_193603) do +ActiveRecord::Schema[7.2].define(version: 2025_03_12_173602) do create_table "action_text_rich_texts", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.string "name", null: false t.text "body", size: :long @@ -271,6 +271,8 @@ t.text "special_instructions" t.integer "required_entries_count", default: 0, null: false t.integer "emails_sent_count", default: 0, null: false + t.boolean "include_average_ranking", default: false + t.boolean "include_advancement_status", default: false t.index ["contest_instance_id", "round_number"], name: "index_judging_rounds_on_contest_instance_id_and_round_number", unique: true t.index ["contest_instance_id"], name: "index_judging_rounds_on_contest_instance_id" end From 65623bed5873c45e347c763e873a72e52299fd60 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Wed, 12 Mar 2025 13:59:47 -0400 Subject: [PATCH 29/59] Add tests for email preferences functionality in ContestInstancesController - Implement RSpec tests for the email_preferences action, ensuring proper rendering of the email preferences template and correct assignment of the judging round. - Add tests for handling non-existent and incomplete judging rounds, verifying appropriate redirection and alert messages. - Enhance the send_round_results action tests to check for updates to email preferences based on user input, ensuring preferences are preserved when not provided. - Introduce system tests for the email preferences form, confirming the presence of checkboxes and the correct display of email content options based on user selections. --- .../contest_instances_controller_spec.rb | 101 ++++++++++++++++++ spec/factories/judging_rounds.rb | 2 + spec/mailers/results_mailer_spec.rb | 71 +++++++++++- spec/models/judging_round_spec.rb | 2 + spec/system/email_preferences_spec.rb | 67 ++++++++++++ spec/system/judging_results_email_spec.rb | 35 +++--- 6 files changed, 262 insertions(+), 16 deletions(-) create mode 100644 spec/system/email_preferences_spec.rb diff --git a/spec/controllers/contest_instances_controller_spec.rb b/spec/controllers/contest_instances_controller_spec.rb index 185577e7..afaf97f6 100644 --- a/spec/controllers/contest_instances_controller_spec.rb +++ b/spec/controllers/contest_instances_controller_spec.rb @@ -3,6 +3,71 @@ RSpec.describe ContestInstancesController, type: :controller do # Add existing specs if there are any... + describe 'GET #email_preferences' do + let(:department) { create(:department) } + let(:user) { create(:user, :axis_mundi) } # Admin user with full privileges + let(:container) { create(:container, department: department) } + let(:contest_description) { create(:contest_description, container: container) } + let(:contest_instance) { create(:contest_instance, contest_description: contest_description) } + let(:judging_round) { create(:judging_round, contest_instance: contest_instance, completed: true) } + + before do + sign_in user + end + + it 'renders the email_preferences template' do + get :email_preferences, params: { + container_id: container.id, + contest_description_id: contest_description.id, + id: contest_instance.id, + round_id: judging_round.id + } + + expect(response).to render_template(:email_preferences) + end + + it 'assigns the judging round' do + get :email_preferences, params: { + container_id: container.id, + contest_description_id: contest_description.id, + id: contest_instance.id, + round_id: judging_round.id + } + + expect(assigns(:judging_round)).to eq(judging_round) + end + + context 'with non-existent judging round' do + it 'redirects with an alert' do + get :email_preferences, params: { + container_id: container.id, + contest_description_id: contest_description.id, + id: contest_instance.id, + round_id: 9999 # Non-existent round + } + + expect(response).to redirect_to(container_contest_description_contest_instance_path(container, contest_description, contest_instance)) + expect(flash[:alert]).to eq('Judging round not found.') + end + end + + context 'with incomplete judging round' do + let(:incomplete_round) { create(:judging_round, contest_instance: contest_instance, completed: false) } + + it 'redirects with an alert' do + get :email_preferences, params: { + container_id: container.id, + contest_description_id: contest_description.id, + id: contest_instance.id, + round_id: incomplete_round.id + } + + expect(response).to redirect_to(container_contest_description_contest_instance_path(container, contest_description, contest_instance)) + expect(flash[:alert]).to eq('Cannot send results for an incomplete judging round.') + end + end + end + describe 'POST #send_round_results' do let(:department) { create(:department) } let(:user) { create(:user, :axis_mundi) } # Admin user with full privileges @@ -69,6 +134,42 @@ expect(flash[:notice]).to include("Successfully queued 2 evaluation result emails") expect(flash[:notice]).to include("email batch #1") end + + it 'updates email preferences when provided' do + # Initially both preferences should be true (default) + judging_round.update(include_average_ranking: true, include_advancement_status: true) + + post :send_round_results, params: { + container_id: container.id, + contest_description_id: contest_description.id, + id: contest_instance.id, + round_id: judging_round.id, + include_average_ranking: "0", + include_advancement_status: "0" + } + + # After the request, both should be false + judging_round.reload + expect(judging_round.include_average_ranking).to be false + expect(judging_round.include_advancement_status).to be false + end + + it 'preserves email preferences when not provided' do + # Set initial values + judging_round.update(include_average_ranking: false, include_advancement_status: false) + + post :send_round_results, params: { + container_id: container.id, + contest_description_id: contest_description.id, + id: contest_instance.id, + round_id: judging_round.id + } + + # Values should remain unchanged + judging_round.reload + expect(judging_round.include_average_ranking).to be false + expect(judging_round.include_advancement_status).to be false + end end context 'with non-existent judging round' do diff --git a/spec/factories/judging_rounds.rb b/spec/factories/judging_rounds.rb index 4cdfcbef..37c8ad9c 100644 --- a/spec/factories/judging_rounds.rb +++ b/spec/factories/judging_rounds.rb @@ -7,6 +7,8 @@ # completed :boolean default(FALSE), not null # emails_sent_count :integer default(0), not null # end_date :datetime +# include_advancement_status :boolean default(FALSE) +# include_average_ranking :boolean default(FALSE) # min_external_comment_words :integer default(0), not null # min_internal_comment_words :integer default(0), not null # require_external_comments :boolean default(FALSE), not null diff --git a/spec/mailers/results_mailer_spec.rb b/spec/mailers/results_mailer_spec.rb index 24b973e3..505e5b6b 100644 --- a/spec/mailers/results_mailer_spec.rb +++ b/spec/mailers/results_mailer_spec.rb @@ -14,7 +14,8 @@ # Create a second entry for the second ranking let(:entry2) { create(:entry, title: 'Second Entry', profile: profile, contest_instance: contest_instance, category: category, pen_name: 'Writer Pen') } let(:judging_round) { create(:judging_round, contest_instance: contest_instance, round_number: 1, completed: true, - start_date: 3.weeks.ago, end_date: 2.weeks.ago) } + start_date: 3.weeks.ago, end_date: 2.weeks.ago, + include_average_ranking: true, include_advancement_status: true) } let(:judge) { create(:user, :with_judge_role) } # Create the judging assignment to link the judge to the contest @@ -83,6 +84,8 @@ before do # Update the existing ranking to be not selected for next round ranking1.update_column(:selected_for_next_round, false) + # Make sure preferences are enabled + judging_round.update(include_average_ranking: true, include_advancement_status: true) end it 'indicates the entry was not selected' do @@ -105,7 +108,8 @@ context 'when it is the final round' do # Make sure the final round dates come after the first round end date let(:final_round) { create(:judging_round, contest_instance: contest_instance, round_number: 2, completed: true, - start_date: 1.week.ago, end_date: 2.days.ago) } + start_date: 1.week.ago, end_date: 2.days.ago, + include_average_ranking: true, include_advancement_status: true) } before do # Assign the same judge to the final round @@ -122,5 +126,68 @@ expect(@mail.body.encoded).to include('selected as a finalist') end end + + context 'with email preferences' do + context 'when include_average_ranking is false' do + before do + judging_round.update(include_average_ranking: false) + end + + it 'does not include the average ranking' do + expect(mail.body.encoded).not_to include('average ranking of') + end + end + + context 'when include_average_ranking is true' do + before do + judging_round.update(include_average_ranking: true) + end + + it 'includes the average ranking' do + expect(mail.body.encoded).to include('average ranking of') + end + end + + context 'when include_advancement_status is false' do + before do + judging_round.update(include_advancement_status: false) + end + + it 'does not include advancement status' do + expect(mail.body.encoded).not_to include('has been selected to advance') + expect(mail.body.encoded).not_to include('selected as a finalist') + expect(mail.body.encoded).not_to include('not selected to advance') + end + end + + context 'when include_advancement_status is true' do + before do + judging_round.update(include_advancement_status: true) + end + + it 'includes advancement status' do + # Since our test entry is selected for next round, it should include this message + expect(mail.body.encoded).to include('has been selected') + end + end + + context 'when both preferences are false' do + before do + judging_round.update(include_average_ranking: false, include_advancement_status: false) + end + + it 'excludes both sections' do + expect(mail.body.encoded).not_to include('average ranking of') + expect(mail.body.encoded).not_to include('has been selected') + expect(mail.body.encoded).not_to include('not selected') + end + + it 'still includes other content' do + expect(mail.body.encoded).to include('Test Entry') + expect(mail.body.encoded).to include('Category: Poetry') + expect(mail.body.encoded).to include('Great work!') + end + end + end end end diff --git a/spec/models/judging_round_spec.rb b/spec/models/judging_round_spec.rb index b92d4e91..4085e232 100644 --- a/spec/models/judging_round_spec.rb +++ b/spec/models/judging_round_spec.rb @@ -7,6 +7,8 @@ # completed :boolean default(FALSE), not null # emails_sent_count :integer default(0), not null # end_date :datetime +# include_advancement_status :boolean default(FALSE) +# include_average_ranking :boolean default(FALSE) # min_external_comment_words :integer default(0), not null # min_internal_comment_words :integer default(0), not null # require_external_comments :boolean default(FALSE), not null diff --git a/spec/system/email_preferences_spec.rb b/spec/system/email_preferences_spec.rb new file mode 100644 index 00000000..9621dcca --- /dev/null +++ b/spec/system/email_preferences_spec.rb @@ -0,0 +1,67 @@ +require 'rails_helper' + +RSpec.describe 'Email Preferences', type: :system do + let(:department) { create(:department, name: 'Test Department') } + let(:admin_user) { create(:user, :axis_mundi) } + let(:container) { create(:container, name: 'Test Container', department: department, contact_email: 'admin@example.com') } + let(:contest_description) { create(:contest_description, name: 'Test Contest', container: container) } + let(:contest_instance) { create(:contest_instance, contest_description: contest_description, date_open: 4.months.ago, date_closed: 3.months.ago) } + + # Create applicant user and entry + let(:applicant) { create(:user, email: 'applicant@example.com') } + let(:profile) { create(:profile, user: applicant) } + let(:entry) { create(:entry, title: 'Test Entry', profile: profile, contest_instance: contest_instance) } + + # Create judge and rankings + let(:judge) { + judge = create(:user, :with_judge_role) + create(:judging_assignment, user: judge, contest_instance: contest_instance, active: true) + judge + } + + # Create judging rounds + let(:judging_round) { create(:judging_round, + contest_instance: contest_instance, + round_number: 1, + start_date: 50.days.ago, + end_date: 30.days.ago, + completed: true) } + + before do + # Create rankings + create(:entry_ranking, + entry: entry, + judging_round: judging_round, + user: judge, + rank: 2, + external_comments: 'Good submission!', + selected_for_next_round: true) + + # Assign the judge to the round + create(:round_judge_assignment, judging_round: judging_round, user: judge) + + # Sign in as admin + login_as(admin_user, scope: :user) + + # Mock the mailer to avoid actually sending emails in tests + allow(ResultsMailer).to receive(:entry_evaluation_notification).and_return(double(deliver_now: true, deliver_later: true)) + end + + it 'displays the email preferences form', :js do + # Visit the email preferences page directly + visit email_preferences_container_contest_description_contest_instance_path( + container, + contest_description, + contest_instance, + round_id: judging_round.id + ) + + # Verify we're on the email preferences page + expect(page).to have_content('Email Results Preferences') + expect(page).to have_content('Round 1 Email Content Options') + + # Check that both checkboxes exist + expect(page).to have_field('include_average_ranking') + expect(page).to have_field('include_advancement_status') + end +end diff --git a/spec/system/judging_results_email_spec.rb b/spec/system/judging_results_email_spec.rb index fc9d45e9..aabd4f9e 100644 --- a/spec/system/judging_results_email_spec.rb +++ b/spec/system/judging_results_email_spec.rb @@ -63,7 +63,7 @@ allow(ResultsMailer).to receive(:entry_evaluation_notification).and_return(double(deliver_now: true, deliver_later: true)) end - it 'shows completed round email counter and enables/disables buttons correctly', :js do + it 'shows completed round email counter and enables/disables links correctly', :js do # Visit the contest instance page visit container_contest_description_contest_instance_path(container, contest_description, contest_instance) @@ -75,25 +75,25 @@ expect(page).to have_content('Round 1') expect(page).to have_content('Round 2') - # Get all buttons with email text - buttons = page.all('button', text: /Email round \d+ results/) + # Get all links with email text + links = page.all('a', text: /Email round \d+ results/) - # Check there are 2 buttons (one for each round) - expect(buttons.size).to eq(2) + # Check there are 2 links (one for each round) + expect(links.size).to eq(2) - # First button should be for round 1 and enabled - expect(buttons[0].text).to include('Email round 1 results') - expect(buttons[0]['disabled']).to eq('false').or eq(false).or be_nil + # First link should be for round 1 and enabled + expect(links[0].text).to include('Email round 1 results') + expect(links[0]['disabled']).to be_nil - # Second button should be for round 2 and disabled - expect(buttons[1].text).to include('Email round 2 results') - expect(buttons[1]['disabled']).to eq('true').or eq(true) + # Second link should be for round 2 and disabled + expect(links[1].text).to include('Email round 2 results') + expect(links[1]['disabled']).to eq('disabled') # Check that the badge is displayed for round 1 expect(page).to have_content('Emails sent: 1 time') end - it 'sends emails and increments counter when button is clicked', :js do + it 'navigates to email preferences and sends emails', :js do # Visit the contest instance page visit container_contest_description_contest_instance_path(container, contest_description, contest_instance) @@ -104,9 +104,16 @@ # Initial badge count expect(page).to have_content('Emails sent: 1 time') - # Click the email button for round 1 + # Click the email link for round 1 + click_link 'Email round 1 results' + + # Verify we're on the email preferences page + expect(page).to have_content('Email Results Preferences') + expect(page).to have_content('Round 1 Email Content Options') + + # Submit the form with default preferences accept_confirm do - click_button 'Email round 1 results' + click_button 'Send Emails' end # Verify success message appears From 1a736079c9691fdeb2166b79581d99fb86e296af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 19:44:14 +0000 Subject: [PATCH 30/59] Bump omniauth-saml in the bundler group across 1 directory Bumps the bundler group with 1 update in the / directory: [omniauth-saml](https://github.com/omniauth/omniauth-saml). Updates `omniauth-saml` from 2.1.2 to 2.1.3 - [Release notes](https://github.com/omniauth/omniauth-saml/releases) - [Changelog](https://github.com/omniauth/omniauth-saml/blob/master/CHANGELOG.md) - [Commits](https://github.com/omniauth/omniauth-saml/compare/v2.1.2...v2.1.3) --- updated-dependencies: - dependency-name: omniauth-saml dependency-type: direct:production dependency-group: bundler ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 135bdc3a..a97383cb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -272,16 +272,16 @@ GEM racc (~> 1.4) nokogiri (1.18.3-x86_64-linux-gnu) racc (~> 1.4) - omniauth (2.1.2) + omniauth (2.1.3) hashie (>= 3.4.6) rack (>= 2.2.3) rack-protection omniauth-rails_csrf_protection (1.0.2) actionpack (>= 4.2) omniauth (~> 2.0) - omniauth-saml (2.1.2) + omniauth-saml (2.1.3) omniauth (~> 2.1) - ruby-saml (~> 1.17) + ruby-saml (~> 1.18) orm_adapter (0.5.0) os (1.1.4) pagy (6.5.0) @@ -313,8 +313,9 @@ GEM rack (3.1.12) rack-accept (0.4.5) rack (>= 0.4) - rack-protection (4.0.0) + rack-protection (4.1.1) base64 (>= 0.1.0) + logger (>= 1.6.0) rack (>= 3.0.0, < 4) rack-session (2.0.0) rack (>= 3.0.0) @@ -375,7 +376,7 @@ GEM actionpack (>= 5.2) railties (>= 5.2) retriable (3.1.2) - rexml (3.3.9) + rexml (3.4.1) rouge (4.3.0) rspec-core (3.13.1) rspec-support (~> 3.13.0) @@ -432,7 +433,7 @@ GEM rubocop (~> 1.61) rubocop-rspec (~> 3, >= 3.0.1) ruby-progressbar (1.13.0) - ruby-saml (1.17.0) + ruby-saml (1.18.0) nokogiri (>= 1.13.10) rexml ruby-vips (2.2.2) From 0eb92eafa3cd10e6f58f8d1402ea191689f03e69 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 13 Mar 2025 09:56:58 -0400 Subject: [PATCH 31/59] Update staging environment configuration - Adjust log_tags formatting for consistency. - Change Active Job queue adapter to :async and set queue name prefix to 'lsa_evaluate_staging', improving job processing in the staging environment. --- config/environments/staging.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/environments/staging.rb b/config/environments/staging.rb index 342017ee..a87fb299 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -36,7 +36,7 @@ # Logging config.log_level = :info - config.log_tags = [:request_id] + config.log_tags = [ :request_id ] if ENV['RAILS_LOG_TO_STDOUT'].present? logger = ActiveSupport::Logger.new($stdout) @@ -65,8 +65,8 @@ config.active_support.report_deprecations = false # Active Job - # config.active_job.queue_adapter = :resque - # config.active_job.queue_name_prefix = "lsa_evaluate_staging" + config.active_job.queue_adapter = :async + config.active_job.queue_name_prefix = 'lsa_evaluate_staging' # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false From c3d7973b2471f72563a25dbf3c959ecdfa118b08 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 13 Mar 2025 11:19:33 -0400 Subject: [PATCH 32/59] Add Sidekiq integration to Capistrano configuration - Require the Capistrano::Sidekiq plugin and install it for managing Sidekiq processes. - Add Capistrano::Sidekiq::Systemd plugin to facilitate systemd service management for Sidekiq, enhancing deployment capabilities. --- Capfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Capfile b/Capfile index 2e5151cb..ca7a5777 100644 --- a/Capfile +++ b/Capfile @@ -30,6 +30,9 @@ install_plugin Capistrano::SCM::Git require 'capistrano/rails' require 'capistrano/bundler' require 'capistrano/asdf' +require 'capistrano/sidekiq' +install_plugin Capistrano::Sidekiq +install_plugin Capistrano::Sidekiq::Systemd # Load custom tasks from `lib/capistrano/tasks` if you have any defined Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r } From 7882bbd7122f57b3a0175d24132fe3a4b97a9514 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 13 Mar 2025 11:19:50 -0400 Subject: [PATCH 33/59] Add Sidekiq and Capistrano integration for background job management - Include the Sidekiq gem for background processing capabilities. - Add Capistrano-Sidekiq gem to facilitate deployment and management of Sidekiq processes, enhancing the deployment workflow. --- Gemfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Gemfile b/Gemfile index a9b55378..87383cdb 100644 --- a/Gemfile +++ b/Gemfile @@ -22,6 +22,7 @@ gem 'pagy', '~> 6.4' gem 'puma' gem 'pundit' gem 'redis', '~> 5.0' +gem 'sidekiq', '~> 7.2' gem 'sassc-rails' gem 'simple_form', '~> 5.3' gem 'stimulus-rails' @@ -37,6 +38,7 @@ group :development do gem 'capistrano', '~> 3.17', require: false gem 'capistrano-rails', '~> 1.6', '>= 1.6.1', require: false gem 'capistrano-asdf', require: false + gem 'capistrano-sidekiq', '~> 2.0', require: false gem 'web-console' end From 691918c02e9d62a42cf435265052fba5ee0d0155 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 13 Mar 2025 11:20:01 -0400 Subject: [PATCH 34/59] Add Procfile for Puma and Sidekiq process management - Create a new Procfile to define the web server (Puma) and background worker (Sidekiq) processes. - This setup enhances the application's deployment and background job processing capabilities. --- Procfile | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Procfile diff --git a/Procfile b/Procfile new file mode 100644 index 00000000..6d19a6cf --- /dev/null +++ b/Procfile @@ -0,0 +1,2 @@ +web: bundle exec puma -C config/puma.rb +worker: bundle exec sidekiq -e ${RAILS_ENV:-development} -C config/sidekiq.yml From 5ab9cfc260b49c0454327b4b850d36ae26d2f9c1 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 13 Mar 2025 11:20:12 -0400 Subject: [PATCH 35/59] Refactor email notification delivery in ContestInstancesController - Simplify the email delivery logic in the `send_round_results` method by removing the environment check for local delivery. - All entries will now use `deliver_later` for sending evaluation notifications, ensuring consistent background processing across environments. --- app/controllers/contest_instances_controller.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/controllers/contest_instances_controller.rb b/app/controllers/contest_instances_controller.rb index 7ada60d4..4233ba06 100644 --- a/app/controllers/contest_instances_controller.rb +++ b/app/controllers/contest_instances_controller.rb @@ -131,11 +131,7 @@ def send_round_results # Send an email for each entry entries.each do |entry| - if Rails.env.local? - ResultsMailer.entry_evaluation_notification(entry, judging_round).deliver_now - else - ResultsMailer.entry_evaluation_notification(entry, judging_round).deliver_later - end + ResultsMailer.entry_evaluation_notification(entry, judging_round).deliver_later email_count += 1 end From b69c4df949edd587005934ca213f7c3ac5485cac Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 13 Mar 2025 11:20:24 -0400 Subject: [PATCH 36/59] Add Sidekiq Web UI route for authenticated users - Introduce a new route for the Sidekiq Web UI, accessible only to users with the `axis_mundi?` role. - This addition enhances monitoring and management of background jobs within the application. --- config/routes.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/routes.rb b/config/routes.rb index ba99d598..d466808c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,6 +6,12 @@ get 'bulk_contest_instances/create' root 'static_pages#home' + # Sidekiq Web UI + require 'sidekiq/web' + authenticate :user, ->(user) { user.axis_mundi? } do + mount Sidekiq::Web => '/sidekiq' + end + resources :entries do member do patch 'soft_delete' From 8eb2f27da111ec91f648f33b2ba237309f6bb25c Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 13 Mar 2025 11:20:40 -0400 Subject: [PATCH 37/59] Add Sidekiq configuration file for background job processing - Create a new `sidekiq.yml` file to define concurrency and queue settings for different environments (development, test, staging, production). - This configuration enhances the management of background jobs by specifying queue priorities and retry limits, optimizing job processing across environments. --- config/sidekiq.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 config/sidekiq.yml diff --git a/config/sidekiq.yml b/config/sidekiq.yml new file mode 100644 index 00000000..16e9dec2 --- /dev/null +++ b/config/sidekiq.yml @@ -0,0 +1,25 @@ +--- +:concurrency: 5 +:max_retries: 3 + +# Sidekiq will run this file through ERB when loading it +:queues: + - [critical, 3] + - [default, 2] + - [<%= ENV['RAILS_ENV'] || 'development' %>, 2] + - [<%= ENV['RAILS_ENV'] || 'development' %>_mailers, 2] + - [low, 1] + +development: + :concurrency: 5 + +test: + :concurrency: 1 + +staging: + :concurrency: 10 + :max_retries: 5 + +production: + :concurrency: 20 + :max_retries: 10 From a45f5dc8c381c65098cfe1313b1d61f80a7d3868 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 13 Mar 2025 11:21:02 -0400 Subject: [PATCH 38/59] Add Sidekiq systemd service template for deployment - Create a new `sidekiq.service.erb` file to define the systemd service configuration for Sidekiq. - This template specifies the service's execution parameters, environment settings, and logging, enhancing the deployment and management of Sidekiq processes. --- config/deploy/templates/sidekiq.service.erb | 22 +++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 config/deploy/templates/sidekiq.service.erb diff --git a/config/deploy/templates/sidekiq.service.erb b/config/deploy/templates/sidekiq.service.erb new file mode 100644 index 00000000..cacc0102 --- /dev/null +++ b/config/deploy/templates/sidekiq.service.erb @@ -0,0 +1,22 @@ +[Unit] +Description=sidekiq +After=syslog.target network.target + +[Service] +Type=simple +WorkingDirectory=<%= current_path %> +ExecStart=/bin/bash -lc 'bundle exec sidekiq -e <%= fetch(:rails_env) %> -C <%= current_path %>/config/sidekiq.yml' +User=<%= fetch(:user) %> +Group=<%= fetch(:user) %> +UMask=0002 +RestartSec=1 +Restart=on-failure +StandardOutput=append:<%= shared_path %>/log/sidekiq.log +StandardError=append:<%= shared_path %>/log/sidekiq.error.log + +# Greatly reduce Ruby memory fragmentation and heap usage +# https://www.mikeperham.com/2018/04/25/taming-rails-memory-bloat/ +Environment=MALLOC_ARENA_MAX=2 + +[Install] +WantedBy=multi-user.target From f658a161de743d48049e8f9ee3ea781216a638ba Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 13 Mar 2025 11:21:12 -0400 Subject: [PATCH 39/59] Update production environment configuration for Sidekiq - Change Active Job queue adapter to Sidekiq and set queue name prefix to 'lsa_evaluate_production'. - This update enhances background job processing in the production environment, aligning with recent Sidekiq integrations. --- config/environments/production.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index 59a33eba..1b617f2f 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -64,8 +64,8 @@ # config.cache_store = :mem_cache_store # Use a real queuing backend for Active Job (and separate queues per environment). - # config.active_job.queue_adapter = :resque - # config.active_job.queue_name_prefix = "lsa_evaluate_production" + config.active_job.queue_adapter = :sidekiq + config.active_job.queue_name_prefix = 'lsa_evaluate_production' config.action_mailer.perform_caching = false From 395594daa3f3acfccb95c120f2b97487e629104e Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 13 Mar 2025 11:21:26 -0400 Subject: [PATCH 40/59] Update staging environment configuration for Sidekiq - Change Active Job queue adapter to Sidekiq, aligning with the production environment setup. - This update improves background job processing in the staging environment, ensuring consistency across environments. --- config/environments/staging.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/environments/staging.rb b/config/environments/staging.rb index a87fb299..c484747b 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -65,7 +65,7 @@ config.active_support.report_deprecations = false # Active Job - config.active_job.queue_adapter = :async + config.active_job.queue_adapter = :sidekiq config.active_job.queue_name_prefix = 'lsa_evaluate_staging' # Do not dump schema after migrations. From 723bd8c5fd4dca41b31277bf81cc21b4bdfa812b Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 13 Mar 2025 11:21:38 -0400 Subject: [PATCH 41/59] Add Sidekiq initializer for background job configuration - Create a new initializer file for Sidekiq to configure Redis connection and set Sidekiq as the ActiveJob queue adapter. - This setup enhances background job processing by establishing a consistent configuration for Sidekiq across all environments. --- config/initializers/sidekiq.rb | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 config/initializers/sidekiq.rb diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb new file mode 100644 index 00000000..98105075 --- /dev/null +++ b/config/initializers/sidekiq.rb @@ -0,0 +1,23 @@ +require 'sidekiq' + +# Configure Redis connection +redis_config = { + url: ENV.fetch('REDIS_URL') { 'redis://localhost:6379/1' }, + ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE } # Only if using Redis over SSL +} + +Sidekiq.configure_server do |config| + config.redis = redis_config + + # Add middleware for retries with exponential backoff + config.server_middleware do |chain| + chain.add Sidekiq::Middleware::Server::RetryJobs, max_retries: 5 + end +end + +Sidekiq.configure_client do |config| + config.redis = redis_config +end + +# Set Sidekiq as the ActiveJob queue adapter +Rails.application.config.active_job.queue_adapter = :sidekiq From 7b8d66b2612f52a295772dd1877fd038c9ca1f55 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 13 Mar 2025 11:21:49 -0400 Subject: [PATCH 42/59] Add Redis tasks for Capistrano deployment - Introduce a new Rake task namespace for Redis, including tasks to check for Redis installation and install Redis if not present. - Hook the Redis check into the deployment flow to ensure Redis is available before proceeding with deployment. - This addition enhances the deployment process by automating Redis setup and verification. --- lib/capistrano/tasks/redis.rake | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 lib/capistrano/tasks/redis.rake diff --git a/lib/capistrano/tasks/redis.rake b/lib/capistrano/tasks/redis.rake new file mode 100644 index 00000000..76f6d15e --- /dev/null +++ b/lib/capistrano/tasks/redis.rake @@ -0,0 +1,24 @@ +namespace :redis do + desc 'Check if Redis is installed' + task :check do + on roles(:app) do + unless test('which redis-server') + error 'Redis is not installed. Please install Redis before deploying.' + exit 1 + end + end + end + + desc 'Install Redis' + task :install do + on roles(:app) do + execute :sudo, 'apt-get update' + execute :sudo, 'apt-get install -y redis-server' + execute :sudo, 'systemctl enable redis-server' + execute :sudo, 'systemctl start redis-server' + end + end +end + +# Hook into the deployment flow +before 'deploy:check', 'redis:check' From 0838db7aa1f39dcc463f91c4855ed97361501daa Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 13 Mar 2025 11:28:09 -0400 Subject: [PATCH 43/59] Update Gemfile.lock to include Capistrano-Sidekiq and Sidekiq gems - Add `capistrano-sidekiq` gem for improved integration of Sidekiq with Capistrano deployment. - Update `sidekiq` gem to version 7.3.9, ensuring compatibility with the latest features and fixes. - This update enhances background job management and deployment processes within the application. --- Gemfile.lock | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Gemfile.lock b/Gemfile.lock index a97383cb..68a1473e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -108,6 +108,10 @@ GEM capistrano-rails (1.6.3) capistrano (~> 3.1) capistrano-bundler (>= 1.1, < 3) + capistrano-sidekiq (2.3.1) + capistrano (>= 3.9.0) + capistrano-bundler + sidekiq (>= 6.0) capybara (3.40.0) addressable matrix @@ -453,6 +457,12 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) + sidekiq (7.3.9) + base64 + connection_pool (>= 2.3.0) + logger + rack (>= 2.2.4) + redis-client (>= 0.22.2) signet (0.19.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) @@ -537,6 +547,7 @@ DEPENDENCIES capistrano (~> 3.17) capistrano-asdf capistrano-rails (~> 1.6, >= 1.6.1) + capistrano-sidekiq (~> 2.0) capybara country_select cssbundling-rails @@ -572,6 +583,7 @@ DEPENDENCIES rubocop-rspec_rails sassc-rails selenium-webdriver + sidekiq (~> 7.2) simple_form (~> 5.3) simplecov skylight From 54cefdf0eb82c63c74b9db4760c660d888c5964f Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 13 Mar 2025 11:43:14 -0400 Subject: [PATCH 44/59] Update Sidekiq configuration to use default_worker_options for retries - Replace :max_retries with :default_worker_options in sidekiq.yml to align with Sidekiq 7.x standards. - Set retry options for development, test, staging, and production environments, ensuring consistent background job behavior across all environments. - This change enhances the configuration by utilizing the latest Sidekiq features for managing job retries. --- config/sidekiq.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 16e9dec2..991730a9 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -1,6 +1,8 @@ --- :concurrency: 5 -:max_retries: 3 +# In Sidekiq 7.x, max_retries is set via default_worker_options +:default_worker_options: + retry: 3 # Sidekiq will run this file through ERB when loading it :queues: @@ -12,14 +14,20 @@ development: :concurrency: 5 + :default_worker_options: + retry: 3 test: :concurrency: 1 + :default_worker_options: + retry: 3 staging: :concurrency: 10 - :max_retries: 5 + :default_worker_options: + retry: 5 production: :concurrency: 20 - :max_retries: 10 + :default_worker_options: + retry: 10 From 34856728ec89f6b6fe4d88abbe5c65aede682d62 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 13 Mar 2025 11:43:34 -0400 Subject: [PATCH 45/59] Update Sidekiq initializer to align with version 7.x retry configuration - Replace the deprecated max_retries middleware with default_worker_options for setting global retry limits. - Provide comments for potential custom retry logic using middleware, enhancing clarity for future modifications. - This change ensures compatibility with Sidekiq 7.x and simplifies the configuration for background job retries. --- config/initializers/sidekiq.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 98105075..76835d80 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -9,10 +9,14 @@ Sidekiq.configure_server do |config| config.redis = redis_config - # Add middleware for retries with exponential backoff - config.server_middleware do |chain| - chain.add Sidekiq::Middleware::Server::RetryJobs, max_retries: 5 - end + # In Sidekiq 7.x, RetryJobs is built-in and configured differently + # The max_retries setting can be set globally + config.default_worker_options = { retry: 5 } + + # If you need custom retry logic, you can use a custom middleware + # config.server_middleware do |chain| + # chain.add YourCustomMiddleware + # end end Sidekiq.configure_client do |config| From 909635664336269f8896c5eede8b9006bf36072b Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 13 Mar 2025 11:47:05 -0400 Subject: [PATCH 46/59] Update Sidekiq and related dependencies in Gemfile and Gemfile.lock - Upgrade Sidekiq gem to version 7.3 for improved performance and features. - Update connection_pool gem to version 2.5.0 and logger gem to version 1.6.6 for compatibility and enhancements. - Upgrade redis-client gem to version 0.24.0 to ensure optimal Redis integration. --- Gemfile | 2 +- Gemfile.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 87383cdb..7831d5e1 100644 --- a/Gemfile +++ b/Gemfile @@ -22,7 +22,7 @@ gem 'pagy', '~> 6.4' gem 'puma' gem 'pundit' gem 'redis', '~> 5.0' -gem 'sidekiq', '~> 7.2' +gem 'sidekiq', '~> 7.3' gem 'sassc-rails' gem 'simple_form', '~> 5.3' gem 'stimulus-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 68a1473e..84a0acfc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -125,7 +125,7 @@ GEM logger (~> 1.5) coderay (1.1.3) concurrent-ruby (1.3.4) - connection_pool (2.4.1) + connection_pool (2.5.0) countries (6.0.1) unaccent (~> 0.3) country_select (9.0.0) @@ -236,7 +236,7 @@ GEM letter_opener (~> 1.9) railties (>= 6.1) rexml - logger (1.6.2) + logger (1.6.6) loofah (2.23.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -367,7 +367,7 @@ GEM psych (>= 4.0.0) redis (5.3.0) redis-client (>= 0.22.0) - redis-client (0.22.2) + redis-client (0.24.0) connection_pool regexp_parser (2.9.2) reline (0.5.9) @@ -583,7 +583,7 @@ DEPENDENCIES rubocop-rspec_rails sassc-rails selenium-webdriver - sidekiq (~> 7.2) + sidekiq (~> 7.3) simple_form (~> 5.3) simplecov skylight From 5d6d2b286b91868c53a8c44f07e4ef2d655d2d9d Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 13 Mar 2025 11:54:54 -0400 Subject: [PATCH 47/59] Refactor Sidekiq configuration to use global retry settings - Update sidekiq.yml to replace deprecated default_worker_options with global :retry settings for all environments. - This change aligns with Sidekiq 7.3.x standards, simplifying the configuration for job retries and ensuring consistent behavior across development, test, staging, and production environments. --- config/sidekiq.yml | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 991730a9..6b437196 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -1,8 +1,7 @@ --- :concurrency: 5 -# In Sidekiq 7.x, max_retries is set via default_worker_options -:default_worker_options: - retry: 3 +# In Sidekiq 7.3.x, we set retry at the global level +:retry: 3 # Sidekiq will run this file through ERB when loading it :queues: @@ -14,20 +13,16 @@ development: :concurrency: 5 - :default_worker_options: - retry: 3 + :retry: 3 test: :concurrency: 1 - :default_worker_options: - retry: 3 + :retry: 3 staging: :concurrency: 10 - :default_worker_options: - retry: 5 + :retry: 5 production: :concurrency: 20 - :default_worker_options: - retry: 10 + :retry: 10 From 834aa99890eb8e603614aec6b5d89d9389b71634 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 13 Mar 2025 11:55:07 -0400 Subject: [PATCH 48/59] Update Sidekiq initializer to use options hash for retry configuration - Modify the Sidekiq initializer to set the global retry option directly using the options hash, aligning with Sidekiq 7.3.x standards. - Remove deprecated default_worker_options comments for clarity and maintainability. - This change simplifies the configuration for job retries, ensuring consistent behavior across all environments. --- config/initializers/sidekiq.rb | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 76835d80..a3f8c410 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -9,14 +9,8 @@ Sidekiq.configure_server do |config| config.redis = redis_config - # In Sidekiq 7.x, RetryJobs is built-in and configured differently - # The max_retries setting can be set globally - config.default_worker_options = { retry: 5 } - - # If you need custom retry logic, you can use a custom middleware - # config.server_middleware do |chain| - # chain.add YourCustomMiddleware - # end + # In Sidekiq 7.3.x, we need to use the options hash directly + Sidekiq.options[:retry] = 5 end Sidekiq.configure_client do |config| From ed96e37a9e413abefc8bd866f04054d1adeab708 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 13 Mar 2025 12:01:27 -0400 Subject: [PATCH 49/59] Refactor Sidekiq configuration for improved clarity and maintainability - Update sidekiq.yml to include default configuration comments and environment-specific settings for concurrency and retry options. - Modify sidekiq.rb to clarify that the retry option is now set in the sidekiq.yml file, enhancing the overall configuration structure. - These changes ensure a more organized and understandable Sidekiq setup across different environments. --- config/initializers/sidekiq.rb | 4 ++-- config/sidekiq.yml | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index a3f8c410..92618c14 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -9,8 +9,8 @@ Sidekiq.configure_server do |config| config.redis = redis_config - # In Sidekiq 7.3.x, we need to use the options hash directly - Sidekiq.options[:retry] = 5 + # Set default retry count for Sidekiq 7.3.x + # The retry option is now set in the sidekiq.yml file end Sidekiq.configure_client do |config| diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 6b437196..94cec5d8 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -1,6 +1,6 @@ --- +# Default configuration :concurrency: 5 -# In Sidekiq 7.3.x, we set retry at the global level :retry: 3 # Sidekiq will run this file through ERB when loading it @@ -11,13 +11,12 @@ - [<%= ENV['RAILS_ENV'] || 'development' %>_mailers, 2] - [low, 1] +# Environment-specific configurations development: :concurrency: 5 - :retry: 3 test: :concurrency: 1 - :retry: 3 staging: :concurrency: 10 From 84f103e5cb83fcf09b21e6dc21b88cd10e8d3604 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 13 Mar 2025 12:34:45 -0400 Subject: [PATCH 50/59] Fix Action Mailer default URL options in staging environment configuration - Update the default_url_options for Action Mailer to correctly reference the host variable, ensuring proper email delivery in the staging environment. - This change enhances clarity and correctness in the configuration, aligning with Rails conventions. --- config/environments/staging.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/environments/staging.rb b/config/environments/staging.rb index c484747b..b86d6913 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -52,7 +52,7 @@ # Action Mailer host = 'https://evaluate-staging.lsa.umich.edu/' - config.action_mailer.default_url_options = { host: } + config.action_mailer.default_url_options = { host: host } config.action_mailer.raise_delivery_errors = true config.action_mailer.perform_caching = false config.action_mailer.delivery_method = :letter_opener_web From 37ee75c31d1cfc5a358ca8887d9c5ae534d268e9 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 13 Mar 2025 13:15:43 -0400 Subject: [PATCH 51/59] Add additional Sidekiq queues for LSA evaluation in configuration - Update sidekiq.yml to include new queues for lsa_evaluate in both default and mailers categories, ensuring proper job processing for the LSA evaluation tasks. - This change enhances the Sidekiq configuration by accommodating specific job requirements for the application environment. --- config/sidekiq.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 94cec5d8..3c3ec642 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -9,6 +9,8 @@ - [default, 2] - [<%= ENV['RAILS_ENV'] || 'development' %>, 2] - [<%= ENV['RAILS_ENV'] || 'development' %>_mailers, 2] + - [lsa_evaluate_<%= ENV['RAILS_ENV'] || 'development' %>_default, 2] + - [lsa_evaluate_<%= ENV['RAILS_ENV'] || 'development' %>_mailers, 2] - [low, 1] # Environment-specific configurations From e7fe151a28d3d327e20d6b2a5ee16d1d19b16493 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Mar 2025 23:55:09 +0000 Subject: [PATCH 52/59] Bump nokogiri in the bundler group across 1 directory Bumps the bundler group with 1 update in the / directory: [nokogiri](https://github.com/sparklemotion/nokogiri). Updates `nokogiri` from 1.18.3 to 1.18.4 - [Release notes](https://github.com/sparklemotion/nokogiri/releases) - [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md) - [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.18.3...v1.18.4) --- updated-dependencies: - dependency-name: nokogiri dependency-type: indirect dependency-group: bundler ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 84a0acfc..8a76c759 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -272,9 +272,9 @@ GEM net-protocol net-ssh (7.2.3) nio4r (2.7.3) - nokogiri (1.18.3-arm64-darwin) + nokogiri (1.18.4-arm64-darwin) racc (~> 1.4) - nokogiri (1.18.3-x86_64-linux-gnu) + nokogiri (1.18.4-x86_64-linux-gnu) racc (~> 1.4) omniauth (2.1.3) hashie (>= 3.4.6) From f1d7ec0cc8fb3eb5323050ddcb0dc829c1a02383 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 25 Mar 2025 12:49:52 -0400 Subject: [PATCH 53/59] Update Rails credentials for enhanced security and configuration - Modify the encrypted credentials file to update sensitive information, ensuring secure access to application secrets. - This change improves the overall security posture of the application by keeping sensitive data up-to-date and properly managed. --- config/credentials.yml.enc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc index 438dca9a..cc6a9a1c 100644 --- a/config/credentials.yml.enc +++ b/config/credentials.yml.enc @@ -1 +1 @@ -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--XlVLPw52t5lI4c+b--nvp3Tp+JVxREaTOqbHZwFA== \ No newline at end of file +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--3Uy3rk2jkSTh1gUy--Kzt4qhpqDPoxBJSdMj39MA== \ No newline at end of file From 9359529485f405f1ecc000cff6a783fa045e1d9b Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 25 Mar 2025 12:50:11 -0400 Subject: [PATCH 54/59] Add SendGrid email configuration instructions to README - Include detailed steps for setting up SendGrid for email delivery in the production environment. - Document the necessary environment variables and the requirement for Sidekiq to process emails asynchronously. - This addition enhances the README by providing clear guidance for email configuration, improving the onboarding experience for new developers. --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 822d02bf..1320ce5c 100644 --- a/README.md +++ b/README.md @@ -47,4 +47,20 @@ In its initial implementation, LSA Evaluate will serve the University of Michiga rails server ``` +## Email Configuration with SendGrid + +This application uses SendGrid for email delivery in the production environment. Follow these steps to set it up: + +1. Create a SendGrid account if you don't have one already +2. Generate an API key in the SendGrid dashboard +3. Set the following environment variables in your production environment: + ``` + SENDGRID_USERNAME=apikey + SENDGRID_API_KEY=your_sendgrid_api_key_here + DOMAIN_NAME=yourdomain.com + ``` +4. Ensure Sidekiq is set up and running to process emails asynchronously + +Emails are automatically configured to be sent asynchronously through Sidekiq background jobs. + ## This project is licensed under the [MIT License](https://github.com/your-repo/lsa-evaluate/blob/main/LICENSE) From c9eb4cc2ea8aefd1305eea1187c6eb19637ba879 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 25 Mar 2025 12:50:28 -0400 Subject: [PATCH 55/59] Update RuboCop configuration for improved linting rules - Add RSpec/MultipleMemoizedHelpers rule to limit the number of memoized helpers to 12, promoting better test organization and readability. - This change enhances the code quality checks by enforcing stricter guidelines for RSpec tests, ensuring maintainable and efficient test code. --- .rubocop.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 80214a19..69ca5324 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -23,7 +23,7 @@ Rails/UnknownEnv: - test - production - staging - + # String Literals Style Style/StringLiterals: EnforcedStyle: single_quotes @@ -51,4 +51,7 @@ Capybara: # RSpec Rails settings RSpecRails: - Enabled: true \ No newline at end of file + Enabled: true + +RSpec/MultipleMemoizedHelpers: + Max: 12 From 2229b1d001fce942e8089c588c59572175842ad3 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 25 Mar 2025 12:51:31 -0400 Subject: [PATCH 56/59] Add SendGrid SMTP configuration for email delivery in production - Implement SendGrid settings in production.rb to enable email delivery via SMTP. - Configure necessary SMTP settings including user credentials, domain, and port. - Set default URL options for Action Mailer to ensure proper link generation in emails. - This change enhances email functionality and aligns with best practices for production environments. --- config/environments/production.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/config/environments/production.rb b/config/environments/production.rb index 1b617f2f..031d905e 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -73,6 +73,22 @@ # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false + # SendGrid configuration for email delivery + config.action_mailer.delivery_method = :smtp + config.action_mailer.perform_deliveries = true + config.action_mailer.smtp_settings = { + user_name: 'apikey', + password: Rails.application.credentials.sendgrid[:apikey], + domain: evaluate.lsa.umich.edu, + address: 'smtp.sendgrid.net', + port: 587, + authentication: :plain, + enable_starttls_auto: true + } + + # Set the host for URL generation in emails + config.action_mailer.default_url_options = { host: 'evaluate.lsa.umich.edu', protocol: 'https' } + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true From 7644063de673cf710ddc1e651cf5e66cd2a397c4 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 25 Mar 2025 12:55:40 -0400 Subject: [PATCH 57/59] Add ActionMailer configuration for asynchronous email delivery with Sidekiq - Introduce a new initializer for ActionMailer to enable asynchronous email delivery using Sidekiq. - Implement delivery logging and notification callbacks to enhance email tracking. - This change improves email handling efficiency and aligns with best practices for background job processing. --- config/initializers/action_mailer.rb | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 config/initializers/action_mailer.rb diff --git a/config/initializers/action_mailer.rb b/config/initializers/action_mailer.rb new file mode 100644 index 00000000..67a821d4 --- /dev/null +++ b/config/initializers/action_mailer.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# Configure ActionMailer to deliver emails asynchronously using Sidekiq +Rails.application.config.action_mailer.deliver_later_queue_name = :default + +# Extend ActionMailer to use mail delivery logging +ActionMailer::Base.class_eval do + # Add delivery tracking method + def notify_delivered + if self.class.delivery_notification_method + send(self.class.delivery_notification_method, message) + end + end + + # Hook into the delivery process + self.register_observer(-> (message) { message.delivery_method.settings[:return_response] = true }) + self.register_interceptor(-> (message) { message }) + self.register_observer(-> (message) { + message.deliver_later unless Rails.env.test? + }) + + class << self + attr_accessor :delivery_notification_method + + # Method to set notification callback + def notify_on_delivery(method_name) + self.delivery_notification_method = method_name + end + end +end From ba1d2482a01024ee577fab55f4e693cfeb828fc1 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Wed, 26 Mar 2025 09:53:49 -0400 Subject: [PATCH 58/59] Update production environment configuration to disable HSTS header - Add `ssl_options` to `production.rb` to disable the HSTS header while maintaining SSL enforcement. - This change allows for HSTS management via the nginx configuration, enhancing flexibility in server settings. --- config/environments/production.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/environments/production.rb b/config/environments/production.rb index 031d905e..8c76959b 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -49,6 +49,7 @@ # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = true + config.ssl_options = { hsts: false } # This disables the HSTS header while still forcing SSL. The header is set in the nginx config on the server. # Ensure the session cookies are also set to secure in production config.session_store :cookie_store, key: 'evaluate_session', secure: Rails.env.production? From 090d570d941a156db70fe63a1c6ad427ce3086e3 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Wed, 26 Mar 2025 10:07:14 -0400 Subject: [PATCH 59/59] Add TestMailer and corresponding view for email testing - Introduce TestMailer class with a method to send a test email. - Create HTML view for the test email, including a subject and timestamp. - This addition facilitates email authentication verification and testing within the application. --- app/mailers/test_mailer.rb | 8 ++++++++ app/views/test_mailer/test_email.html.erb | 11 +++++++++++ 2 files changed, 19 insertions(+) create mode 100644 app/mailers/test_mailer.rb create mode 100644 app/views/test_mailer/test_email.html.erb diff --git a/app/mailers/test_mailer.rb b/app/mailers/test_mailer.rb new file mode 100644 index 00000000..7c471b73 --- /dev/null +++ b/app/mailers/test_mailer.rb @@ -0,0 +1,8 @@ +class TestMailer < ApplicationMailer + def test_email + mail( + to: 'test-t68vvtnfc@srv1.mail-tester.com', + subject: 'Test Email from LSA Evaluate' + ) + end +end diff --git a/app/views/test_mailer/test_email.html.erb b/app/views/test_mailer/test_email.html.erb new file mode 100644 index 00000000..ed843f9d --- /dev/null +++ b/app/views/test_mailer/test_email.html.erb @@ -0,0 +1,11 @@ + + + + + + +

Test Email from LSA Evaluate

+

This is a test email to verify email authentication settings.

+

Sent at: <%= Time.current %>

+ +