Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
7b2aa71
Bump the bundler group across 1 directory with 2 updates
dependabot[bot] Mar 10, 2025
13bf95c
Merge pull request #105 from lsa-mis/dependabot/bundler/bundler-54337…
rsmoke Mar 11, 2025
1c2fc2e
Simplify HTML structure in judging results partial view
rsmoke Mar 11, 2025
e4ad254
Refactor manage judges partial for improved readability and structure
rsmoke Mar 11, 2025
de65996
Add contact email validation to Container model
rsmoke Mar 11, 2025
a8c75b8
Add contact email field to Container form
rsmoke Mar 11, 2025
c969cf3
Add tracking columns for emails and contact information
rsmoke Mar 11, 2025
878046d
Configure inline Active Job adapter for development email processing
rsmoke Mar 11, 2025
5a608cb
Add ResultsMailer for entry evaluation notifications
rsmoke Mar 11, 2025
9ff0837
Add complete? method to JudgingRound for view compatibility
rsmoke Mar 11, 2025
8a74459
Add send_round_results route for ContestInstance
rsmoke Mar 11, 2025
87ac346
Implement send_round_results action for ContestInstances
rsmoke Mar 11, 2025
5b0646e
Enhance judging results view with improved styling and email tracking
rsmoke Mar 11, 2025
ce1bcb8
Add authorization method for sending round results in ContestInstance…
rsmoke Mar 11, 2025
45d3eaa
Add emails_sent_count and contact_email to model specs and factories
rsmoke Mar 11, 2025
c946734
Refactor judging results view to use Stimulus confirm controller
rsmoke Mar 11, 2025
d6b03fe
Update authorization check in ContestInstancesController for sending …
rsmoke Mar 12, 2025
2034d2b
Configure test environment for SSL and ActiveJob settings
rsmoke Mar 12, 2025
b6ddd2c
Add tests for ContestInstancesController and ResultsMailer
rsmoke Mar 12, 2025
e75dea6
Update environment check in ContestInstancesController for email noti…
rsmoke Mar 12, 2025
34d63d9
Enhance ResultsMailer to include judge information with feedback comm…
rsmoke Mar 12, 2025
ecbdfd2
Add email preferences action to ContestInstancesController
rsmoke Mar 12, 2025
c849f65
Add advancement status and average ranking attributes to JudgingRound…
rsmoke Mar 12, 2025
5f2fc17
Refactor judging results view to use link_to for email preferences
rsmoke Mar 12, 2025
c82e3f5
Add email preferences view for judging round results
rsmoke Mar 12, 2025
94b0915
Update entry evaluation notification view to incorporate user prefere…
rsmoke Mar 12, 2025
c1dae84
Update entry evaluation notification view to conditionally display ad…
rsmoke Mar 12, 2025
893d0fa
Add email preferences route for contest instances
rsmoke Mar 12, 2025
cb4e738
Add email preferences columns to JudgingRounds table
rsmoke Mar 12, 2025
65623be
Add tests for email preferences functionality in ContestInstancesCont…
rsmoke Mar 12, 2025
1a73607
Bump omniauth-saml in the bundler group across 1 directory
dependabot[bot] Mar 12, 2025
8eb369d
Merge pull request #107 from lsa-mis/reports_take_2
rsmoke Mar 12, 2025
b61fe6f
Merge pull request #106 from lsa-mis/dependabot/bundler/bundler-f2960…
rsmoke Mar 12, 2025
0eb92ea
Update staging environment configuration
rsmoke Mar 13, 2025
c3d7973
Add Sidekiq integration to Capistrano configuration
rsmoke Mar 13, 2025
7882bbd
Add Sidekiq and Capistrano integration for background job management
rsmoke Mar 13, 2025
691918c
Add Procfile for Puma and Sidekiq process management
rsmoke Mar 13, 2025
5ab9cfc
Refactor email notification delivery in ContestInstancesController
rsmoke Mar 13, 2025
b69c4df
Add Sidekiq Web UI route for authenticated users
rsmoke Mar 13, 2025
8eb2f27
Add Sidekiq configuration file for background job processing
rsmoke Mar 13, 2025
a45f5dc
Add Sidekiq systemd service template for deployment
rsmoke Mar 13, 2025
f658a16
Update production environment configuration for Sidekiq
rsmoke Mar 13, 2025
395594d
Update staging environment configuration for Sidekiq
rsmoke Mar 13, 2025
723bd8c
Add Sidekiq initializer for background job configuration
rsmoke Mar 13, 2025
7b8d66b
Add Redis tasks for Capistrano deployment
rsmoke Mar 13, 2025
0838db7
Update Gemfile.lock to include Capistrano-Sidekiq and Sidekiq gems
rsmoke Mar 13, 2025
54cefdf
Update Sidekiq configuration to use default_worker_options for retries
rsmoke Mar 13, 2025
3485672
Update Sidekiq initializer to align with version 7.x retry configuration
rsmoke Mar 13, 2025
9096356
Update Sidekiq and related dependencies in Gemfile and Gemfile.lock
rsmoke Mar 13, 2025
5d6d2b2
Refactor Sidekiq configuration to use global retry settings
rsmoke Mar 13, 2025
834aa99
Update Sidekiq initializer to use options hash for retry configuration
rsmoke Mar 13, 2025
ed96e37
Refactor Sidekiq configuration for improved clarity and maintainability
rsmoke Mar 13, 2025
84f103e
Fix Action Mailer default URL options in staging environment configur…
rsmoke Mar 13, 2025
37ee75c
Add additional Sidekiq queues for LSA evaluation in configuration
rsmoke Mar 13, 2025
e7fe151
Bump nokogiri in the bundler group across 1 directory
dependabot[bot] Mar 21, 2025
f1d7ec0
Update Rails credentials for enhanced security and configuration
rsmoke Mar 25, 2025
9359529
Add SendGrid email configuration instructions to README
rsmoke Mar 25, 2025
c9eb4cc
Update RuboCop configuration for improved linting rules
rsmoke Mar 25, 2025
2229b1d
Add SendGrid SMTP configuration for email delivery in production
rsmoke Mar 25, 2025
7644063
Add ActionMailer configuration for asynchronous email delivery with S…
rsmoke Mar 25, 2025
4f70a3e
Merge pull request #108 from lsa-mis/dependabot/bundler/bundler-bee64…
rsmoke Mar 25, 2025
ba1d248
Update production environment configuration to disable HSTS header
rsmoke Mar 26, 2025
090d570
Add TestMailer and corresponding view for email testing
rsmoke Mar 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Rails/UnknownEnv:
- test
- production
- staging

