Skip to content

Commit b114aec

Browse files
authored
Merge pull request #114 from lsa-mis/staging
Add reports and sample emails
2 parents bb5680a + 08b0393 commit b114aec

16 files changed

Lines changed: 574 additions & 2 deletions

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ gem 'actiontext'
1010
gem 'bootsnap', require: false
1111
gem 'country_select'
1212
gem 'cssbundling-rails'
13+
gem 'csv', '~> 3.2'
1314
gem 'devise', '~> 4.9'
1415
gem 'google-cloud-storage', '~> 1.52'
1516
gem 'image_processing', '~> 1.2'

Gemfile.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ GEM
133133
crass (1.0.6)
134134
cssbundling-rails (1.4.1)
135135
railties (>= 6.0.0)
136+
csv (3.3.3)
136137
database_cleaner-active_record (2.2.0)
137138
activerecord (>= 5.a)
138139
database_cleaner-core (~> 2.0.0)
@@ -551,6 +552,7 @@ DEPENDENCIES
551552
capybara
552553
country_select
553554
cssbundling-rails
555+
csv (~> 3.2)
554556
database_cleaner-active_record (~> 2.0)
555557
debug
556558
devise (~> 4.9)

app/assets/images/base_email.png

194 KB
Loading
111 KB
Loading

app/controllers/containers_controller.rb

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# app/controllers/containers_controller.rb
22
class ContainersController < ApplicationController
33
include ContestDescriptionsHelper
4-
before_action :set_container, only: %i[show edit update destroy description]
4+
before_action :set_container, only: %i[show edit update destroy description active_applicants_report]
55
before_action :authorize_container, only: %i[edit show update destroy description]
66
before_action :authorize_index, only: [ :index ]
77

@@ -15,6 +15,7 @@ def show
1515
).includes(:user, :role)
1616
@assignment = @container.assignments.build
1717
@container_contest_descriptions = @container.contest_descriptions.reorder('contest_descriptions.name ASC')
18+
@active_contest_descriptions = @container.contest_descriptions.active.reorder('contest_descriptions.name ASC')
1819
end
1920

2021
def new
@@ -95,6 +96,51 @@ def description
9596
end
9697
end
9798

99+
def active_applicants_report
100+
contest_description_ids = params[:contest_description_ids] || []
101+
@active_contest_descriptions = @container.contest_descriptions.active.where(id: contest_description_ids)
102+
103+
authorize @container
104+
105+
if @active_contest_descriptions.empty?
106+
respond_to do |format|
107+
format.csv { redirect_to @container, alert: 'Please select at least one contest description.' }
108+
format.html { redirect_to @container, alert: 'Please select at least one contest description.' }
109+
end
110+
return
111+
end
112+
113+
service = ActiveApplicantsReportService.new(
114+
container: @container,
115+
contest_descriptions: @active_contest_descriptions
116+
)
117+
118+
@profiles = service.call
119+
120+
respond_to do |format|
121+
format.csv do
122+
filename = "active-applicants-in-#{@container.name.parameterize}_#{Time.zone.today}.csv"
123+
124+
csv_data = CSV.generate do |csv|
125+
csv << ['Last Name', 'First Name', 'Email']
126+
127+
@profiles.each do |profile|
128+
csv << [
129+
profile.last_name,
130+
profile.first_name,
131+
profile.user.email
132+
]
133+
end
134+
end
135+
136+
send_data csv_data,
137+
type: 'text/csv; charset=utf-8; header=present',
138+
disposition: "attachment; filename=#{filename}"
139+
end
140+
format.html { redirect_to @container, alert: 'Please request the report in CSV format.' }
141+
end
142+
end
143+
98144
private
99145

100146
def authorize_container

app/controllers/contest_instances_controller.rb

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,28 @@ def send_round_results
143143
notice: "Successfully queued #{email_count} evaluation result emails for round #{judging_round.round_number}. This is email batch ##{judging_round.emails_sent_count}."
144144
end
145145

146+
def export_entries
147+
@contest_instance = ContestInstance.find(params[:id])
148+
@contest_description = @contest_instance.contest_description
149+
@container = @contest_description.container
150+
151+
authorize @contest_instance
152+
153+
@entries = @contest_instance.entries.active.includes(:profile, :category)
154+
155+
respond_to do |format|
156+
format.csv do
157+
filename = "#{@contest_description.name.parameterize}-entries_printed-#{Time.zone.today}.csv"
158+
159+
csv_data = generate_entries_csv(@entries, @contest_description, @contest_instance)
160+
161+
send_data csv_data,
162+
type: 'text/csv; charset=utf-8; header=present',
163+
disposition: "attachment; filename=#{filename}"
164+
end
165+
end
166+
end
167+
146168
private
147169

