Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f82004a
Remove default value for archived attribute in BulkContestInstancesCo…
rsmoke May 14, 2025
4a4d82b
Update contest_description_params to remove archived attribute, ensur…
rsmoke May 14, 2025
bade002
Remove archived attribute from contest_instance_params for explicit h…
rsmoke May 14, 2025
39ba781
Remove archived_icon helper method from EntriesHelper to streamline c…
rsmoke May 14, 2025
a8b3dbc
Refactor Container model to remove archived checks for contest instan…
rsmoke May 14, 2025
71801d0
Remove archived scope from ContestDescription model to streamline act…
rsmoke May 14, 2025
a10af00
Refactor ContestInstance model to remove archived checks from active_…
rsmoke May 14, 2025
58ed55f
Remove archived input fields from contest description and contest ins…
rsmoke May 14, 2025
6b11c45
Remove archived display elements from contest instances index view to…
rsmoke May 14, 2025
972326a
Remove archived attribute from seed data for contest descriptions and…
rsmoke May 14, 2025
6a56703
Remove archived attribute and related trait from contest descriptions…
rsmoke May 14, 2025
33ba7dc
Remove archived attribute and related trait from contest instances fa…
rsmoke May 14, 2025
343d42e
Remove test for excluding entries from archived contest instances in …
rsmoke May 14, 2025
c859a5e
Remove tests for archived contests in ContestDescription model specs …
rsmoke May 14, 2025
b36630e
Remove tests for archived contest instances in ContestInstance model …
rsmoke May 14, 2025
1f23626
Remove container partial view to eliminate references to archived con…
rsmoke May 14, 2025
68c6531
Merge pull request #127 from lsa-mis/remove_archive
rsmoke May 14, 2025
c6c8223
Potential fix for code scanning alert no. 3: Workflow does not contai…
rsmoke May 14, 2025
13aa9d9
Merge pull request #128 from lsa-mis/alert-autofix-3
rsmoke May 14, 2025
05bd568
Bump undici in the npm_and_yarn group across 1 directory
dependabot[bot] May 15, 2025
f34b597
Merge pull request #129 from lsa-mis/dependabot/npm_and_yarn/npm_and_…
rsmoke May 15, 2025
a16a7c4
Add modal details route and view for entry details
rsmoke May 19, 2025
3c18c54
Add entry details partial view for displaying entry information and a…
rsmoke May 19, 2025
97cbf7d
Enhance judging results view with entry detail buttons and modal inte…
rsmoke May 19, 2025
a81792f
Add modal_details action to EntriesController for rendering entry det…
rsmoke May 19, 2025
c3a75fc
Implement show? method in EntryPolicy to manage entry visibility base…
rsmoke May 19, 2025
9e582f9
Refactor EntryPolicy to improve authorization checks and add comprehe…
rsmoke May 20, 2025
e27c55a
Add export_round_results action to ContestInstancesController for exp…
rsmoke May 21, 2025
99b5237
Enhance judging results view by adding export button for round result…
rsmoke May 21, 2025
595c10d
Add export_round_results route to enable exporting judging round resu…
rsmoke May 21, 2025
834b7d1
Configure RSpec to include Capybara for system tests, setting up brow…
rsmoke May 21, 2025
e7bc693
Add comprehensive tests for export_round_results action in ContestIns…
rsmoke May 21, 2025
beee551
Merge pull request #130 from lsa-mis/flush_out_judging_results
rsmoke May 21, 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
4 changes: 4 additions & 0 deletions .github/workflows/brakeman.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ on:
schedule:
- cron: '0 0 * * 0' # Run weekly on Sunday

permissions:
contents: read
actions: write

jobs:
security:
runs-on: ubuntu-latest
Expand Down
1 change: 0 additions & 1 deletion app/controllers/bulk_contest_instances_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ def create_contest_instances
new_instance.date_closed = params[:bulk_contest_instance_form][:date_closed]
new_instance.created_by = current_user.email
new_instance.active = false
new_instance.archived = false

# Copy relationships if they exist
if last_instance
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/contest_descriptions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def set_contest_description
end

