Implement CSV import and conversion to contestants
All checks were successful
CI / scan_ruby (push) Successful in 17s
CI / scan_js (push) Successful in 11s
CI / lint (push) Successful in 12s
CI / test (push) Successful in 26s

This commit is contained in:
sto 2025-05-17 17:40:03 +02:00
parent 939e2157ab
commit ec2201f9a8
12 changed files with 165 additions and 17 deletions

View File

@ -45,15 +45,48 @@ class ContestantsController < ApplicationController
def import def import
authorize @contest authorize @contest
if params[:csv_import] @csv_import = CsvImport.new
@csv_import = CsvImport.new(params.require(:csv_import).permit(:file, :separator)) end
if @csv_import.save
@csv_import = CsvImport.new def upload_csv
else authorize @contest
render :import, status: :unprocessable_entity
end @csv_import = CsvImport.new(params.require(:csv_import).permit(:file, :separator))
if @csv_import.save
redirect_to "/contests/#{@contest.id}/import/#{@csv_import.id}"
else else
@csv_import = CsvImport.new render :import, status: :unprocessable_entity
end
end
def convert_csv
authorize @contest
@csv_import = CsvImport.find(params[:id])
@content = JSON.parse(@csv_import.content)
@form = Forms::CsvConversionForm.new
end
def finalize_import
authorize @contest
@csv_import = CsvImport.find(params[:id])
@content = JSON.parse(@csv_import.content)
all_params = params.require(:forms_csv_conversion_form)
@form = Forms::CsvConversionForm.new(params.require(:forms_csv_conversion_form).permit(:email_column, :name_column))
if @form.valid?
@content.each_with_index do |row, i|
if all_params["row_#{i}".to_sym] == "1"
if @form.email_column == -1
Contestant.create(name: row[@form.name_column], contest: @contest)
else
Contestant.create(name: row[@form.name_column], email: row[@form.email_column], contest: @contest)
end
end
end
redirect_to contest_path(@contest)
else
render :convert_csv, status: :unprocessable_entity
end end
end end

14
app/lib/forms.rb Normal file
View File

@ -0,0 +1,14 @@
module Forms
class CsvConversionForm
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveModel::Validations::Callbacks
include ActiveRecord::Transactions
attribute :name_column, :integer
attribute :email_column, :integer
validates :name_column, presence: true
validates_numericality_of :name_column, greater_than: -1
end
end

View File

@ -3,23 +3,26 @@
# Table name: csv_imports # Table name: csv_imports
# #
# id :integer not null, primary key # id :integer not null, primary key
# separator :integer not null # content :string not null
# separator :string not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# #
class CsvImport < ApplicationRecord class CsvImport < ApplicationRecord
enum :separator, { comma: ",", semicolon: ";" }, default: :comma enum :separator, { comma: ",", semicolon: ";" }, suffix: true, default: :comma
has_one_attached :file has_one_attached :file
validates :file, presence: true validates :file, presence: true
validate :acceptable_csv, on: :create validate :acceptable_csv, on: :create
before_save :read_csv
def acceptable_csv def acceptable_csv
return unless file.attached? return unless file.attached?
if file.blob.byte_size > 20 if file.blob.byte_size > 5 * 1024 * 1024
errors.add(:file, "this csv file is too large, it must be under 20MB") errors.add(:file, "this csv file is too large, it must be under 5MB")
return return
end end
@ -31,14 +34,16 @@ class CsvImport < ApplicationRecord
begin begin
csv = CSV.read(attachment_changes["file"].attachable.path, col_sep: separator) csv = CSV.read(attachment_changes["file"].attachable.path, col_sep: separator)
logger = Logger.new(STDOUT)
logger.info(csv)
errors.add(:file, :empty) if csv.count < 1 || (csv.count == 1 && csv[0].count == 1 && csv[0][0] == "") errors.add(:file, :empty) if csv.count < 1 || (csv.count == 1 && csv[0].count == 1 && csv[0][0] == "")
rescue CSV::MalformedCSVError => e rescue CSV::MalformedCSVError => e
errors.add(:file, e.message) errors.add(:file, e.message)
end end
end end
def read_csv
self.content = JSON.dump(CSV.read(attachment_changes["file"].attachable.path, col_sep: separator_for_database))
end
def options_for_separator def options_for_separator
keys = self.class.separators.keys keys = self.class.separators.keys
keys.map(&:humanize).zip(keys).to_h keys.map(&:humanize).zip(keys).to_h

View File

@ -19,10 +19,18 @@ class ContestPolicy < ApplicationPolicy
record.user.id == user.id || user.admin? record.user.id == user.id || user.admin?
end end
def convert_csv?
record.user.id == user.id || user.admin?
end
def edit? def edit?
record.user.id == user.id || user.admin? record.user.id == user.id || user.admin?
end end
def finalize_import?
record.user.id == user.id || user.admin?
end
def update? def update?
record.user.id == user.id || user.admin? record.user.id == user.id || user.admin?
end end
@ -38,4 +46,8 @@ class ContestPolicy < ApplicationPolicy
def scoreboard? def scoreboard?
true true
end end
def upload_csv?
record.user.id == user.id || user.admin?
end
end end

View File