# String Literals Style
Style/StringLiterals:
EnforcedStyle: single_quotes
Expand Down Expand Up @@ -51,4 +51,7 @@ Capybara:

# RSpec Rails settings
RSpecRails:
Enabled: true
Enabled: true

RSpec/MultipleMemoizedHelpers:
Max: 12
3 changes: 3 additions & 0 deletions Capfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ gem 'pagy', '~> 6.4'
gem 'puma'
gem 'pundit'
gem 'redis', '~> 5.0'
gem 'sidekiq', '~> 7.3'
gem 'sassc-rails'
gem 'simple_form', '~> 5.3'
gem 'stimulus-rails'
Expand All @@ -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

Expand Down
39 changes: 26 additions & 13 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -121,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)
Expand Down Expand Up @@ -232,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)
Expand Down Expand Up @@ -268,20 +272,20 @@ 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.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)
Expand Down Expand Up @@ -310,11 +314,12 @@ 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)
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)
Expand Down Expand Up @@ -362,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)
Expand All @@ -375,7 +380,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)
Expand Down Expand Up @@ -432,7 +437,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)
Expand All @@ -452,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)
Expand Down Expand Up @@ -501,7 +512,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)
Expand Down Expand Up @@ -536,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
Expand Down Expand Up @@ -571,6 +583,7 @@ DEPENDENCIES
rubocop-rspec_rails
sassc-rails
selenium-webdriver
sidekiq (~> 7.3)
simple_form (~> 5.3)
simplecov
skylight
Expand Down
2 changes: 2 additions & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
web: bundle exec puma -C config/puma.rb
worker: bundle exec sidekiq -e ${RAILS_ENV:-development} -C config/sidekiq.yml
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion app/controllers/containers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
66 changes: 65 additions & 1 deletion app/controllers/contest_instances_controller.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -78,6 +78,70 @@ def destroy
end
end

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?

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

# 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

email_count = 0

# Send an email for each entry
entries.each do |entry|
ResultsMailer.entry_evaluation_notification(entry, judging_round).deliver_later
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
Expand Down
44 changes: 44 additions & 0 deletions app/mailers/results_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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
# 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}"

mail(
to: @user.email,
subject: subject
)
end
end
8 changes: 8 additions & 0 deletions app/mailers/test_mailer.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions app/models/container.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions app/models/judging_round.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
# 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
# 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
Expand Down Expand Up @@ -48,6 +51,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?

Expand Down
6 changes: 5 additions & 1 deletion app/policies/contest_instance_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading