Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ gem 'actiontext'
gem 'bootsnap', require: false
gem 'country_select'
gem 'cssbundling-rails'
gem 'csv', '~> 3.2'
gem 'devise', '~> 4.9'
gem 'google-cloud-storage', '~> 1.52'
gem 'image_processing', '~> 1.2'
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ GEM
crass (1.0.6)
cssbundling-rails (1.4.1)
railties (>= 6.0.0)
csv (3.3.3)
database_cleaner-active_record (2.2.0)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
Expand Down Expand Up @@ -551,6 +552,7 @@ DEPENDENCIES
capybara
country_select
cssbundling-rails
csv (~> 3.2)
database_cleaner-active_record (~> 2.0)
debug
devise (~> 4.9)
Expand Down
Binary file added app/assets/images/base_email.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/email_with_extra_info.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 47 additions & 1 deletion app/controllers/containers_controller.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# app/controllers/containers_controller.rb
class ContainersController < ApplicationController
include ContestDescriptionsHelper
before_action :set_container, only: %i[show edit update destroy description]
before_action :set_container, only: %i[show edit update destroy description active_applicants_report]
before_action :authorize_container, only: %i[edit show update destroy description]
before_action :authorize_index, only: [ :index ]

Expand All @@ -15,6 +15,7 @@ def show
).includes(:user, :role)
@assignment = @container.assignments.build
@container_contest_descriptions = @container.contest_descriptions.reorder('contest_descriptions.name ASC')
@active_contest_descriptions = @container.contest_descriptions.active.reorder('contest_descriptions.name ASC')
end

def new
Expand Down Expand Up @@ -95,6 +96,51 @@ def description
end
end

def active_applicants_report
contest_description_ids = params[:contest_description_ids] || []
@active_contest_descriptions = @container.contest_descriptions.active.where(id: contest_description_ids)

authorize @container

if @active_contest_descriptions.empty?
respond_to do |format|
format.csv { redirect_to @container, alert: 'Please select at least one contest description.' }
format.html { redirect_to @container, alert: 'Please select at least one contest description.' }
end
return
end

service = ActiveApplicantsReportService.new(
container: @container,
contest_descriptions: @active_contest_descriptions
)

@profiles = service.call

respond_to do |format|
format.csv do
filename = "active-applicants-in-#{@container.name.parameterize}_#{Time.zone.today}.csv"

csv_data = CSV.generate do |csv|
csv << ['Last Name', 'First Name', 'Email']

@profiles.each do |profile|
csv << [
profile.last_name,
profile.first_name,
profile.user.email
]
end
end

send_data csv_data,
type: 'text/csv; charset=utf-8; header=present',
disposition: "attachment; filename=#{filename}"
end
format.html { redirect_to @container, alert: 'Please request the report in CSV format.' }
end
end

private

def authorize_container
Expand Down
64 changes: 64 additions & 0 deletions app/controllers/contest_instances_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,28 @@ def send_round_results
notice: "Successfully queued #{email_count} evaluation result emails for round #{judging_round.round_number}. This is email batch ##{judging_round.emails_sent_count}."
end

def export_entries
@contest_instance = ContestInstance.find(params[:id])
@contest_description = @contest_instance.contest_description
@container = @contest_description.container

authorize @contest_instance

@entries = @contest_instance.entries.active.includes(:profile, :category)

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

csv_data = generate_entries_csv(@entries, @contest_description, @contest_instance)

send_data csv_data,
type: 'text/csv; charset=utf-8; header=present',
disposition: "attachment; filename=#{filename}"
end
end
end

private

def authorize_container_access
Expand Down Expand Up @@ -179,4 +201,46 @@ def contest_instance_params
category_ids: [], class_level_ids: []
)
end

def generate_entries_csv(entries, contest_description, contest_instance)
require 'csv'

CSV.generate do |csv|
# Header section - split across multiple columns for better layout
contest_info = "#{contest_description.name} - #{contest_instance.date_open.strftime('%b %Y')} to #{contest_instance.date_closed.strftime('%b %Y')}"

# Distribute header across columns more evenly
header_row1 = [contest_info] + Array.new(11, '')
csv << header_row1
csv << Array.new(12, '') # Empty row as separator with 12 empty cells

# Column headers
headers = [
'Title', 'Category',
'Pen Name', 'First Name', 'Last Name', 'UMID', 'Uniqname',
'Class Level', 'Campus', 'Entry ID', 'Created At', 'Disqualified'
]
csv << headers

# Entry data
entries.each do |entry|
profile = entry.profile

csv << [
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,
entry.created_at.strftime('%m/%d/%Y %I:%M %p'),
entry.disqualified? ? 'Yes' : 'No'
]
end
end
end
end
51 changes: 51 additions & 0 deletions app/javascript/controllers/email_preview_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["baseEmail", "optionsEmail"]
static values = { type: { type: String, default: "base" } }

connect() {
console.log("Email preview controller connected")

// Add event listener for the Bootstrap modal shown event
const modal = this.element.closest('.modal')
if (modal) {
console.log("Modal found, adding event listener")
modal.addEventListener('shown.bs.modal', () => {
console.log("Modal shown event triggered")
console.log("Current type value:", this.typeValue)
this.updatePreview()
})
}
}

// Set the type value when a button is clicked
setType(event) {
const newType = event.currentTarget.dataset.previewType
console.log("Setting type to:", newType)
this.typeValue = newType
}