@ -0,0 +1,40 @@
= form_with model: @form, url: "/contests/#{@contest.id}/import/#{@csv_import.id}" do |form|
.row.mb-3
.col
.form-floating
= form.select :name_column, [[t("helpers.none"), -1]] + Array.new(@content[0].count) {|i| [t("helpers.field") + "_#{i}", i] }, {}, class: "form-select"
= form.label :name_column
= t("contestants.import.name_column")
.row.mb-3
.col
.form-floating
= form.select :email_column, [[t("helpers.none"), -1]] + Array.new(@content[0].count) {|i| [t("helpers.field") + "_#{i}", i] }, {}, class: "form-select"
= form.label :email_column
= t("contestants.import.email_column")
.row.g-3
.col
table.table.table-striped.table-hover
thead
tr
- @content[0].each_with_index do |_, i|
th scope="col"
= t("helpers.field") + "_#{i}"
th scope="col"
= t("contestants.import.import_column")
tbody
- @content.each_with_index do |row, i|
tr scope="row"
- row.each do |value|
td
= value
td
.form-check.form-switch
= form.check_box "row_#{i}".to_sym, class: "form-check-input", checked: true
.row.g-3
.col
= form.submit t("helpers.buttons.confirm"), class: "btn btn-primary"

View File

@ -28,6 +28,14 @@
# enabled: "ON" # enabled: "ON"
en: en:
activemodel:
errors:
models:
forms/csv_conversion_form:
attributes:
name_column:
blank: "Participant names are required"
greater_than: "Participant names are required"
activerecord: activerecord:
attributes: attributes:
contest: contest:
@ -78,11 +86,18 @@ en:
add_puzzle: "Add puzzle" add_puzzle: "Add puzzle"
public_scoreboard: "Public scoreboard: " public_scoreboard: "Public scoreboard: "
contestants: contestants:
convert_csv:
title: "Import participants"
edit: edit:
title: "Participant" title: "Participant"
team_title: "Teams" team_title: "Teams"
finalize_import:
title: "Import participants"
import: import:
title: "Import participants from a CSV file" email_column: "Participant email"
import_column: "Import?"
name_column: "Participant name"
title: "Import participants"
new: new:
title: "New participant" title: "New participant"
team_title: "New team" team_title: "New team"
@ -94,9 +109,11 @@ en:
helpers: helpers:
buttons: buttons:
add: "Add" add: "Add"
confirm: "Confirm"
create: "Create" create: "Create"
import: "CSV Import" import: "CSV Import"
save: "Save" save: "Save"
field: "Field"
messages: messages:
convert: convert:
title: "Convert message into completion" title: "Convert message into completion"

View File

@ -1,4 +1,12 @@
fr: fr:
activemodel:
errors:
models:
forms/csv_conversion_form:
attributes:
name_column:
blank: "Choisir une colonne pour les noms des participant.e.s est nécessaire"
greater_than: "Choisir une colonne pour les noms des participant.e.s est nécessaire"
activerecord: activerecord:
attributes: attributes:
contest: contest:
@ -49,10 +57,17 @@ fr:
add_puzzle: "Ajouter un puzzle" add_puzzle: "Ajouter un puzzle"
public_scoreboard: "Classement public : " public_scoreboard: "Classement public : "
contestants: contestants:
convert_csv:
title: "Importer des participant.e.s"
edit: edit:
title: "Participant.e" title: "Participant.e"
team_title: "Équipe" team_title: "Équipe"
finalize_import:
title: "Importer des participant.e.s"
import: import:
email_column: "Email des participant.e.s"
import_column: "Importer ?"
name_column: "Noms des participant.e.s"
title: "Importer des participant.e.s" title: "Importer des participant.e.s"
new: new:
title: "Nouveau.elle participant.e" title: "Nouveau.elle participant.e"
@ -65,9 +80,11 @@ fr:
helpers: helpers:
buttons: buttons:
add: "Ajouter" add: "Ajouter"
confirm: "Confirmer"
create: "Créer" create: "Créer"
import: "Importer un CSV" import: "Importer un CSV"
save: "Modifier" save: "Modifier"
field: "Champ"
messages: messages:
convert: convert:
title: "Conversion d'un message en complétion" title: "Conversion d'un message en complétion"

View File

@ -16,7 +16,9 @@ Rails.application.routes.draw do
get "convert", to: "messages#convert" get "convert", to: "messages#convert"
end end
get "import", to: "contestants#import" get "import", to: "contestants#import"
post "import", to: "contestants#import" post "import", to: "contestants#upload_csv"
get "import/:id", to: "contestants#convert_csv"
post "import/:id", to: "contestants#finalize_import"
end end
resources :passwords, param: :token resources :passwords, param: :token
resource :session resource :session

View File

@ -0,0 +1,5 @@
class AddContentToCsvImport < ActiveRecord::Migration[8.0]
def change
add_column :csv_imports, :content, :string, null: false
end
end

3
db/schema.rb generated
View File

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_05_17_083830) do ActiveRecord::Schema[8.0].define(version: 2025_05_17_131707) do
create_table "active_storage_attachments", force: :cascade do |t| create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "record_type", null: false t.string "record_type", null: false
@ -79,6 +79,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_17_083830) do
t.string "separator", null: false t.string "separator", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "content", null: false
end end
create_table "friendly_id_slugs", force: :cascade do |t| create_table "friendly_id_slugs", force: :cascade do |t|

View File

@ -3,6 +3,7 @@
# Table name: csv_imports # Table name: csv_imports
# #
# id :integer not null, primary key # id :integer not null, primary key
# content :string not null
# separator :string not null # separator :string not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null

View File

@ -3,6 +3,7 @@
# Table name: csv_imports # Table name: csv_imports
# #
# id :integer not null, primary key # id :integer not null, primary key
# content :string not null
# separator :string not null # separator :string not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null