diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb new file mode 100644 index 0000000..91e3528 --- /dev/null +++ b/app/controllers/categories_controller.rb @@ -0,0 +1,37 @@ +class CategoriesController < ApplicationController + before_action :set_contest + before_action :set_category, only: %i[ destroy] + + def create + authorize @contest + + @category = Category.new(category_params) + @category.contest_id = @contest.id + if @category.save + redirect_to edit_contest_path(@contest), notice: t("categories.new.notice") + else + redirect_to edit_contest_path(@contest), notice: t("categories.new.error") + end + end + + def destroy + authorize @contest + + @category.destroy + redirect_to edit_contest_path(@contest), notice: t("categories.destroy.notice") + end + + private + + def set_contest + @contest = Contest.find(params[:contest_id]) + end + + def set_category + @category = Category.find(params[:id]) + end + + def category_params + params.expect(category: [ :name ]) + end +end diff --git a/app/models/category.rb b/app/models/category.rb new file mode 100644 index 0000000..1065d3a --- /dev/null +++ b/app/models/category.rb @@ -0,0 +1,24 @@ +# == Schema Information +# +# Table name: categories +# +# id :integer not null, primary key +# name :string +# created_at :datetime not null +# updated_at :datetime not null +# contest_id :integer not null +# +# Indexes +# +# index_categories_on_contest_id (contest_id) +# +# Foreign Keys +# +# contest_id (contest_id => contests.id) +# +class Category < ApplicationRecord + belongs_to :contest + has_and_belongs_to_many :contestants + + validates :name, presence: true +end diff --git a/app/models/contest.rb b/app/models/contest.rb index 0a094b0..b5ef310 100644 --- a/app/models/contest.rb +++ b/app/models/contest.rb @@ -26,6 +26,7 @@ class Contest < ApplicationRecord extend FriendlyId belongs_to :user + has_many :categories has_many :completions, dependent: :destroy has_many :contestants, dependent: :destroy has_many :puzzles, dependent: :destroy diff --git a/app/models/contestant.rb b/app/models/contestant.rb index dfa32c7..5e6751f 100644 --- a/app/models/contestant.rb +++ b/app/models/contestant.rb @@ -22,6 +22,7 @@ class Contestant < ApplicationRecord belongs_to :contest has_many :completions, dependent: :destroy + has_and_belongs_to_many :categories before_validation :initialize_time_seconds_if_empty diff --git a/app/views/contests/_form.html.slim b/app/views/contests/_form.html.slim index c242aec..85b7c66 100644 --- a/app/views/contests/_form.html.slim +++ b/app/views/contests/_form.html.slim @@ -1,3 +1,4 @@ +h4.mt-5 = t("contests.form.general") = form_with model: contest do |form| .row.mb-3 .col @@ -20,7 +21,7 @@ = form.check_box :team, class: "form-check-input" = form.label :team .form-text = t("activerecord.attributes.contest.team_description") - .row.mb-3 + .row.mb-3 style="display: none" .col .form-check.form-switch = form.check_box :allow_registration, class: "form-check-input" @@ -28,4 +29,37 @@ .form-text = t("activerecord.attributes.contest.allow_registration_description") .row .col - = form.submit submit_text, class: "btn btn-primary" \ No newline at end of file + = form.submit submit_text, class: "btn btn-primary" + + +h4.mt-5 = t("contests.form.categories") + += form_with model: Category, url: "/contests/#{@contest.id}/categories" do |form| + - if @contest.categories.size > 0 + .row + .col-6 + table.table.table-striped.table-hover + thead + tr + th + = t("activerecord.attributes.category.name") + th + = t("activerecord.attributes.category.contestant_count") + tbody + - @contest.categories.each do |category| + tr.align-middle scope="row" + td + = category.name + td + = category.contestants.size + td + = link_to t("helpers.buttons.delete"), contest_category_path(@contest, category), data: { turbo_method: :delete }, class: "btn btn-sm btn-danger ms-2" + .row.mt-3 + .col-4 + .form-floating + = form.text_field :name, autocomplete: "off", value: nil, class: "form-control" + = form.label :name, class: "required" + = t("activerecord.attributes.category.new") + .row.mt-3 + .col + = form.submit t("helpers.buttons.add"), class: "btn btn-primary" \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index 8e64008..dd1cb8e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -38,6 +38,10 @@ en: greater_than: "Participant names are required" activerecord: attributes: + category: + contestant_count: Contestants count + new: New category + name: Category completion: contestant: Participant display_time: Time @@ -116,6 +120,12 @@ en: blank: Your email cannot be empty username: blank: Your username cannot be empty + categories: + destroy: + notice: Category deleted + new: + error: The category name can't be empty + notice: Category added completions: destroy: notice: Completion deleted @@ -132,6 +142,9 @@ en: edit: notice: Contest updated title: Edit contest settings + form: + categories: Participant categories + general: General parameters index: title: Welcome %{username}! manage_contests: Manage my contests diff --git a/config/locales/fr.yml b/config/locales/fr.yml index a2b4f84..76ad0e7 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -9,6 +9,10 @@ fr: greater_than: "Choisir une colonne pour les noms des participant.e.s est nécessaire" activerecord: attributes: + category: + contestant_count: Nombre de participant.e.s + new: Nouvelle catégorie + name: Catégorie completion: contestant_id: Participant.e display_time: Temps @@ -87,6 +91,12 @@ fr: blank: L'email est obligatoire username: blank: Le nom d'utilisateur.ice est obligatoire + categories: + destroy: + notice: Catégorie supprimée + new: + error: Le nom de la catégorie ne peut pas être vide + notice: Catégorie ajoutée completions: destroy: notice: Complétion supprimée @@ -103,6 +113,9 @@ fr: edit: notice: Concours modifié title: Paramètres du concours + form: + categories: Catégories de participant.e.s + general: Paramètres généraux index: title: Bienvenue %{username} ! manage_contests: Mes concours de puzzle diff --git a/config/routes.rb b/config/routes.rb index fceb1ef..d0e5e17 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -9,6 +9,7 @@ Rails.application.routes.draw do root "contests#index" resources :contests do + resources :categories, only: [ :create, :destroy ] resources :completions resources :contestants resources :puzzles diff --git a/db/migrate/20250714115208_create_categories.rb b/db/migrate/20250714115208_create_categories.rb new file mode 100644 index 0000000..f34104b --- /dev/null +++ b/db/migrate/20250714115208_create_categories.rb @@ -0,0 +1,15 @@ +class CreateCategories < ActiveRecord::Migration[8.0] + def change + create_table :categories do |t| + t.string :name + t.belongs_to :contest, null: false, foreign_key: true + + t.timestamps + end + + create_join_table :categories, :contestants do |t| + t.index :category_id + t.index :contestant_id + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 2069187..e722692 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_06_27_070407) do +ActiveRecord::Schema[8.0].define(version: 2025_07_14_115208) do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -39,6 +39,21 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_27_070407) do t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true end + create_table "categories", force: :cascade do |t| + t.string "name" + t.integer "contest_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["contest_id"], name: "index_categories_on_contest_id" + end + + create_table "categories_contestants", id: false, force: :cascade do |t| + t.integer "category_id", null: false + t.integer "contestant_id", null: false + t.index ["category_id"], name: "index_categories_contestants_on_category_id" + t.index ["contestant_id"], name: "index_categories_contestants_on_contestant_id" + end + create_table "completions", force: :cascade do |t| t.integer "time_seconds" t.integer "contestant_id", null: false @@ -141,6 +156,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_27_070407) do add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" + add_foreign_key "categories", "contests" add_foreign_key "completions", "contestants" add_foreign_key "completions", "contests" add_foreign_key "completions", "messages" diff --git a/spec/factories/categories.rb b/spec/factories/categories.rb new file mode 100644 index 0000000..5fc344f --- /dev/null +++ b/spec/factories/categories.rb @@ -0,0 +1,23 @@ +# == Schema Information +# +# Table name: categories +# +# id :integer not null, primary key +# name :string +# created_at :datetime not null +# updated_at :datetime not null +# contest_id :integer not null +# +# Indexes +# +# index_categories_on_contest_id (contest_id) +# +# Foreign Keys +# +# contest_id (contest_id => contests.id) +# +FactoryBot.define do + factory :category do + name { "MyString" } + end +end diff --git a/spec/models/category_spec.rb b/spec/models/category_spec.rb new file mode 100644 index 0000000..86c8383 --- /dev/null +++ b/spec/models/category_spec.rb @@ -0,0 +1,23 @@ +# == Schema Information +# +# Table name: categories +# +# id :integer not null, primary key +# name :string +# created_at :datetime not null +# updated_at :datetime not null +# contest_id :integer not null +# +# Indexes +# +# index_categories_on_contest_id (contest_id) +# +# Foreign Keys +# +# contest_id (contest_id => contests.id) +# +require 'rails_helper' + +RSpec.describe Category, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end