// Update the preview based on the current type
updatePreview() {
console.log("Updating preview with type:", this.typeValue)

if (this.typeValue === "base") {
this.showBaseEmail()
} else if (this.typeValue === "options") {
this.showOptionsEmail()
}
}

showBaseEmail() {
console.log("Showing base email")
this.baseEmailTarget.style.display = "block"
this.optionsEmailTarget.style.display = "none"
}

showOptionsEmail() {
console.log("Showing options email")
this.baseEmailTarget.style.display = "none"
this.optionsEmailTarget.style.display = "block"
}
}
4 changes: 4 additions & 0 deletions app/policies/container_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ def description?
true
end

def active_applicants_report?
owns_container? || axis_mundi?
end

private

def user_has_containers?
Expand Down
4 changes: 4 additions & 0 deletions app/policies/contest_instance_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,8 @@ def deactivate?
def send_round_results?
user&.has_container_role?(record.contest_description.container) || axis_mundi?
end

def export_entries?
user&.has_container_role?(record.contest_description.container) || axis_mundi?
end
end
28 changes: 28 additions & 0 deletions app/services/active_applicants_report_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
class ActiveApplicantsReportService
def initialize(container:, contest_descriptions:)
@container = container
@active_contest_descriptions = contest_descriptions
end

def call
# Get all active entries for the specified contest descriptions
# Use distinct to avoid duplicate profiles
Profile
.joins(:entries)
.joins('INNER JOIN contest_instances ON entries.contest_instance_id = contest_instances.id')
.joins('INNER JOIN contest_descriptions ON contest_instances.contest_description_id = contest_descriptions.id')
.joins(:user)
.where(
entries: { deleted: false, disqualified: false },
contest_descriptions: {
id: @active_contest_descriptions.map(&:id),
active: true
},
contest_instances: {
active: true
}
)
.select('DISTINCT profiles.*, users.first_name, users.last_name, users.email')
.order('users.last_name, users.first_name')
end
end
30 changes: 30 additions & 0 deletions app/views/containers/_active_applicants_report_form.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<%= turbo_frame_tag "active_applicants_report_form" do %>
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">Active Applicants Report</h5>
</div>
<div class="card-body">
<%= form_with url: active_applicants_report_container_path(@container, format: :csv), method: :get, data: { turbo: false } do |f| %>
<div class="mb-3">
<label class="form-label">Select Contest Descriptions</label>
<div class="border rounded p-3" style="max-height: 300px; overflow-y: auto;">
<% @active_contest_descriptions.each do |contest_description| %>
<div class="form-check">
<%= check_box_tag "contest_description_ids[]",
contest_description.id,
false,
class: "form-check-input",
id: "contest_description_#{contest_description.id}" %>
<%= label_tag "contest_description_#{contest_description.id}",
contest_description.name,
class: "form-check-label" %>
</div>
<% end %>
</div>
</div>

<%= f.submit "Generate Report", class: "btn btn-primary" %>
<% end %>
</div>
</div>
<% end %>
5 changes: 5 additions & 0 deletions app/views/containers/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@

<hr>

<h3 class="mt-4">Reports</h3>
<%= render 'active_applicants_report_form' %>

<hr>

<h3 class="mt-4">User Permissions</h3>
<% if (content = render_editable_content('container', 'permissions')) %>
<%= content %>
Expand Down
14 changes: 13 additions & 1 deletion app/views/contest_instances/_contest_instance_entries.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
<% if @contest_instance_entries.any? %>
<h2 class="text-muted"><%= pluralize(@contest_instance_entries.count, 'Entry') %></h2>
<div class="d-flex align-items-center justify-content-between mb-3">
<h2 class="text-muted mb-0"><%= pluralize(@contest_instance_entries.count, 'Entry') %></h2>
<%= link_to export_entries_container_contest_description_contest_instance_path(
@container,
@contest_description,
@contest_instance,
format: :csv
),
class: "btn btn-sm btn-outline-primary",
data: { turbo: false } do %>
<i class="fas fa-file-download me-1"></i> Export to CSV
<% end %>
</div>
<table class="table table-striped w-100">
<thead>
<tr>
Expand Down
52 changes: 52 additions & 0 deletions app/views/contest_instances/email_preferences.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@
<div class="card-body">
<p class="mb-4">
Select which information to include in the evaluation result emails for this round:
<button type="button"
class="btn btn-sm btn-outline-secondary"
data-bs-toggle="modal"
data-bs-target="#baseEmailModal">
Sample of the base email
</button>
<button type="button"
class="btn btn-sm btn-outline-secondary"
data-bs-toggle="modal"
data-bs-target="#optionsEmailModal">
Sample of the email with options
</button>
</p>

<%= form_with url: send_round_results_container_contest_description_contest_instance_path(
Expand Down Expand Up @@ -47,3 +59,43 @@
<% end %>
</div>
</div>

<!-- Base Email Modal -->
<div class="modal fade" id="baseEmailModal" tabindex="-1" aria-labelledby="baseEmailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="baseEmailModalLabel">Base Email Preview</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="email-preview">
<%= image_tag asset_path("base_email.png"), class: "img-fluid", alt: "Base email preview" %>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>

<!-- Options Email Modal -->
<div class="modal fade" id="optionsEmailModal" tabindex="-1" aria-labelledby="optionsEmailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="optionsEmailModalLabel">Email with Options Preview</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="email-preview">
<%= image_tag asset_path("email_with_extra_info.png"), class: "img-fluid", alt: "Email with options preview" %>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
Loading