def contest_description_params
params.require(:contest_description).permit(:created_by, :active, :archived,
params.require(:contest_description).permit(:created_by, :active,
:eligibility_rules, :name, :notes,
:short_name,
:container_id)
Expand Down
100 changes: 99 additions & 1 deletion app/controllers/contest_instances_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,42 @@ def export_entries
end
end

def export_round_results
begin
@contest_instance = @contest_description.contest_instances.find(params[:id])
authorize @contest_instance, :export_entries?

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

@entries = judging_round.entries.distinct.includes(
:profile, :category,
entry_rankings: [:user]
)

respond_to do |format|
format.csv do
filename = "#{@contest_description.name.parameterize}-round-#{judging_round.round_number}-results-#{Time.zone.today}.csv"

csv_data = generate_round_results_csv(@entries, @contest_description, @contest_instance, judging_round)

send_data csv_data,
type: 'text/csv; charset=utf-8; header=present',
disposition: "attachment; filename=#{filename}"
end
end
rescue Pundit::NotAuthorizedError
flash[:alert] = 'Not authorized to access this contest instance'
redirect_to root_path
end
end

private

def authorize_container_access
Expand All @@ -202,7 +238,7 @@ def redirect_to_contest_instance_path

def contest_instance_params
params.require(:contest_instance).permit(
:active, :archived, :contest_description_id, :date_open, :date_closed,
:active, :contest_description_id, :date_open, :date_closed,
:notes, :judging_open, :judge_evaluations_complete,
:maximum_number_entries_per_applicant, :require_pen_name,
:require_campus_employment_info, :require_finaid_info, :created_by,
Expand Down Expand Up @@ -255,4 +291,66 @@ def generate_entries_csv(entries, contest_description, contest_instance)
end
end
end

def generate_round_results_csv(entries, contest_description, contest_instance, judging_round)
require 'csv'

CSV.generate do |csv|
# Header section
contest_info = "#{contest_description.name} - Round #{judging_round.round_number} Results"
header_row1 = [contest_info] + Array.new(15, '')
csv << header_row1
csv << Array.new(16, '') # Empty row as separator

# Column headers
headers = [
'Title', 'Category',
'Pen Name', 'First Name', 'Last Name', 'UMID', 'Uniqname',
'Class Level', 'Campus', 'Entry ID', 'Selected for Next Round',
'Judge Name', 'Score', 'Judge Comments [External]', 'Judge Comments [Internal]'
]
csv << headers

# Entry data
entries.each do |entry|
profile = entry.profile
rankings = entry.entry_rankings.where(judging_round: judging_round)
selected = rankings.exists?(selected_for_next_round: true)

# Base entry data
base_data = [
entry.title,
entry.category&.kind,
entry.pen_name,
profile&.user&.first_name,
profile&.user&.last_name,
profile&.umid,
profile&.user&.uniqname,
profile&.class_level&.name,
profile&.campus&.campus_descr,
entry.id,
selected ? 'Yes' : 'No'
]

# If there are rankings, create a row for each judge's ranking
if rankings.any?
rankings.each do |ranking|
score = ranking.rank
external_comments = ranking.external_comments.presence || 'No comment entered'
internal_comments = ranking.internal_comments.presence || 'No comment entered'

csv << base_data + [
"#{ranking.user.display_name_or_first_name_last_name} (#{ranking.user.uid})",
score,
external_comments,
internal_comments
]
end
else
# If no rankings, just output the base data with empty judge fields
csv << base_data + [ '', '' ]
end
end
end
end
end
8 changes: 7 additions & 1 deletion app/controllers/entries_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class EntriesController < ApplicationController
include AvailableContestsConcern
before_action :set_entry, only: %i[ show edit update destroy soft_delete toggle_disqualified ]
before_action :set_entry, only: %i[ show edit update destroy soft_delete toggle_disqualified modal_details ]
before_action :set_entry_for_profile, only: %i[ applicant_profile ]
before_action :authorize_entry, only: %i[show edit update destroy]
before_action :authorize_index, only: [ :index ]
Expand All @@ -16,6 +16,12 @@ def show
authorize @entry
end

# GET /entries/1/modal_details
def modal_details
authorize @entry, :show?
render layout: false
end

# GET /entries/new
def new
contest_instance_id = params[:contest_instance_id]
Expand Down
8 changes: 0 additions & 8 deletions app/helpers/entries_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,4 @@ def disqualified_icon(entry)
''
end
end

def archived_icon(entry)
if entry.archived
content_tag(:i, '', class: 'bi bi-eye', style: 'font-size: 1.5rem;', aria: { hidden: 'true' })
else
''
end
end
end
4 changes: 2 additions & 2 deletions app/models/container.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def active_entries
Entry.joins(contest_instance: { contest_description: :container })
.where(containers: { id: id })
.where(deleted: false)
.where(contest_instances: { active: true, archived: false })
.where(contest_descriptions: { active: true, archived: false })
.where(contest_instances: { active: true })
.where(contest_descriptions: { active: true })
end
end
1 change: 0 additions & 1 deletion app/models/contest_description.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,4 @@ class ContestDescription < ApplicationRecord
validates :name, presence: true, uniqueness: true

scope :active, -> { where(active: true) }
scope :archived, -> { where(archived: true) }
end
4 changes: 2 additions & 2 deletions app/models/contest_instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class ContestInstance < ApplicationRecord

# Scopes
scope :active_and_open, -> {
where(active: true, archived: false)
where(active: true)
.where('date_open <= ? AND date_closed >= ?', Time.zone.now, Time.zone.now)
}

Expand Down Expand Up @@ -89,7 +89,7 @@ class ContestInstance < ApplicationRecord
}

def open?
active && !archived && Time.current.between?(date_open, date_closed)
active && Time.current.between?(date_open, date_closed)
end

def judging_open?(user = nil)
Expand Down
17 changes: 16 additions & 1 deletion app/policies/entry_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,22 @@ def toggle_disqualified?
def view_applicant_profile?
container = record.contest_instance.contest_description.container
record.profile.user == user ||
user&.has_container_role?(container, ['Collection Administrator', 'Collection Manager']) ||
user&.has_container_role?(container, [ 'Collection Administrator', 'Collection Manager' ]) ||
axis_mundi?
end

def show?
# Allow users to see their own entries
return true if record.profile.user == user

# Allow collection admins/managers to see entries from their containers
container = record.contest_instance.contest_description.container
return true if user&.has_container_role?(container)

# Allow judges to see entries they've been assigned to judge
return true if user&.judging_assignments&.pluck(:contest_instance_id)&.include?(record.contest_instance_id)

# Fall back to axis_mundi check
axis_mundi?
end

Expand Down
90 changes: 0 additions & 90 deletions app/views/containers/_container.html.erb

This file was deleted.

1 change: 0 additions & 1 deletion app/views/contest_descriptions/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
<%= f.input :notes, as: :rich_text_area, hint: "These notes are not visible on the applicant entry form" %>
<%= f.input :short_name %>
<%= f.input :active, hint: "Toggle this checkbox to make the contest active. When active, administrators will be able to manage the submitted entries. Judges, during their assigned judging period, will be able to view the entries and provide feedback. Note that the contest's instances availabilty for submissions will follow the visibility settings based on specific dates configured in each contest instance. If this checkbox is unchecked, the contest and its instances will remain inactive and hidden from evaluation workflows." %>
<%= f.input :archived %>
</div>

<div class="form-actions">
Expand Down
1 change: 0 additions & 1 deletion app/views/contest_instances/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

<div class="form-inputs">
<%= f.input :active, hint: "Specify whether this contest is currently accepting entries during the contest open and close dates specified below." %>
<%= f.input :archived %>
<%= f.association :contest_description, label_method: :name %>
<div class="row">
<div class="col-md-6">
Expand Down
31 changes: 28 additions & 3 deletions app/views/contest_instances/_judging_results.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,21 @@
<% if @contest_instance.judging_rounds.any? %>
<% @contest_instance.judging_rounds.order(:round_number).each do |round| %>
<div class="p-4 mb-4 card border-primary">
<h2>Round <%= round.round_number %></h2>
<div class="d-flex justify-content-between align-items-center">
<h2>Round <%= round.round_number %></h2>
<%= link_to export_round_results_container_contest_description_contest_instance_path(
@container,
@contest_description,
@contest_instance,
round_id: round.id,
format: :csv
),
class: "btn btn-sm btn-outline-primary",
data: { turbo: false } do %>
<i class="bi bi-file-earmark-spreadsheet"></i>
Export round <%= round.round_number %> results
<% end %>
</div>
<div class="d-flex align-items-center mb-3">
<div class="d-inline-block"
data-bs-toggle="tooltip"
Expand Down Expand Up @@ -36,7 +50,7 @@
<table class="table">
<thead>
<tr>
<th>Entry ID</th>
<th class="small">Entry ID</th>
<th>Title</th>
<th>Average Rank</th>
<th>Individual Rankings</th>
Expand All @@ -52,7 +66,18 @@

<% entries_with_avg_rank.each do |entry, _| %>
<tr>
<td><%= entry.id %></td>
<td>
<button type="button"
class="btn btn-sm btn-outline-primary"
data-action="click->modal#open"
data-url="<%= modal_details_entry_path(entry) %>"
data-modal-title="Entry <%= entry.id %> Details"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="View entry details">
<%= entry.id %>
</button>
</td>
<td><%= entry.title %></td>
<td>
<%= entry.entry_rankings.where(judging_round: round).average(:rank)&.round(2) || 'No rankings' %>
Expand Down
Loading
Loading