diff --git a/app/controllers/completions_controller.rb b/app/controllers/completions_controller.rb index 810a700..6cf9f2e 100644 --- a/app/controllers/completions_controller.rb +++ b/app/controllers/completions_controller.rb @@ -42,7 +42,7 @@ class CompletionsController < ApplicationController if @contestant && !params[:completion].key?(:message_id) redirect_to edit_contest_contestant_path(@contest, @contestant), notice: t("completions.new.notice") else - redirect_to @contest, notice: t("completions.new.notice") + redirect_to contest_messages_path(@contest), notice: t("completions.new.notice") end else if params[:completion].key?(:message_id) diff --git a/app/controllers/contestants_controller.rb b/app/controllers/contestants_controller.rb index 3430adc..185d796 100644 --- a/app/controllers/contestants_controller.rb +++ b/app/controllers/contestants_controller.rb @@ -3,6 +3,19 @@ class ContestantsController < ApplicationController before_action :set_contestant, only: %i[ destroy edit update] before_action :set_completions, only: %i[edit update ] + def index + authorize @contest + + @title = @contest.name + @contestants = @contest.contestants.sort_by { |contestant| [ + -contestant.completions.where(remaining_pieces: nil).size, + (contestant.completions.where(remaining_pieces: nil).size == @contest.puzzles.length ? 1 : 0) * contestant.time_seconds, + contestant.completions.size > 0 && contestant.completions[-1].remaining_pieces ? contestant.completions[-1].remaining_pieces : 1000000, + contestant.time_seconds + ] } + filter_contestants_per_category + end + def edit authorize @contest @@ -158,4 +171,14 @@ class ContestantsController < ApplicationController end @contestant.save end + + def filter_contestants_per_category + if params.key?(:category) && params[:category] != "-1" + if params[:category] == "-2" + @contestants = @contestants.select { |contestant| contestant.categories.size == 0 } + else + @contestants = @contestants.select { |contestant| contestant.categories.where(id: params[:category]).any? } + end + end + end end diff --git a/app/controllers/contests_controller.rb b/app/controllers/contests_controller.rb index 8608d51..c2c5102 100644 --- a/app/controllers/contests_controller.rb +++ b/app/controllers/contests_controller.rb @@ -16,19 +16,7 @@ class ContestsController < ApplicationController def show authorize @contest - @title = I18n.t("contests.show.title", name: @contest.name) - @action_name = t("helpers.buttons.settings") - @action_path = "/contests/#{@contest.id}/settings/general" - @contestants = @contest.contestants.sort_by { |contestant| [ - -contestant.completions.where(remaining_pieces: nil).size, - (contestant.completions.where(remaining_pieces: nil).size == @contest.puzzles.length ? 1 : 0) * contestant.time_seconds, - contestant.completions.size > 0 && contestant.completions[-1].remaining_pieces ? contestant.completions[-1].remaining_pieces : 1000000, - contestant.time_seconds - ] } - filter_contestants_per_category - @puzzles = @contest.puzzles.order(:id) - @messages = @contest.messages.order(:time_seconds) - set_badges + redirect_to contest_contestants_path(@contest) end def edit @@ -240,12 +228,6 @@ class ContestsController < ApplicationController @title = I18n.t("contests.scoreboard.title", name: @contest.name) end - def set_badges - @badges = [] - @badges.push(t("helpers.badges.team")) if @contest.team - @badges.push(t("helpers.badges.registration")) if @contest.allow_registration - end - def set_contest @contest = Contest.find(params[:id]) end diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb index c9a5848..916c807 100644 --- a/app/controllers/messages_controller.rb +++ b/app/controllers/messages_controller.rb @@ -5,7 +5,7 @@ class MessagesController < ApplicationController skip_before_action :require_authentication, only: %i[ create connect cors_preflight_check ] before_action :cors_set_access_control_headers, only: %i[ create connect cors_preflight_check ] - before_action :set_contest, only: %i[ convert destroy ] + before_action :set_contest, only: %i[ convert destroy index ] before_action :set_data, only: %i[ convert ] def self.local_prefixes @@ -68,6 +68,14 @@ class MessagesController < ApplicationController end end + def index + authorize @contest + + @title = @contest.name + @messages = @contest.messages.order(:time_seconds) + @puzzles = @contest.puzzles + end + def convert authorize @contest @@ -76,6 +84,7 @@ class MessagesController < ApplicationController @completion = Completion.new() @completion.display_time_from_start = @message.display_time + @completion.completed = true render "completions/new" end diff --git a/app/controllers/puzzles_controller.rb b/app/controllers/puzzles_controller.rb index 3ba86ca..1913ebc 100644 --- a/app/controllers/puzzles_controller.rb +++ b/app/controllers/puzzles_controller.rb @@ -2,6 +2,13 @@ class PuzzlesController < ApplicationController before_action :set_contest before_action :set_puzzle, only: %i[ destroy edit update] + def index + authorize @contest + + @title = @contest.name + @puzzles = @contest.puzzles.order(:id) + end + def edit authorize @contest diff --git a/app/policies/completion_policy.rb b/app/policies/completion_policy.rb index 9ed51d7..2aab862 100644 --- a/app/policies/completion_policy.rb +++ b/app/policies/completion_policy.rb @@ -1,9 +1,2 @@ class CompletionPolicy < ContestPolicy - def index? - false - end - - def show? - false - end end diff --git a/app/policies/contest_policy.rb b/app/policies/contest_policy.rb index 332185d..76761b4 100644 --- a/app/policies/contest_policy.rb +++ b/app/policies/contest_policy.rb @@ -1,30 +1,38 @@ class ContestPolicy < ApplicationPolicy + def owner_or_admin + if record == :contest + true + else + record.user.id == user.id || user.admin? + end + end + def index? - true + owner_or_admin end def show? - record.user.id == user.id || user.admin? + owner_or_admin end def new? - true + owner_or_admin end def create? - true + owner_or_admin end def convert? - record.user.id == user.id || user.admin? + owner_or_admin end def convert_csv? - record.user.id == user.id || user.admin? + owner_or_admin end def edit? - record.user.id == user.id || user.admin? + owner_or_admin end def settings_general_edit? @@ -48,23 +56,27 @@ class ContestPolicy < ApplicationPolicy end def finalize_import? - record.user.id == user.id || user.admin? + owner_or_admin end def update? - record.user.id == user.id || user.admin? + owner_or_admin end def destroy? - record.user.id == user.id || user.admin? + owner_or_admin end def import? - record.user.id == user.id || user.admin? + owner_or_admin end def export? - record.user.id == user.id || user.admin? + owner_or_admin + end + + def upload_csv? + owner_or_admin end def offline? @@ -94,8 +106,4 @@ class ContestPolicy < ApplicationPolicy def scoreboard? record.public end - - def upload_csv? - record.user.id == user.id || user.admin? - end end diff --git a/app/policies/contestant_policy.rb b/app/policies/contestant_policy.rb index 1b35919..e5ef1d0 100644 --- a/app/policies/contestant_policy.rb +++ b/app/policies/contestant_policy.rb @@ -1,9 +1,2 @@ class ContestantPolicy < ContestPolicy - def index? - false - end - - def show? - false - end end diff --git a/app/policies/puzzle_policy.rb b/app/policies/puzzle_policy.rb index 16e7645..16483f9 100644 --- a/app/policies/puzzle_policy.rb +++ b/app/policies/puzzle_policy.rb @@ -1,9 +1,2 @@ class PuzzlePolicy < ContestPolicy - def index? - false - end - - def show? - false - end end diff --git a/app/views/application/_contest_nav.html.slim b/app/views/application/_contest_nav.html.slim new file mode 100644 index 0000000..9534f86 --- /dev/null +++ b/app/views/application/_contest_nav.html.slim @@ -0,0 +1,44 @@ +javascript: + async function copyExtensionUrlToClipboard() { + await navigator.clipboard.writeText("#{message_url}?token=#{@contest.generate_token_for(:token)}"); + alert("#{t("contests.show.url_copied")}"); + } + +.row.mb-4 + .col + - if @contest.public + a.btn.btn-success href="/public/#{@contest.slug}" + = t("contests.show.open_public_scoreboard") + - else + a.btn.btn-success.disabled + = t("contests.show.public_scoreboard_disabled") + - if @contest.offline_form && @contest.puzzles.length < 2 + a.ms-3.btn.btn-success href="/public/#{@contest.slug}/offline" + = t("contests.show.open_offline_form") + - else + a.ms-3.btn.btn-success.disabled + = t("contests.show.offline_form_disabled") + button.btn.btn-success.ms-3 onclick="copyExtensionUrlToClipboard()" + css: + button > svg { + margin-right: 2px; + margin-top: -3px; + } + + + + + =< t("contests.show.copy_extension_url") + +.row + .col + ul.nav.nav-tabs.mb-4 + li.nav-item + a.nav-link class=active_page(contest_contestants_path(@contest)) href=contest_contestants_path(@contest) + = t("contestants.plural").capitalize + li.nav-item + a.nav-link class=active_page(contest_puzzles_path(@contest)) href=contest_puzzles_path(@contest) + = t("puzzles.plural").capitalize + li.nav-item + a.nav-link class=active_page(contest_messages_path(@contest)) href=contest_messages_path(@contest) + = t("messages.plural").capitalize \ No newline at end of file diff --git a/app/views/contestants/index.html.slim b/app/views/contestants/index.html.slim new file mode 100644 index 0000000..8d04bb8 --- /dev/null +++ b/app/views/contestants/index.html.slim @@ -0,0 +1,64 @@ += render "contest_nav" + +.row.mb-4 style="height: calc(100vh - 280px)" + .col.d-flex.flex-column style="height: 100%" + .row.mb-4 + .col + a.btn.btn-primary href=new_contest_contestant_path(@contest) style="margin-top: -3px" + | + #{t("helpers.buttons.add")} + a.ms-2.btn.btn.btn-primary href=contest_import_path(@contest) style="margin-top: -3px" + | #{t("helpers.buttons.import")} + a.ms-2.btn.btn.btn-primary href="/contests/#{@contest.id}/export.csv" style="margin-top: -3px" + | #{t("helpers.buttons.export")} + + - if @contest.categories.size > 0 + .row + .col + select.mt-2.mb-2 id="categories" style="padding: 5px" + option value=-1 + | Tous.tes les participant.e.s + option value=-2 + | Participant.e.s sans catégorie + - @contest.categories.each do |category| + option value=category.id + = category.name + javascript: + categorySelectEl = document.getElementById('categories'); + urlParams = new URLSearchParams(window.location.search); + selectedCategory = urlParams.get('category'); + Array.from(categorySelectEl.children).forEach((option) => { + if (option.value == selectedCategory) option.selected = true; + }); + categorySelectEl.addEventListener('change', (e) => { + window.location.replace(`#{contest_path(@contest)}?category=${e.target.value}`); + }) + .d-flex.flex-column style="overflow-y: auto" + table.table.table-striped.table-hover + thead + tr + th + = t("activerecord.attributes.contestant.name") + th + = t("activerecord.attributes.contestant.offline") + th + = t("activerecord.attributes.contestant.completions") + th + = t("activerecord.attributes.contestant.display_time") + tbody + - @contestants.each_with_index do |contestant, index| + tr scope="row" + td + = contestant.name + td + - if contestant.offline.present? + + + + + td + = contestant.completions.where(remaining_pieces: nil).length + td + = contestant.completions.size > 0 && contestant.completions[-1].remaining_pieces ? "#{contestant.completions.map{|completion| completion.puzzle.pieces}.sum - contestant.completions[-1].remaining_pieces}p" : contestant.display_time + td + a.btn.btn-sm.btn-secondary href=edit_contest_contestant_path(@contest, contestant) + = t("helpers.buttons.open") \ No newline at end of file diff --git a/app/views/contests/show.html.slim b/app/views/contests/show.html.slim deleted file mode 100644 index 8639f99..0000000 --- a/app/views/contests/show.html.slim +++ /dev/null @@ -1,180 +0,0 @@ -- if @badges.size > 0 && false - .row.mb-4 - .col - .badges style="margin-top: -18px; position: absolute" - - @badges.each do |badge| - span.badge.text-bg-info.me-2 - = badge - -javascript: - async function copyExtensionUrlToClipboard() { - await navigator.clipboard.writeText("#{message_url}?token=#{@contest.generate_token_for(:token)}"); - alert("#{t("contests.show.url_copied")}"); - } - -.row.mb-5 - .col - - if @contest.public - a.btn.btn-success href="/public/#{@contest.slug}" - = t("contests.show.open_public_scoreboard") - - else - a.btn.btn-success.disabled - = t("contests.show.public_scoreboard_disabled") - - if @contest.offline_form && @contest.puzzles.length < 2 - a.ms-3.btn.btn-success href="/public/#{@contest.slug}/offline" - = t("contests.show.open_offline_form") - - else - a.ms-3.btn.btn-success.disabled - = t("contests.show.offline_form_disabled") - button.btn.btn-success.ms-3 onclick="copyExtensionUrlToClipboard()" - css: - button > svg { - margin-right: 2px; - margin-top: -3px; - } - - - - - =< t("contests.show.copy_extension_url") - -.row.mb-4 style="height: calc(100vh - 280px)" - .col-6.d-flex.flex-column style="height: 100%" - .row - .col - h4 - = t("contestants.plural").capitalize - a.ms-3.btn.btn-sm.btn-primary href=new_contest_contestant_path(@contest) style="margin-top: -3px" - | + #{t("helpers.buttons.add")} - a.ms-2.btn.btn-sm.btn.btn-primary href=contest_import_path(@contest) style="margin-top: -3px" - | #{t("helpers.buttons.import")} - a.ms-2.btn.btn-sm.btn.btn-primary href="/contests/#{@contest.id}/export.csv" style="margin-top: -3px" - | #{t("helpers.buttons.export")} - - if @contest.categories.size > 0 - .row - .col - select.mt-2.mb-2 id="categories" style="padding: 5px" - option value=-1 - | Tous.tes les participant.e.s - option value=-2 - | Participant.e.s sans catégorie - - @contest.categories.each do |category| - option value=category.id - = category.name - javascript: - categorySelectEl = document.getElementById('categories'); - urlParams = new URLSearchParams(window.location.search); - selectedCategory = urlParams.get('category'); - Array.from(categorySelectEl.children).forEach((option) => { - if (option.value == selectedCategory) option.selected = true; - }); - categorySelectEl.addEventListener('change', (e) => { - window.location.replace(`#{contest_path(@contest)}?category=${e.target.value}`); - }) - .d-flex.flex-column style="overflow-y: auto" - table.table.table-striped.table-hover - thead - tr - th - = t("activerecord.attributes.contestant.name") - th - = t("activerecord.attributes.contestant.offline") - th - = t("activerecord.attributes.contestant.completions") - th - = t("activerecord.attributes.contestant.display_time") - tbody - - @contestants.each_with_index do |contestant, index| - tr scope="row" - td - = contestant.name - td - - if contestant.offline.present? - - - - - td - = contestant.completions.where(remaining_pieces: nil).length - td - = contestant.completions.size > 0 && contestant.completions[-1].remaining_pieces ? "#{contestant.completions.map{|completion| completion.puzzle.pieces}.sum - contestant.completions[-1].remaining_pieces}p" : contestant.display_time - td - a.btn.btn-sm.btn-secondary href=edit_contest_contestant_path(@contest, contestant) - = t("helpers.buttons.open") - .col-6.d-flex.flex-column style="height: 100%" - .row - .col - h4 - = t("puzzles.plural").capitalize - a.ms-3.btn.btn-sm.btn-primary href=new_contest_puzzle_path(@contest) style="margin-top: -3px" - | + #{t("helpers.buttons.add")} - table.table.table-striped.table-hover - thead - tr - th - = t("activerecord.attributes.puzzle.image") - th - = t("activerecord.attributes.puzzle.name") - th - = t("activerecord.attributes.puzzle.brand") - th - = t("activerecord.attributes.puzzle.pieces") - tbody - - @puzzles.each do |puzzle| - tr.align-middle scope="row" - td - = image_tag(puzzle.image, class: "img-fluid", style: "max-height: 48px;") if puzzle.image.attached? - td - = puzzle.name - td - = puzzle.brand - td - = puzzle.pieces - td - a.btn.btn-sm.btn-secondary href=edit_contest_puzzle_path(@contest, puzzle) - = t("helpers.buttons.edit") - - if @messages - .row.mt-5 - .col - h4 = t("messages.plural").capitalize - - if @puzzles.size == 0 - .row - .col.alert.alert-danger - = t("messages.warning") - .d-flex.flex-column style="overflow-y: auto" - table.table.table-striped.table-hover - thead - tr - th scope="col" style="white-space: nowrap" - = t("activerecord.attributes.message.processed") - th scope="col" - = t("activerecord.attributes.message.time") - th scope="col" - = t("activerecord.attributes.message.author") - th.w-25 scope="col" - = t("activerecord.attributes.message.text") - th.w-25 scope="col" - tbody - - @messages.each do |message| - tr.align-middle scope="row" - td style="text-align: center" - - if message.completions.size > 0 - - - - - td - = message.display_time - td - = message.author - td - = message.text - td - .d-inline-flex - - if @puzzles.size > 0 - a.btn.btn-sm.btn-secondary href=contest_message_convert_path(@contest, message) style="white-space: nowrap;" - = t("helpers.buttons.add_completion") - - else - a.btn.btn-sm.btn-secondary.disabled href=contest_message_convert_path(@contest, message) style="white-space: nowrap;" - = t("helpers.buttons.add_completion") - = link_to "x", contest_message_path(@contest, message), data: { turbo_method: :delete }, class: "btn btn-sm btn-danger ms-2" diff --git a/app/views/messages/index.html.slim b/app/views/messages/index.html.slim new file mode 100644 index 0000000..78b34e3 --- /dev/null +++ b/app/views/messages/index.html.slim @@ -0,0 +1,52 @@ += render "contest_nav" + +.row.mb-4 style="height: calc(100vh - 280px)" + .col.d-flex.flex-column style="height: 100%" + + .row.mb-4 + .col + - if @messages.length == 0 + .alert.alert-warning + = t("messages.index.no_messages") + - else + - if @puzzles.size == 0 + .row + .col.alert.alert-danger + = t("messages.warning") + .d-flex.flex-column style="overflow-y: auto" + table.table.table-striped.table-hover + thead + tr + th scope="col" style="white-space: nowrap" + = t("activerecord.attributes.message.processed") + th scope="col" + = t("activerecord.attributes.message.time") + th scope="col" + = t("activerecord.attributes.message.author") + th.w-25 scope="col" + = t("activerecord.attributes.message.text") + th.w-25 scope="col" + tbody + - @messages.each do |message| + tr.align-middle scope="row" + td style="text-align: center" + - if message.completions.size > 0 + + + + + td + = message.display_time + td + = message.author + td + = message.text + td + .d-inline-flex + - if @puzzles.size > 0 + a.btn.btn-sm.btn-secondary href=contest_message_convert_path(@contest, message) style="white-space: nowrap;" + = t("helpers.buttons.add_completion") + - else + a.btn.btn-sm.btn-secondary.disabled href=contest_message_convert_path(@contest, message) style="white-space: nowrap;" + = t("helpers.buttons.add_completion") + = link_to "x", contest_message_path(@contest, message), data: { turbo_method: :delete }, class: "btn btn-sm btn-danger ms-2" diff --git a/app/views/puzzles/index.html.slim b/app/views/puzzles/index.html.slim new file mode 100644 index 0000000..6f8d55d --- /dev/null +++ b/app/views/puzzles/index.html.slim @@ -0,0 +1,34 @@ += render "contest_nav" + +.row.mb-4 style="height: calc(100vh - 280px)" + .col.d-flex.flex-column style="height: 100%" + + .row.mb-4 + .col + a.btn.btn-primary href=new_contest_puzzle_path(@contest) style="margin-top: -3px" + | + #{t("helpers.buttons.add")} + table.table.table-striped.table-hover + thead + tr + th + = t("activerecord.attributes.puzzle.image") + th + = t("activerecord.attributes.puzzle.name") + th + = t("activerecord.attributes.puzzle.brand") + th + = t("activerecord.attributes.puzzle.pieces") + tbody + - @puzzles.each do |puzzle| + tr.align-middle scope="row" + td + = image_tag(puzzle.image, class: "img-fluid", style: "max-height: 128px;") if puzzle.image.attached? + td + = puzzle.name + td + = puzzle.brand + td + = puzzle.pieces + td + a.btn.btn-sm.btn-secondary href=edit_contest_puzzle_path(@contest, puzzle) + = t("helpers.buttons.edit") \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index dba687f..7828c94 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -260,6 +260,8 @@ en: none: No field selected rank: Rank messages: + index: + no_messages: No messages received yet convert: title: New completion destroy: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index cd5ffea..bb6b5a7 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -231,6 +231,8 @@ fr: none: Aucun champ sélectionné rank: Rang messages: + index: + no_messages: Pas de messages reçus pour le moment convert: title: Ajout d'une complétion destroy: diff --git a/config/routes.rb b/config/routes.rb index 908ffa3..8ae2cf6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -18,7 +18,7 @@ Rails.application.routes.draw do resources :completions resources :contestants resources :puzzles - resources :messages, only: :destroy do + resources :messages, only: [ :destroy, :index ] do get "convert", to: "messages#convert" end get "import", to: "contestants#import"