148170
def authorize_container_access
@@ -179,4 +201,46 @@ def contest_instance_params
179201
category_ids: [], class_level_ids: []
180202
)
181203
end
204+
205+
def generate_entries_csv(entries, contest_description, contest_instance)
206+
require 'csv'
207+
208+
CSV.generate do |csv|
209+
# Header section - split across multiple columns for better layout
210+
contest_info = "#{contest_description.name} - #{contest_instance.date_open.strftime('%b %Y')} to #{contest_instance.date_closed.strftime('%b %Y')}"
211+
212+
# Distribute header across columns more evenly
213+
header_row1 = [contest_info] + Array.new(11, '')
214+
csv << header_row1
215+
csv << Array.new(12, '') # Empty row as separator with 12 empty cells
216+
217+
# Column headers
218+
headers = [
219+
'Title', 'Category',
220+
'Pen Name', 'First Name', 'Last Name', 'UMID', 'Uniqname',
221+
'Class Level', 'Campus', 'Entry ID', 'Created At', 'Disqualified'
222+
]
223+
csv << headers
224+
225+
# Entry data
226+
entries.each do |entry|
227+
profile = entry.profile
228+
229+
csv << [
230+
entry.title,
231+
entry.category&.kind,
232+
entry.pen_name,
233+
profile&.user&.first_name,
234+
profile&.user&.last_name,
235+
profile&.umid,
236+
profile&.user&.uniqname,
237+
profile&.class_level&.name,
238+
profile&.campus&.campus_descr,
239+
entry.id,
240+
entry.created_at.strftime('%m/%d/%Y %I:%M %p'),
241+
entry.disqualified? ? 'Yes' : 'No'
242+
]
243+
end
244+
end
245+
end
182246
end
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Controller } from "@hotwired/stimulus"
2+
3+
export default class extends Controller {
4+
static targets = ["baseEmail", "optionsEmail"]
5+
static values = { type: { type: String, default: "base" } }
6+
7+
connect() {
8+
console.log("Email preview controller connected")
9+
10+
// Add event listener for the Bootstrap modal shown event
11+
const modal = this.element.closest('.modal')
12+
if (modal) {
13+
console.log("Modal found, adding event listener")
14+
modal.addEventListener('shown.bs.modal', () => {
15+
console.log("Modal shown event triggered")
16+
console.log("Current type value:", this.typeValue)
17+
this.updatePreview()
18+
})
19+
}
20+
}
21+
22+
// Set the type value when a button is clicked
23+
setType(event) {
24+
const newType = event.currentTarget.dataset.previewType
25+
console.log("Setting type to:", newType)
26+
this.typeValue = newType
27+
}
28+
29+
// Update the preview based on the current type
30+
updatePreview() {
31+
console.log("Updating preview with type:", this.typeValue)
32+
33+
if (this.typeValue === "base") {
34+
this.showBaseEmail()
35+
} else if (this.typeValue === "options") {
36+
this.showOptionsEmail()
37+
}
38+
}
39+
40+
showBaseEmail() {
41+
console.log("Showing base email")
42+
this.baseEmailTarget.style.display = "block"
43+
this.optionsEmailTarget.style.display = "none"
44+
}
45+
46+
showOptionsEmail() {
47+
console.log("Showing options email")
48+
this.baseEmailTarget.style.display = "none"
49+
this.optionsEmailTarget.style.display = "block"
50+
}
51+
}

app/policies/container_policy.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ def description?
4848
true
4949
end
5050

51+
def active_applicants_report?
52+
owns_container? || axis_mundi?
53+
end
54+
5155
private
5256

5357
def user_has_containers?

app/policies/contest_instance_policy.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,8 @@ def deactivate?
6565
def send_round_results?
6666
user&.has_container_role?(record.contest_description.container) || axis_mundi?
6767
end
68+
69+
def export_entries?
70+
user&.has_container_role?(record.contest_description.container) || axis_mundi?
71+
end
6872
end
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
class ActiveApplicantsReportService
2+
def initialize(container:, contest_descriptions:)
3+
@container = container
4+
@active_contest_descriptions = contest_descriptions
5+
end
6+
7+
def call
8+
# Get all active entries for the specified contest descriptions
9+
# Use distinct to avoid duplicate profiles
10+
Profile
11+
.joins(:entries)
12+
.joins('INNER JOIN contest_instances ON entries.contest_instance_id = contest_instances.id')
13+
.joins('INNER JOIN contest_descriptions ON contest_instances.contest_description_id = contest_descriptions.id')
14+
.joins(:user)
15+
.where(
16+
entries: { deleted: false, disqualified: false },
17+
contest_descriptions: {
18+
id: @active_contest_descriptions.map(&:id),
19+
active: true
20+
},
21+
contest_instances: {
22+
active: true
23+
}
24+
)
25+
.select('DISTINCT profiles.*, users.first_name, users.last_name, users.email')
26+
.order('users.last_name, users.first_name')
27+
end
28+
end

0 commit comments

Comments
 (0)