From dae86bd1eabd305d8b41f1bce57a7c786666a587 Mon Sep 17 00:00:00 2001 From: sto Date: Sun, 6 Sep 2026 10:07:47 +0200 Subject: [PATCH] Worlds Bets --- .vscode/settings.json | 3 + app/controllers/worlds_controller.rb | 135 +++++++++ app/helpers/worlds_helper.rb | 2 + app/lib/flags.rb | 261 ++++++++++++++++++ app/models/world_bet.rb | 93 +++++++ app/models/world_country.rb | 24 ++ app/models/world_participant.rb | 56 ++++ app/policies/world_bet_policy.rb | 29 ++ app/views/application/_worlds_nav.html.slim | 15 + app/views/worlds/about.html.slim | 83 ++++++ app/views/worlds/collab.html.slim | 131 +++++++++ app/views/worlds/main.html.slim | 52 ++++ app/views/worlds/new.html.slim | 45 +++ app/views/worlds/view.html.slim | 49 ++++ config/locales/en.yml | 53 ++++ config/locales/fr.yml | 55 ++++ config/routes.rb | 9 + .../20260904063514_create_world_countries.rb | 10 + ...0260904063926_create_world_participants.rb | 14 + ...04065915_add_round_to_world_participant.rb | 5 + .../20260904131402_create_world_bets.rb | 11 + ...04132641_add_score_to_world_participant.rb | 5 + ...0905120054_add_world_score_to_world_bet.rb | 5 + ...905120106_add_french_score_to_world_bet.rb | 5 + db/schema.rb | 34 ++- spec/factories/world_bets.rb | 20 ++ spec/factories/world_countries.rb | 16 ++ spec/factories/world_participants.rb | 34 +++ spec/helpers/worlds_helper_spec.rb | 15 + spec/models/world_bet_spec.rb | 18 ++ spec/models/world_country_spec.rb | 15 + spec/models/world_participant_spec.rb | 29 ++ spec/requests/worlds_spec.rb | 7 + 33 files changed, 1337 insertions(+), 1 deletion(-) create mode 100644 .vscode/settings.json create mode 100644 app/controllers/worlds_controller.rb create mode 100644 app/helpers/worlds_helper.rb create mode 100644 app/lib/flags.rb create mode 100644 app/models/world_bet.rb create mode 100644 app/models/world_country.rb create mode 100644 app/models/world_participant.rb create mode 100644 app/policies/world_bet_policy.rb create mode 100644 app/views/application/_worlds_nav.html.slim create mode 100644 app/views/worlds/about.html.slim create mode 100644 app/views/worlds/collab.html.slim create mode 100644 app/views/worlds/main.html.slim create mode 100644 app/views/worlds/new.html.slim create mode 100644 app/views/worlds/view.html.slim create mode 100644 db/migrate/20260904063514_create_world_countries.rb create mode 100644 db/migrate/20260904063926_create_world_participants.rb create mode 100644 db/migrate/20260904065915_add_round_to_world_participant.rb create mode 100644 db/migrate/20260904131402_create_world_bets.rb create mode 100644 db/migrate/20260904132641_add_score_to_world_participant.rb create mode 100644 db/migrate/20260905120054_add_world_score_to_world_bet.rb create mode 100644 db/migrate/20260905120106_add_french_score_to_world_bet.rb create mode 100644 spec/factories/world_bets.rb create mode 100644 spec/factories/world_countries.rb create mode 100644 spec/factories/world_participants.rb create mode 100644 spec/helpers/worlds_helper_spec.rb create mode 100644 spec/models/world_bet_spec.rb create mode 100644 spec/models/world_country_spec.rb create mode 100644 spec/models/world_participant_spec.rb create mode 100644 spec/requests/worlds_spec.rb diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0a77011 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.tabSize": 2 +} \ No newline at end of file diff --git a/app/controllers/worlds_controller.rb b/app/controllers/worlds_controller.rb new file mode 100644 index 0000000..e5dba40 --- /dev/null +++ b/app/controllers/worlds_controller.rb @@ -0,0 +1,135 @@ +class WorldsController < ApplicationController + allow_unauthenticated_access + + before_action :set_title + before_action :set_fr_lang + + def set_title + @title = I18n.t("worlds.title") + @nonav = true + end + + def set_fr_lang + I18n.locale = "fr" + end + + def main + authorize :world_bet + + @world_bets_w = WorldBet.all.order(world_score: :desc) + @world_bets_f = WorldBet.all.order(french_score: :desc) + end + + def about + authorize :world_bet + end + + def collab + authorize :world_bet + + participant_ids = Set[] + @world_bets = WorldBet.all.each do |b| + b.bet.split(";", -1).first.split(",", -1).each do |participant_id| + participant_ids.add(participant_id) + end + b.bet.split(";", -1).last.split(",", -1).each do |participant_id| + participant_ids.add(participant_id) + end + end + + @participants = WorldParticipant.includes([ :world_country ]).find(participant_ids.to_a).sort_by { |p| p.name } + end + + def view + authorize :world_bet + + @world_bet = WorldBet.find(params[:bet_id]) + @french_participants = WorldParticipant.includes([ :world_country ]).find(@world_bet.bet.split(";", -1).last.split(",", -1).map { |x| Integer(x) }) + @world_participants = WorldParticipant.includes([ :world_country ]).find(@world_bet.bet.split(";", -1).first.split(",", -1).map { |x| Integer(x) }) + end + + def new + authorize :world_bet + + @world_bet = WorldBet.new + @world_participants = WorldParticipant.includes([ :world_country ]).all.order(:name) + france = WorldCountry.where(code: "FR").first + @french_participants = WorldParticipant.includes([ :world_country ]).where(world_country: france).order(:name) + end + + def create + authorize :world_bet + + + @world_bet = WorldBet.new(world_bet_params) + @world_bet.score = 0 + @world_bet.french_score = 0 + @world_bet.world_score = 0 + all_params = params.require(:world_bet) + + w_set = Set[] + f_set = Set[] + @w_array = [] + @f_array = [] + (1..10).each do |i| + if all_params.key?("w_bet_#{i}") + p_id = all_params["w_bet_#{i}"] + w_set.add(p_id) + @w_array.push(p_id) + end + if all_params.key?("f_bet_#{i}") + p_id = all_params["f_bet_#{i}"] + f_set.add(p_id) + @f_array.push(p_id) + end + end + @world_bet.serialize_bet(@w_array, @f_array) + + if w_set.length != 10 || f_set.length != 10 + @world_bet.errors.add(:name, I18n.t("activerecord.errors.models.world_bet.attributes.bet.duplicate")) + end + + if w_set.length == 10 && f_set.length == 10 && @world_bet.save + redirect_to "/wjpc_bets/main", notice: t("worlds.new.notice") + else + @world_participants = WorldParticipant.all.sort_by { |p| p.name } + france = WorldCountry.where(code: "FR").first + @french_participants = WorldParticipant.where(world_country: france).sort_by { |p| p.name } + render :new, status: :unprocessable_entity + end + end + + def update + authorize :world_bet + + @participant = WorldParticipant.find(params[:participant_id]) + if @participant.update(world_participant_params) + participants_hash = {} + WorldParticipant.all.each do |p| + if p.score != nil + participants_hash[p.id] = p.score + end + end + WorldBet.all.each do |b| + old_score = b.score + b.compute_score(participants_hash) + if b.score != old_score + b.save + end + end + redirect_back(fallback_location: root_path, notice: t("worlds.collab.notice")) + else + redirect_to "/wjpc_bets/collab", notice: t("worlds.collab.error") + end + end + + private + + def world_bet_params + params.expect(world_bet: [ :name ]) + end + + def world_participant_params + params.expect(world_participant: [ :qualifier_result, :semifinal_result, :final_result ]) + end +end diff --git a/app/helpers/worlds_helper.rb b/app/helpers/worlds_helper.rb new file mode 100644 index 0000000..a1429c0 --- /dev/null +++ b/app/helpers/worlds_helper.rb @@ -0,0 +1,2 @@ +module WorldsHelper +end diff --git a/app/lib/flags.rb b/app/lib/flags.rb new file mode 100644 index 0000000..184b563 --- /dev/null +++ b/app/lib/flags.rb @@ -0,0 +1,261 @@ +module Flags + FLAGS = { + AD: "๐Ÿ‡ฆ๐Ÿ‡ฉ", + AE: "๐Ÿ‡ฆ๐Ÿ‡ช", + AF: "๐Ÿ‡ฆ๐Ÿ‡ซ", + AG: "๐Ÿ‡ฆ๐Ÿ‡ฌ", + AI: "๐Ÿ‡ฆ๐Ÿ‡ฎ", + AL: "๐Ÿ‡ฆ๐Ÿ‡ฑ", + AM: "๐Ÿ‡ฆ๐Ÿ‡ฒ", + AO: "๐Ÿ‡ฆ๐Ÿ‡ด", + AQ: "๐Ÿ‡ฆ๐Ÿ‡ถ", + AR: "๐Ÿ‡ฆ๐Ÿ‡ท", + AS: "๐Ÿ‡ฆ๐Ÿ‡ธ", + AT: "๐Ÿ‡ฆ๐Ÿ‡น", + AU: "๐Ÿ‡ฆ๐Ÿ‡บ", + AW: "๐Ÿ‡ฆ๐Ÿ‡ผ", + AX: "๐Ÿ‡ฆ๐Ÿ‡ฝ", + AZ: "๐Ÿ‡ฆ๐Ÿ‡ฟ", + BA: "๐Ÿ‡ง๐Ÿ‡ฆ", + BB: "๐Ÿ‡ง๐Ÿ‡ง", + BD: "๐Ÿ‡ง๐Ÿ‡ฉ", + BE: "๐Ÿ‡ง๐Ÿ‡ช", + BF: "๐Ÿ‡ง๐Ÿ‡ซ", + BG: "๐Ÿ‡ง๐Ÿ‡ฌ", + BH: "๐Ÿ‡ง๐Ÿ‡ญ", + BI: "๐Ÿ‡ง๐Ÿ‡ฎ", + BJ: "๐Ÿ‡ง๐Ÿ‡ฏ", + BL: "๐Ÿ‡ง๐Ÿ‡ฑ", + BM: "๐Ÿ‡ง๐Ÿ‡ฒ", + BN: "๐Ÿ‡ง๐Ÿ‡ณ", + BO: "๐Ÿ‡ง๐Ÿ‡ด", + BQ: "๐Ÿ‡ง๐Ÿ‡ถ", + BR: "๐Ÿ‡ง๐Ÿ‡ท", + BS: "๐Ÿ‡ง๐Ÿ‡ธ", + BT: "๐Ÿ‡ง๐Ÿ‡น", + BV: "๐Ÿ‡ง๐Ÿ‡ป", + BW: "๐Ÿ‡ง๐Ÿ‡ผ", + BY: "๐Ÿ‡ง๐Ÿ‡พ", + BZ: "๐Ÿ‡ง๐Ÿ‡ฟ", + CA: "๐Ÿ‡จ๐Ÿ‡ฆ", + CC: "๐Ÿ‡จ๐Ÿ‡จ", + CD: "๐Ÿ‡จ๐Ÿ‡ฉ", + CF: "๐Ÿ‡จ๐Ÿ‡ซ", + CG: "๐Ÿ‡จ๐Ÿ‡ฌ", + CH: "๐Ÿ‡จ๐Ÿ‡ญ", + CI: "๐Ÿ‡จ๐Ÿ‡ฎ", + CK: "๐Ÿ‡จ๐Ÿ‡ฐ", + CL: "๐Ÿ‡จ๐Ÿ‡ฑ", + CM: "๐Ÿ‡จ๐Ÿ‡ฒ", + CN: "๐Ÿ‡จ๐Ÿ‡ณ", + CO: "๐Ÿ‡จ๐Ÿ‡ด", + CR: "๐Ÿ‡จ๐Ÿ‡ท", + CU: "๐Ÿ‡จ๐Ÿ‡บ", + CV: "๐Ÿ‡จ๐Ÿ‡ป", + CW: "๐Ÿ‡จ๐Ÿ‡ผ", + CX: "๐Ÿ‡จ๐Ÿ‡ฝ", + CY: "๐Ÿ‡จ๐Ÿ‡พ", + CZ: "๐Ÿ‡จ๐Ÿ‡ฟ", + DE: "๐Ÿ‡ฉ๐Ÿ‡ช", + DJ: "๐Ÿ‡ฉ๐Ÿ‡ฏ", + DK: "๐Ÿ‡ฉ๐Ÿ‡ฐ", + DM: "๐Ÿ‡ฉ๐Ÿ‡ฒ", + DO: "๐Ÿ‡ฉ๐Ÿ‡ด", + DZ: "๐Ÿ‡ฉ๐Ÿ‡ฟ", + EC: "๐Ÿ‡ช๐Ÿ‡จ", + EE: "๐Ÿ‡ช๐Ÿ‡ช", + EG: "๐Ÿ‡ช๐Ÿ‡ฌ", + EH: "๐Ÿ‡ช๐Ÿ‡ญ", + ER: "๐Ÿ‡ช๐Ÿ‡ท", + ES: "๐Ÿ‡ช๐Ÿ‡ธ", + ET: "๐Ÿ‡ช๐Ÿ‡น", + FI: "๐Ÿ‡ซ๐Ÿ‡ฎ", + FJ: "๐Ÿ‡ซ๐Ÿ‡ฏ", + FK: "๐Ÿ‡ซ๐Ÿ‡ฐ", + FM: "๐Ÿ‡ซ๐Ÿ‡ฒ", + FO: "๐Ÿ‡ซ๐Ÿ‡ด", + FR: "๐Ÿ‡ซ๐Ÿ‡ท", + GA: "๐Ÿ‡ฌ๐Ÿ‡ฆ", + GB: "๐Ÿ‡ฌ๐Ÿ‡ง", + GD: "๐Ÿ‡ฌ๐Ÿ‡ฉ", + GE: "๐Ÿ‡ฌ๐Ÿ‡ช", + GF: "๐Ÿ‡ฌ๐Ÿ‡ซ", + GG: "๐Ÿ‡ฌ๐Ÿ‡ฌ", + GH: "๐Ÿ‡ฌ๐Ÿ‡ญ", + GI: "๐Ÿ‡ฌ๐Ÿ‡ฎ", + GL: "๐Ÿ‡ฌ๐Ÿ‡ฑ", + GM: "๐Ÿ‡ฌ๐Ÿ‡ฒ", + GN: "๐Ÿ‡ฌ๐Ÿ‡ณ", + GP: "๐Ÿ‡ฌ๐Ÿ‡ต", + GQ: "๐Ÿ‡ฌ๐Ÿ‡ถ", + GR: "๐Ÿ‡ฌ๐Ÿ‡ท", + GS: "๐Ÿ‡ฌ๐Ÿ‡ธ", + GT: "๐Ÿ‡ฌ๐Ÿ‡น", + GU: "๐Ÿ‡ฌ๐Ÿ‡บ", + GW: "๐Ÿ‡ฌ๐Ÿ‡ผ", + GY: "๐Ÿ‡ฌ๐Ÿ‡พ", + HK: "๐Ÿ‡ญ๐Ÿ‡ฐ", + HM: "๐Ÿ‡ญ๐Ÿ‡ฒ", + HN: "๐Ÿ‡ญ๐Ÿ‡ณ", + HR: "๐Ÿ‡ญ๐Ÿ‡ท", + HT: "๐Ÿ‡ญ๐Ÿ‡น", + HU: "๐Ÿ‡ญ๐Ÿ‡บ", + ID: "๐Ÿ‡ฎ๐Ÿ‡ฉ", + IE: "๐Ÿ‡ฎ๐Ÿ‡ช", + IL: "๐Ÿ‡ฎ๐Ÿ‡ฑ", + IM: "๐Ÿ‡ฎ๐Ÿ‡ฒ", + IN: "๐Ÿ‡ฎ๐Ÿ‡ณ", + IO: "๐Ÿ‡ฎ๐Ÿ‡ด", + IQ: "๐Ÿ‡ฎ๐Ÿ‡ถ", + IR: "๐Ÿ‡ฎ๐Ÿ‡ท", + IS: "๐Ÿ‡ฎ๐Ÿ‡ธ", + IT: "๐Ÿ‡ฎ๐Ÿ‡น", + JE: "๐Ÿ‡ฏ๐Ÿ‡ช", + JM: "๐Ÿ‡ฏ๐Ÿ‡ฒ", + JO: "๐Ÿ‡ฏ๐Ÿ‡ด", + JP: "๐Ÿ‡ฏ๐Ÿ‡ต", + KE: "๐Ÿ‡ฐ๐Ÿ‡ช", + KG: "๐Ÿ‡ฐ๐Ÿ‡ฌ", + KH: "๐Ÿ‡ฐ๐Ÿ‡ญ", + KI: "๐Ÿ‡ฐ๐Ÿ‡ฎ", + KM: "๐Ÿ‡ฐ๐Ÿ‡ฒ", + KN: "๐Ÿ‡ฐ๐Ÿ‡ณ", + KP: "๐Ÿ‡ฐ๐Ÿ‡ต", + KR: "๐Ÿ‡ฐ๐Ÿ‡ท", + KW: "๐Ÿ‡ฐ๐Ÿ‡ผ", + KY: "๐Ÿ‡ฐ๐Ÿ‡พ", + KZ: "๐Ÿ‡ฐ๐Ÿ‡ฟ", + LA: "๐Ÿ‡ฑ๐Ÿ‡ฆ", + LB: "๐Ÿ‡ฑ๐Ÿ‡ง", + LC: "๐Ÿ‡ฑ๐Ÿ‡จ", + LI: "๐Ÿ‡ฑ๐Ÿ‡ฎ", + LK: "๐Ÿ‡ฑ๐Ÿ‡ฐ", + LR: "๐Ÿ‡ฑ๐Ÿ‡ท", + LS: "๐Ÿ‡ฑ๐Ÿ‡ธ", + LT: "๐Ÿ‡ฑ๐Ÿ‡น", + LU: "๐Ÿ‡ฑ๐Ÿ‡บ", + LV: "๐Ÿ‡ฑ๐Ÿ‡ป", + LY: "๐Ÿ‡ฑ๐Ÿ‡พ", + MA: "๐Ÿ‡ฒ๐Ÿ‡ฆ", + MC: "๐Ÿ‡ฒ๐Ÿ‡จ", + MD: "๐Ÿ‡ฒ๐Ÿ‡ฉ", + ME: "๐Ÿ‡ฒ๐Ÿ‡ช", + MF: "๐Ÿ‡ฒ๐Ÿ‡ซ", + MG: "๐Ÿ‡ฒ๐Ÿ‡ฌ", + MH: "๐Ÿ‡ฒ๐Ÿ‡ญ", + MK: "๐Ÿ‡ฒ๐Ÿ‡ฐ", + ML: "๐Ÿ‡ฒ๐Ÿ‡ฑ", + MM: "๐Ÿ‡ฒ๐Ÿ‡ฒ", + MN: "๐Ÿ‡ฒ๐Ÿ‡ณ", + MO: "๐Ÿ‡ฒ๐Ÿ‡ด", + MP: "๐Ÿ‡ฒ๐Ÿ‡ต", + MQ: "๐Ÿ‡ฒ๐Ÿ‡ถ", + MR: "๐Ÿ‡ฒ๐Ÿ‡ท", + MS: "๐Ÿ‡ฒ๐Ÿ‡ธ", + MT: "๐Ÿ‡ฒ๐Ÿ‡น", + MU: "๐Ÿ‡ฒ๐Ÿ‡บ", + MV: "๐Ÿ‡ฒ๐Ÿ‡ป", + MW: "๐Ÿ‡ฒ๐Ÿ‡ผ", + MX: "๐Ÿ‡ฒ๐Ÿ‡ฝ", + MY: "๐Ÿ‡ฒ๐Ÿ‡พ", + MZ: "๐Ÿ‡ฒ๐Ÿ‡ฟ", + NA: "๐Ÿ‡ณ๐Ÿ‡ฆ", + NC: "๐Ÿ‡ณ๐Ÿ‡จ", + NE: "๐Ÿ‡ณ๐Ÿ‡ช", + NF: "๐Ÿ‡ณ๐Ÿ‡ซ", + NG: "๐Ÿ‡ณ๐Ÿ‡ฌ", + NI: "๐Ÿ‡ณ๐Ÿ‡ฎ", + NL: "๐Ÿ‡ณ๐Ÿ‡ฑ", + NO: "๐Ÿ‡ณ๐Ÿ‡ด", + NP: "๐Ÿ‡ณ๐Ÿ‡ต", + NR: "๐Ÿ‡ณ๐Ÿ‡ท", + NU: "๐Ÿ‡ณ๐Ÿ‡บ", + NZ: "๐Ÿ‡ณ๐Ÿ‡ฟ", + OM: "๐Ÿ‡ด๐Ÿ‡ฒ", + PA: "๐Ÿ‡ต๐Ÿ‡ฆ", + PE: "๐Ÿ‡ต๐Ÿ‡ช", + PF: "๐Ÿ‡ต๐Ÿ‡ซ", + PG: "๐Ÿ‡ต๐Ÿ‡ฌ", + PH: "๐Ÿ‡ต๐Ÿ‡ญ", + PK: "๐Ÿ‡ต๐Ÿ‡ฐ", + PL: "๐Ÿ‡ต๐Ÿ‡ฑ", + PM: "๐Ÿ‡ต๐Ÿ‡ฒ", + PN: "๐Ÿ‡ต๐Ÿ‡ณ", + PR: "๐Ÿ‡ต๐Ÿ‡ท", + PS: "๐Ÿ‡ต๐Ÿ‡ธ", + PT: "๐Ÿ‡ต๐Ÿ‡น", + PW: "๐Ÿ‡ต๐Ÿ‡ผ", + PY: "๐Ÿ‡ต๐Ÿ‡พ", + QA: "๐Ÿ‡ถ๐Ÿ‡ฆ", + RE: "๐Ÿ‡ท๐Ÿ‡ช", + RO: "๐Ÿ‡ท๐Ÿ‡ด", + RS: "๐Ÿ‡ท๐Ÿ‡ธ", + RU: "๐Ÿ‡ท๐Ÿ‡บ", + RW: "๐Ÿ‡ท๐Ÿ‡ผ", + SA: "๐Ÿ‡ธ๐Ÿ‡ฆ", + SB: "๐Ÿ‡ธ๐Ÿ‡ง", + SC: "๐Ÿ‡ธ๐Ÿ‡จ", + SD: "๐Ÿ‡ธ๐Ÿ‡ฉ", + SE: "๐Ÿ‡ธ๐Ÿ‡ช", + SG: "๐Ÿ‡ธ๐Ÿ‡ฌ", + SH: "๐Ÿ‡ธ๐Ÿ‡ญ", + SI: "๐Ÿ‡ธ๐Ÿ‡ฎ", + SJ: "๐Ÿ‡ธ๐Ÿ‡ฏ", + SK: "๐Ÿ‡ธ๐Ÿ‡ฐ", + SL: "๐Ÿ‡ธ๐Ÿ‡ฑ", + SM: "๐Ÿ‡ธ๐Ÿ‡ฒ", + SN: "๐Ÿ‡ธ๐Ÿ‡ณ", + SO: "๐Ÿ‡ธ๐Ÿ‡ด", + SR: "๐Ÿ‡ธ๐Ÿ‡ท", + SS: "๐Ÿ‡ธ๐Ÿ‡ธ", + ST: "๐Ÿ‡ธ๐Ÿ‡น", + SV: "๐Ÿ‡ธ๐Ÿ‡ป", + SX: "๐Ÿ‡ธ๐Ÿ‡ฝ", + SY: "๐Ÿ‡ธ๐Ÿ‡พ", + SZ: "๐Ÿ‡ธ๐Ÿ‡ฟ", + TC: "๐Ÿ‡น๐Ÿ‡จ", + TD: "๐Ÿ‡น๐Ÿ‡ฉ", + TF: "๐Ÿ‡น๐Ÿ‡ซ", + TG: "๐Ÿ‡น๐Ÿ‡ฌ", + TH: "๐Ÿ‡น๐Ÿ‡ญ", + TJ: "๐Ÿ‡น๐Ÿ‡ฏ", + TK: "๐Ÿ‡น๐Ÿ‡ฐ", + TL: "๐Ÿ‡น๐Ÿ‡ฑ", + TM: "๐Ÿ‡น๐Ÿ‡ฒ", + TN: "๐Ÿ‡น๐Ÿ‡ณ", + TO: "๐Ÿ‡น๐Ÿ‡ด", + TR: "๐Ÿ‡น๐Ÿ‡ท", + TT: "๐Ÿ‡น๐Ÿ‡น", + TV: "๐Ÿ‡น๐Ÿ‡ป", + TW: "๐Ÿ‡น๐Ÿ‡ผ", + TZ: "๐Ÿ‡น๐Ÿ‡ฟ", + UA: "๐Ÿ‡บ๐Ÿ‡ฆ", + UG: "๐Ÿ‡บ๐Ÿ‡ฌ", + UM: "๐Ÿ‡บ๐Ÿ‡ฒ", + US: "๐Ÿ‡บ๐Ÿ‡ธ", + UY: "๐Ÿ‡บ๐Ÿ‡พ", + UZ: "๐Ÿ‡บ๐Ÿ‡ฟ", + VA: "๐Ÿ‡ป๐Ÿ‡ฆ", + VC: "๐Ÿ‡ป๐Ÿ‡จ", + VE: "๐Ÿ‡ป๐Ÿ‡ช", + VG: "๐Ÿ‡ป๐Ÿ‡ฌ", + VI: "๐Ÿ‡ป๐Ÿ‡ฎ", + VN: "๐Ÿ‡ป๐Ÿ‡ณ", + VU: "๐Ÿ‡ป๐Ÿ‡บ", + WF: "๐Ÿ‡ผ๐Ÿ‡ซ", + WS: "๐Ÿ‡ผ๐Ÿ‡ธ", + YE: "๐Ÿ‡พ๐Ÿ‡ช", + YT: "๐Ÿ‡พ๐Ÿ‡น", + ZA: "๐Ÿ‡ฟ๐Ÿ‡ฆ", + ZM: "๐Ÿ‡ฟ๐Ÿ‡ฒ", + ZW: "๐Ÿ‡ฟ๐Ÿ‡ผ" + } + + def self.get_flag(code) + sym_code = code.to_sym + if FLAGS.has_key?(sym_code) + return FLAGS[sym_code] + end + "" + end +end diff --git a/app/models/world_bet.rb b/app/models/world_bet.rb new file mode 100644 index 0000000..2d498a4 --- /dev/null +++ b/app/models/world_bet.rb @@ -0,0 +1,93 @@ +# == Schema Information +# +# Table name: world_bets +# +# id :integer not null, primary key +# bet :string +# french_score :integer +# name :string +# score :integer +# world_score :integer +# created_at :datetime not null +# updated_at :datetime not null +# +class WorldBet < ApplicationRecord + validates :name, presence: true, uniqueness: true + validates :score, presence: true, numericality: { greater_than_or_equal_to: 0 } + validates :french_score, presence: true, numericality: { greater_than_or_equal_to: 0 } + validates :world_score, presence: true, numericality: { greater_than_or_equal_to: 0 } + + @@multiplier_array = [ 20, 15, 12, 10, 8, 6, 4, 3, 2, 1 ] + @@points_array = [ 100, 70, 50, 40, 30, 26, 23, 20, 17, 15 ] + + def self.multiplier(i) + if i < 0 or i > 9 + return 0 + end + @@multiplier_array[i] + end + + def self.points(i) + if !i || i < 1 + return 0 + end + if i <= 10 + return @@points_array[i - 1] + end + if i <= 20 + return 12 + end + if i <= 30 + return 10 + end + if i <= 40 + return 8 + end + if i <= 50 + return 7 + end + if i <= 60 + return 6 + end + if i <= 70 + return 5 + end + if i <= 80 + return 4 + end + if i <= 90 + return 3 + end + if i <= 100 + return 2 + end + if i <= 150 + return 1 + end + 0 + end + + def serialize_bet(w, f) + self.bet = "#{w.join(',')};#{f.join(',')}" + end + + def compute_score(participant_scores) + self.score = 0 + self.french_score = 0 + self.world_score = 0 + w = self.bet.split(";", -1).first.split(",", -1) + w.each_with_index do |participant_id, i| + if participant_scores.has_key?(Integer(participant_id)) + self.score += participant_scores[Integer(participant_id)] * WorldBet.multiplier(i) + self.world_score += participant_scores[Integer(participant_id)] * WorldBet.multiplier(i) + end + end + f = self.bet.split(";", -1).last.split(",", -1) + f.each_with_index do |participant_id, i| + if participant_scores.has_key?(Integer(participant_id)) + self.score += participant_scores[Integer(participant_id)] * WorldBet.multiplier(i) + self.french_score += participant_scores[Integer(participant_id)] * WorldBet.multiplier(i) + end + end + end +end diff --git a/app/models/world_country.rb b/app/models/world_country.rb new file mode 100644 index 0000000..cf7ea8f --- /dev/null +++ b/app/models/world_country.rb @@ -0,0 +1,24 @@ +# == Schema Information +# +# Table name: world_countries +# +# id :integer not null, primary key +# code :string not null +# name :string not null +# created_at :datetime not null +# updated_at :datetime not null +# +class WorldCountry < ApplicationRecord + has_many :world_participants + + validates :code, presence: true, uniqueness: true + validates :name, presence: true, uniqueness: true + + def self.add_countries(str) + str.split(";", -1).each do |c| + name = c.split(",", -1).first + code = c.split(",", -1).last + WorldCountry.create(name: name, code: code) + end + end +end diff --git a/app/models/world_participant.rb b/app/models/world_participant.rb new file mode 100644 index 0000000..d515d47 --- /dev/null +++ b/app/models/world_participant.rb @@ -0,0 +1,56 @@ +# == Schema Information +# +# Table name: world_participants +# +# id :integer not null, primary key +# final_result :integer +# name :string not null +# qualifier_result :integer +# round :string not null +# score :integer +# semifinal_result :integer +# username :string not null +# created_at :datetime not null +# updated_at :datetime not null +# world_country_id :integer not null +# +# Indexes +# +# index_world_participants_on_world_country_id (world_country_id) +# +# Foreign Keys +# +# world_country_id (world_country_id => world_countries.id) +# +class WorldParticipant < ApplicationRecord + belongs_to :world_country + + validates :name, presence: true + validates :username, presence: true + validates :round, inclusion: { in: %w[a b c d], message: "%{value} is not a valid round" } + validates :qualifier_result, numericality: { greater_than_or_equal_to: 1 }, if: -> { qualifier_result.present? } + validates :semifinal_result, numericality: { greater_than_or_equal_to: 1 }, if: -> { semifinal_result.present? } + validates :final_result, numericality: { greater_than_or_equal_to: 1 }, if: -> { final_result.present? } + + before_save :compute_score + + def compute_score + self.score = WorldBet.points(qualifier_result) + WorldBet.points(semifinal_result) * 2 + WorldBet.points(final_result) * 4 + end + + def self.add_participants(str) + str.split(";", -1).each do |c| + cs = c.split(",", -1) + if cs.length >= 4 + name = cs.first + username = cs[1] + country_code = cs[2] + round = cs.last + country = WorldCountry.where(code: country_code).first + if country + WorldParticipant.create(name: name, username: username, world_country: country, round: round) + end + end + end + end +end diff --git a/app/policies/world_bet_policy.rb b/app/policies/world_bet_policy.rb new file mode 100644 index 0000000..dadef9a --- /dev/null +++ b/app/policies/world_bet_policy.rb @@ -0,0 +1,29 @@ +class WorldBetPolicy < ApplicationPolicy + def main? + true + end + + def about? + true + end + + def new? + true + end + + def create? + true + end + + def collab? + true + end + + def update? + true + end + + def view? + true + end +end diff --git a/app/views/application/_worlds_nav.html.slim b/app/views/application/_worlds_nav.html.slim new file mode 100644 index 0000000..357d496 --- /dev/null +++ b/app/views/application/_worlds_nav.html.slim @@ -0,0 +1,15 @@ +.row + .col + ul.nav.nav-tabs.mb-4 + li.nav-item + a.nav-link class=active_page("/wjpc_bets/main") href="/wjpc_bets/main" + = t("worlds.main.tab") + li.nav-item + a.nav-link class=active_page("/wjpc_bets/about") href="/wjpc_bets/about" + = t("worlds.about.tab") + li.nav-item + a.nav-link class=active_page("/wjpc_bets/collaborative_filling") href="/wjpc_bets/collaborative_filling" + = t("worlds.collab.tab") + li.nav-item + a.nav-link class=active_page("/wjpc_bets/new") href="/wjpc_bets/new" + = t("worlds.new.tab") \ No newline at end of file diff --git a/app/views/worlds/about.html.slim b/app/views/worlds/about.html.slim new file mode 100644 index 0000000..2ab988c --- /dev/null +++ b/app/views/worlds/about.html.slim @@ -0,0 +1,83 @@ += render "worlds_nav" + +.alert.alert-warning role="alert" + = t("worlds.about.one_bet_message") + +.alert.alert-primary role="alert" + = t("worlds.about.details_message") + .mt-3 + = t("worlds.about.two_rankings_message") + .mt-3 + = t("worlds.about.collaborative_filling_message") + +.row.mt-4 + .col + table.table.table-striped.table-hover + thead + tr + th = t("worlds.about.ranking") + th = t("worlds.about.qualifier_round") + th = t("worlds.about.semifinal_round") + th = t("worlds.about.finals_round") + tbody + - (1..10).each do |i| + tr + td #{i} + td #{WorldBet.points(i)} + td #{WorldBet.points(i) * 2} + td #{WorldBet.points(i) * 4} + tr + td 11-20 + td #{WorldBet.points(20)} + td #{WorldBet.points(20) * 2} + td #{WorldBet.points(20) * 4} + tr + td 21-30 + td #{WorldBet.points(30)} + td #{WorldBet.points(30) * 2} + td #{WorldBet.points(30) * 4} + tr + td 31-40 + td #{WorldBet.points(40)} + td #{WorldBet.points(40) * 2} + td #{WorldBet.points(40) * 4} + tr + td 41-50 + td #{WorldBet.points(50)} + td #{WorldBet.points(50) * 2} + td #{WorldBet.points(50) * 4} + tr + td 51-60 + td #{WorldBet.points(60)} + td #{WorldBet.points(60) * 2} + td #{WorldBet.points(60) * 4} + tr + td 61-70 + td #{WorldBet.points(70)} + td #{WorldBet.points(70) * 2} + td #{WorldBet.points(70) * 4} + tr + td 71-80 + td #{WorldBet.points(80)} + td #{WorldBet.points(80) * 2} + td #{WorldBet.points(80) * 4} + tr + td 81-90 + td #{WorldBet.points(90)} + td #{WorldBet.points(90) * 2} + td #{WorldBet.points(90) * 4} + tr + td 91-100 + td #{WorldBet.points(100)} + td #{WorldBet.points(100) * 2} + td #{WorldBet.points(100) * 4} + tr + td 101-150 + td #{WorldBet.points(150)} + td #{WorldBet.points(150) * 2} + td #{WorldBet.points(150) * 4} + tr + td 151+ + td #{WorldBet.points(151)} + td #{WorldBet.points(151) * 2} + td #{WorldBet.points(151) * 4} \ No newline at end of file diff --git a/app/views/worlds/collab.html.slim b/app/views/worlds/collab.html.slim new file mode 100644 index 0000000..e26d6b2 --- /dev/null +++ b/app/views/worlds/collab.html.slim @@ -0,0 +1,131 @@ += render "worlds_nav" + +.alert.alert-primary role="alert" + = t("worlds.collab.details_message") + .mt-3 + = t("worlds.collab.error_message") + +.alert.alert-warning role="alert" + = t("worlds.collab.rank_message") + +h4.mt-4 Round A + +- @participants.each do |p| + - if p.round == 'a' + .row.mt-3 + .col + = form_with model: WorldParticipant, url: "/wjpc_bets/update/#{p.id}", method: :patch do |form| + .input-group + .input-group-text style="width: 350px;" #{Flags.get_flag(p.world_country.code) + " " + p.name} + .input-group-text = t("worlds.main.rank") + = form.text_field :qualifier_result, value: p.qualifier_result, autocomplete: "off", class: "form-control", style: "color: orange; max-width: 60px;" + .input-group-text = t("worlds.main.points") + .input-group-text style="color: green; width: 60px;" #{WorldBet.points(p.qualifier_result)} + = form.submit t("helpers.buttons.validate"), class: "btn btn-primary", onclick: "updateScrollPos()" + +javascript: + function updateScrollPos() { + wsy = window.scrollY; + setTimeout(() => window.scrollTo(0, wsy), 25); + setTimeout(() => window.scrollTo(0, wsy), 50); + setTimeout(() => window.scrollTo(0, wsy), 75); + setTimeout(() => window.scrollTo(0, wsy), 100); + setTimeout(() => window.scrollTo(0, wsy), 125); + setTimeout(() => window.scrollTo(0, wsy), 150); + setTimeout(() => window.scrollTo(0, wsy), 175); + setTimeout(() => window.scrollTo(0, wsy), 200); + setTimeout(() => window.scrollTo(0, wsy), 225); + setTimeout(() => window.scrollTo(0, wsy), 250); + } + + +h4.mt-5 Round B + +- @participants.each do |p| + - if p.round == 'b' + .row.mt-3 + .col + = form_with model: WorldParticipant, url: "/wjpc_bets/update/#{p.id}", method: :patch do |form| + .input-group + .input-group-text style="width: 350px;" #{Flags.get_flag(p.world_country.code) + " " + p.name} + .input-group-text = t("worlds.main.rank") + = form.text_field :qualifier_result, value: p.qualifier_result, autocomplete: "off", class: "form-control", style: "color: orange; max-width: 60px;" + .input-group-text = t("worlds.main.points") + .input-group-text style="color: green; width: 60px;" #{WorldBet.points(p.qualifier_result)} + = form.submit t("helpers.buttons.validate"), class: "btn btn-primary", onclick: "updateScrollPos()" + +h4.mt-5 Round C + +- @participants.each do |p| + - if p.round == 'c' + .row.mt-3 + .col + = form_with model: WorldParticipant, url: "/wjpc_bets/update/#{p.id}", method: :patch do |form| + .input-group + .input-group-text style="width: 350px;" #{Flags.get_flag(p.world_country.code) + " " + p.name} + .input-group-text = t("worlds.main.rank") + = form.text_field :qualifier_result, value: p.qualifier_result, autocomplete: "off", class: "form-control", style: "color: orange; max-width: 60px;" + .input-group-text = t("worlds.main.points") + .input-group-text style="color: green; width: 60px;" #{WorldBet.points(p.qualifier_result)} + = form.submit t("helpers.buttons.validate"), class: "btn btn-primary", onclick: "updateScrollPos()" + +h4.mt-5 Round D + +- @participants.each do |p| + - if p.round == 'd' + .row.mt-3 + .col + = form_with model: WorldParticipant, url: "/wjpc_bets/update/#{p.id}", method: :patch do |form| + .input-group + .input-group-text style="width: 350px;" #{Flags.get_flag(p.world_country.code) + " " + p.name} + .input-group-text = t("worlds.main.rank") + = form.text_field :qualifier_result, value: p.qualifier_result, autocomplete: "off", class: "form-control", style: "color: orange; max-width: 60px;" + .input-group-text = t("worlds.main.points") + .input-group-text style="color: green; width: 60px;" #{WorldBet.points(p.qualifier_result)} + = form.submit t("helpers.buttons.validate"), class: "btn btn-primary", onclick: "updateScrollPos()" + +h4.mt-5 = "#{t("worlds.collab.semifinal")} 1" + +- @participants.each do |p| + - if p.round == 'a' || p.round == 'b' + .row.mt-3 + .col + = form_with model: WorldParticipant, url: "/wjpc_bets/update/#{p.id}", method: :patch do |form| + .input-group + .input-group-text style="width: 350px;" #{Flags.get_flag(p.world_country.code) + " " + p.name} + .input-group-text = t("worlds.main.rank") + = form.text_field :semifinal_result, value: p.semifinal_result, autocomplete: "off", class: "form-control", style: "color: orange; max-width: 60px;" + .input-group-text = t("worlds.main.points") + .input-group-text style="color: green; width: 60px;" #{WorldBet.points(p.semifinal_result) * 2} + = form.submit t("helpers.buttons.validate"), class: "btn btn-primary", onclick: "updateScrollPos()" + +h4.mt-5 = "#{t("worlds.collab.semifinal")} 2" + +- @participants.each do |p| + - if p.round == 'c' || p.round == 'd' + .row.mt-3 + .col + = form_with model: WorldParticipant, url: "/wjpc_bets/update/#{p.id}", method: :patch do |form| + .input-group + .input-group-text style="width: 350px;" #{Flags.get_flag(p.world_country.code) + " " + p.name} + .input-group-text = t("worlds.main.rank") + = form.text_field :semifinal_result, value: p.semifinal_result, autocomplete: "off", class: "form-control", style: "color: orange; max-width: 60px;" + .input-group-text = t("worlds.main.points") + .input-group-text style="color: green; width: 60px;" #{WorldBet.points(p.semifinal_result) * 2} + = form.submit t("helpers.buttons.validate"), class: "btn btn-primary", onclick: "updateScrollPos()" + +h4.mt-5 = "#{t("worlds.collab.final")}" + +- @participants.each do |p| + .row.mt-3 + .col + = form_with model: WorldParticipant, url: "/wjpc_bets/update/#{p.id}", method: :patch do |form| + .input-group + .input-group-text style="width: 350px;" #{Flags.get_flag(p.world_country.code) + " " + p.name} + .input-group-text = t("worlds.main.rank") + = form.text_field :final_result, value: p.final_result, autocomplete: "off", class: "form-control", style: "color: orange; max-width: 60px;" + .input-group-text = t("worlds.main.points") + .input-group-text style="color: green; width: 60px;" #{WorldBet.points(p.final_result) * 4} + = form.submit t("helpers.buttons.validate"), class: "btn btn-primary", onclick: "updateScrollPos()" + +.mb-5 diff --git a/app/views/worlds/main.html.slim b/app/views/worlds/main.html.slim new file mode 100644 index 0000000..f69799b --- /dev/null +++ b/app/views/worlds/main.html.slim @@ -0,0 +1,52 @@ += render "worlds_nav" + +.row.mt-3 + .col-6 + h4 = t("worlds.main.world_participants") + table.table.table-striped.table-hover + thead + tr + th = t("worlds.main.rank") + th = t("worlds.main.name") + th = t("worlds.main.score") + tbody + - @world_bets_w.each_with_index do |b, i| + tr + - if i == 0 + td ๐Ÿฅ‡ + - elsif i == 1 + td ๐Ÿฅˆ + - elsif i == 2 + td ๐Ÿฅ‰ + - else + td #{i + 1} + td #{b.name} + td #{b.world_score} + td + a.btn.btn-sm.btn-secondary href="/wjpc_bets/view/#{b.id}" + = t("worlds.main.view_bet") + + .col-6 + h4 = t("worlds.main.french_participants") + table.table.table-striped.table-hover + thead + tr + th = t("worlds.main.rank") + th = t("worlds.main.name") + th = t("worlds.main.score") + tbody + - @world_bets_f.each_with_index do |b, i| + tr + - if i == 0 + td ๐Ÿฅ‡ + - elsif i == 1 + td ๐Ÿฅˆ + - elsif i == 2 + td ๐Ÿฅ‰ + - else + td #{i + 1} + td #{b.name} + td #{b.french_score} + td + a.btn.btn-sm.btn-secondary href="/wjpc_bets/view/#{b.id}" + = t("worlds.main.view_bet") \ No newline at end of file diff --git a/app/views/worlds/new.html.slim b/app/views/worlds/new.html.slim new file mode 100644 index 0000000..581d06d --- /dev/null +++ b/app/views/worlds/new.html.slim @@ -0,0 +1,45 @@ += render "worlds_nav" + += form_with model: @world_bet, url: "/wjpc_bets/new", method: :post do |form| + .alert.alert-primary role="alert" + = t("worlds.new.message") + + .row.mt-4 + .col + .form-floating + = form.text_field :name, autocomplete: "off", class: "form-control" + = form.label :name, class: "required" + + .row.mt-4 + .col + h4 = t("worlds.new.world_section") + + - (1..10).each do |i| + .row.mt-3 + .col + .input-group + .input-group-text ##{i} (x#{WorldBet.multiplier(i - 1)}) + = form.select "w_bet_#{i}", @world_participants.map { |p| [Flags.get_flag(p.world_country.code) + " " + p.name, p.id] }, {}, class: "form-select" + + .row.mt-4 + .col + h4 = t("worlds.new.french_section") + + - (1..10).each do |i| + .row.mt-3 + .col + .input-group + .input-group-text ##{i} (x#{WorldBet.multiplier(i - 1)}) + = form.select "f_bet_#{i}", @french_participants.map { |p| [Flags.get_flag(p.world_country.code) + " " + p.name, p.id] }, {}, class: "form-select" + + - if @w_array && @f_array + - (1..10).each do |i| + javascript: + el = document.querySelector('select[name="world_bet[w_bet_#{i}]"]'); + el.value = "#{@w_array[i - 1]}" + el = document.querySelector('select[name="world_bet[f_bet_#{i}]"]'); + el.value = "#{@f_array[i - 1]}" + + .row.mt-4.mb-5 + .col + = form.submit t("helpers.buttons.validate"), class: "btn btn-primary" \ No newline at end of file diff --git a/app/views/worlds/view.html.slim b/app/views/worlds/view.html.slim new file mode 100644 index 0000000..e688be9 --- /dev/null +++ b/app/views/worlds/view.html.slim @@ -0,0 +1,49 @@ += render "worlds_nav" + +h4.mt-3 = t("worlds.view.subtitle", name: @world_bet.name).html_safe + +.row.mt-3 + .col + h5 = t("worlds.view.world_participants") + table.table.table-striped.table-hover + thead + tr + th = t("worlds.view.participant") + th = t("worlds.view.multiplier") + th Q + th S + th F + th = t("worlds.view.total") + tbody + - @world_participants.each_with_index do |p, i| + tr + td = Flags.get_flag(p.world_country.code) + " " + p.name + td style="color: cadetblue;" + = "x#{WorldBet.multiplier(i)}" + td = "#{p.qualifier_result || "?"} > #{WorldBet.points(p.qualifier_result)}p".html_safe + td = "#{p.semifinal_result || "?"} > #{WorldBet.points(p.semifinal_result) * 2}p".html_safe + td = "#{p.final_result || "?"} > #{WorldBet.points(p.final_result) * 4}p".html_safe + td = "#{p.score || 0} x #{WorldBet.multiplier(i)} = #{(p.score || 0) * WorldBet.multiplier(i)}p".html_safe + +.row.mt-3 + .col + h5 = t("worlds.view.french_participants") + table.table.table-striped.table-hover + thead + tr + th = t("worlds.view.participant") + th = t("worlds.view.multiplier") + th Q + th S + th F + th = t("worlds.view.total") + tbody + - @french_participants.each_with_index do |p, i| + tr + td = Flags.get_flag(p.world_country.code) + " " + p.name + td style="color: cadetblue;" + = "x#{WorldBet.multiplier(i)}" + td = "#{p.qualifier_result || "?"} > #{WorldBet.points(p.qualifier_result)}p".html_safe + td = "#{p.semifinal_result || "?"} > #{WorldBet.points(p.semifinal_result) * 2}p".html_safe + td = "#{p.final_result || "?"} > #{WorldBet.points(p.final_result) * 4}p".html_safe + td = "#{p.score || 0} x #{WorldBet.multiplier(i)} = #{(p.score || 0) * WorldBet.multiplier(i)}p".html_safe \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index a831e85..932017b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -113,6 +113,8 @@ en: email_address: Email address lang: Language password: New password + world_bet: + name: Name errors: models: completion: @@ -182,6 +184,13 @@ en: taken: This username is already taken password: blank: Your password cannot be empty + world_bet: + attributes: + bet: + duplicate: You can't select a participant twice in either of the two selection + name: + blank: You must provide a name + taken: This name is already taken categories: destroy: notice: Category deleted @@ -420,3 +429,47 @@ en: new: notice: User created title: "New user" + worlds: + about: + collaborative_filling_message: The ranking is updated collaboratively, see the dedicated tab for more details. + details_message: To make a bet you'll have to select some contestants (see the dedicated tab for more details on that, and multipliers). You'll score points when they do well in the competition, the table below gives the amount of points you get depending on their ranking on each round, from qualifiers to the finals. This however only accounts for the solo category. + final_round: Finals + one_bet_message: Only one bet is allowed per person, please do not overflow the system with bets. + points: Points + qualifier_round: Qualifier round + ranking: Ranking + semifinal_round: Semifinals + tab: Rules + two_rankings_message: There are two rankings, one for the bet over all participants, and one for the bet over French participants. + collab: + details_message: Here you will all be able to collaboratively keep up-to-date the ranks of the participants that were selectec by bets. Anyone is able to make any update here, I trust you to do it with care, not cheating, and correcting errors if you see any. I'm doing it this way, so you don't have to wait for me to have the time to update everything :) + error: An error occured, the rank must be a numerical value in the range [1, 250] + error_message: If a participant did not qualify, do not fill their ranks for the subsequent rounds. If a rank was filled for them by mistake, please change it to any value above 150 so its score remains 0. + final: Final + notice: Participant rank updated + rank_message: Below should be indicated the *rank* reached by each participant on each round, not the points. Points are automatically calculated. + semifinal: Semifinal + tab: Collaborative filling + main: + french_participants: French participants + name: Name + points: Points + rank: Rank + score: Score + tab: Rankings + view_bet: View bet + world_participants: All participants + new: + french_section: Bet over French participants + message: To make a bet, you have to provide two ordered lists of 10 participants, one among all participants, and the other one only among French participants. The order matters! A score multiplier will be applied to each selected contestant (in parenthesis below). You can't select a participant twice on a list, but of course you can totally put a French contestant in both lists ;) + notice: The bet has been made! + tab: Make a bet! + world_section: Bet over all participants + title: WJPC bets + view: + french_participants: On French participants + multiplier: Multiplier + participant: Participant + subtitle: "%{name}'s bet" + total: Total + world_participants: On all participants diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 662137e..ea00d94 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -84,6 +84,8 @@ fr: email_address: Adresse email lang: Langue de l'interface password: Nouveau mot de passe + world_bet: + name: Nom errors: models: completion: @@ -153,6 +155,14 @@ fr: taken: Ce nom d'utilisateur.ice est dรฉjร  utilisรฉ password: blank: Le mot de passe ne peut pas รชtre vide + world_bet: + attributes: + bet: + duplicate: + Il n'est pas possible de choisir un.e participant.e plusieurs fois dans une des deux listes + name: + blank: Le nom est obligatoire + taken: Ce nom est dรฉjร  pris categories: destroy: notice: Catรฉgorie supprimรฉe @@ -391,3 +401,48 @@ fr: new: notice: Utilisateur.ice ajoutรฉ.e title: "Nouveau.elle utilisateur.ice" + worlds: + about: + collaborative_filling_message: Les rรฉsultats des participant.e.s sont mis ร  jour de maniรจre collaborative, plus d'infos dans l'onglet dรฉdiรฉ. + details_message: Pour faire un pari, tu devras choisir plusieurs participant.e.s (plus d'infos sur comment รงa marche et sur les facteurs multiplicatifs dans l'onglet dรฉdiรฉ). Tu marqueras des points en fonction de comment iels se classent dans chaque round, depuis les qualifs jusqu'ร  la finale (catรฉgorie solo uniquement). La table ci-dessous donne la rรฉpartition des points en fonction des classements. + final_round: Finale + one_bet_message: Un seul pari par personne, ne surchargez pas le systรจme svp. + points: Points + qualifier_round: Qualifications + ranking: Classement + semifinal_round: Demi-finales + tab: Rรจgles + two_rankings_message: "Il y a deux classements : un sur les paris parmi l'ensemble des participant.e.s, et l'autres sur les paris sur les franรงais.e.s." + collab: + details_message: Ici est l'endroit pour mettre librement et collaborativement ร  jour les rรฉsultats des participant.e.s sรฉlectionnรฉ.e.s dans les paris. Je mets cet espace ร  disposition pour permettre un remplissage plus rapide sans avoir besoin d'attendre que j'ai le temps de tout remplir moi-mรชme. Je vous fais confiance pour faire รงa avec soin, sans tricher, et en corrigeant des erreurs si vous en trouvez :) + error: "Une erreur est survenue : vรฉrifie que tu as bien rentrรฉ un rang dans l'intervalle [1, 250]" + error_message: Si une participant.e ne se qualifie pas lors d'un round, pas besoin alors de remplir son rang pour les rounds qui suivent. Si un rang a รฉtรฉ rempli par erreur, changez-le pour n'importe quel rang au-dessus de 150 histoire que รงa revienne ร  un score de 0. + final: Finale + notice: "Le rang de cet.te participant.e a รฉtรฉ mis ร  jour" + rank_message: Il s'agit ici d'indiquer les *classements* obtenus par les participant.e.s lors des diffรฉrents rounds. Les points seront ensuite calculรฉs automatiquement. + semifinal: Semifinal + tab: Remplisage collaboratif + main: + french_participants: Sur les participant.e.s franรงais.e.s + name: Nom + points: Points + rank: Rang + score: Score + tab: Classements + view_bet: Voir le pari + world_participants: Sur toutes les participant.e.s + new: + french_section: Pari sur les participant.e.s franรงais.e.s + message: Pour faire un pari, tu dois choisir 10 participant.e.s sur 2 listes, une comprenant l'ensemble des participant.e.s, l'autre comprenant uniquement les participant.e.s franรงais.e.s. L'ordre a son importance ! Un facteur multiplicatif sera appliquรฉ sur chaque participant.e que tu auras sรฉlectionnรฉ.e (en parenthรจses ci-dessous). Tu ne peux pas choisir un.e mรชme participant.e plusieurs fois dans une liste, mais tu peux en revanche tout ร  fait choisir un.e participant.e franรงais.e dans les deux listes ! + notice: Le pari est enregistrรฉ ! + tab: Fais ton pari ! + title: Nouveau pari + world_section: Pari sur l'ensemble des participant.e.s + title: Les paris des WJPC + view: + french_participants: Sur les franรงais.e.s + multiplier: Multiplicateur + participant: Participant.e + subtitle: "Le pari de %{name}" + total: Total + world_participants: Sur tout le monde diff --git a/config/routes.rb b/config/routes.rb index 89a863c..3e416d2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -82,4 +82,13 @@ Rails.application.routes.draw do direct :offline_form_completed do |contest, offline| "/public/#{contest.friendly_id}/offline/#{offline.generate_token_for(:token)}/completed" end + + get "wjpc_bets", to: redirect("/wjpc_bets/main") + get "wjpc_bets/main", to: "worlds#main" + get "wjpc_bets/about", to: "worlds#about" + get "wjpc_bets/collaborative_filling", to: "worlds#collab" + get "wjpc_bets/new", to: "worlds#new" + post "wjpc_bets/new", to: "worlds#create" + patch "wjpc_bets/update/:participant_id", to: "worlds#update" + get "wjpc_bets/view/:bet_id", to: "worlds#view" end diff --git a/db/migrate/20260904063514_create_world_countries.rb b/db/migrate/20260904063514_create_world_countries.rb new file mode 100644 index 0000000..25a715a --- /dev/null +++ b/db/migrate/20260904063514_create_world_countries.rb @@ -0,0 +1,10 @@ +class CreateWorldCountries < ActiveRecord::Migration[8.0] + def change + create_table :world_countries do |t| + t.string :name, null: false + t.string :code, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20260904063926_create_world_participants.rb b/db/migrate/20260904063926_create_world_participants.rb new file mode 100644 index 0000000..5f0db26 --- /dev/null +++ b/db/migrate/20260904063926_create_world_participants.rb @@ -0,0 +1,14 @@ +class CreateWorldParticipants < ActiveRecord::Migration[8.0] + def change + create_table :world_participants do |t| + t.string :username, null: false + t.string :name, null: false + t.belongs_to :world_country, null: false, foreign_key: true + t.integer :qualifier_result + t.integer :semifinal_result + t.integer :final_result + + t.timestamps + end + end +end diff --git a/db/migrate/20260904065915_add_round_to_world_participant.rb b/db/migrate/20260904065915_add_round_to_world_participant.rb new file mode 100644 index 0000000..f01a366 --- /dev/null +++ b/db/migrate/20260904065915_add_round_to_world_participant.rb @@ -0,0 +1,5 @@ +class AddRoundToWorldParticipant < ActiveRecord::Migration[8.0] + def change + add_column :world_participants, :round, :string, null: false + end +end diff --git a/db/migrate/20260904131402_create_world_bets.rb b/db/migrate/20260904131402_create_world_bets.rb new file mode 100644 index 0000000..5d11ef6 --- /dev/null +++ b/db/migrate/20260904131402_create_world_bets.rb @@ -0,0 +1,11 @@ +class CreateWorldBets < ActiveRecord::Migration[8.0] + def change + create_table :world_bets do |t| + t.string :name + t.string :bet + t.integer :score + + t.timestamps + end + end +end diff --git a/db/migrate/20260904132641_add_score_to_world_participant.rb b/db/migrate/20260904132641_add_score_to_world_participant.rb new file mode 100644 index 0000000..590aae8 --- /dev/null +++ b/db/migrate/20260904132641_add_score_to_world_participant.rb @@ -0,0 +1,5 @@ +class AddScoreToWorldParticipant < ActiveRecord::Migration[8.0] + def change + add_column :world_participants, :score, :integer + end +end diff --git a/db/migrate/20260905120054_add_world_score_to_world_bet.rb b/db/migrate/20260905120054_add_world_score_to_world_bet.rb new file mode 100644 index 0000000..4a24fff --- /dev/null +++ b/db/migrate/20260905120054_add_world_score_to_world_bet.rb @@ -0,0 +1,5 @@ +class AddWorldScoreToWorldBet < ActiveRecord::Migration[8.0] + def change + add_column :world_bets, :world_score, :integer + end +end diff --git a/db/migrate/20260905120106_add_french_score_to_world_bet.rb b/db/migrate/20260905120106_add_french_score_to_world_bet.rb new file mode 100644 index 0000000..897ca0f --- /dev/null +++ b/db/migrate/20260905120106_add_french_score_to_world_bet.rb @@ -0,0 +1,5 @@ +class AddFrenchScoreToWorldBet < ActiveRecord::Migration[8.0] + def change + add_column :world_bets, :french_score, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index c0eef81..9fe7250 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: 2026_08_25_071844) do +ActiveRecord::Schema[8.0].define(version: 2026_09_05_120106) do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -203,6 +203,37 @@ ActiveRecord::Schema[8.0].define(version: 2026_08_25_071844) do t.index ["email_address"], name: "index_users_on_email_address", unique: true end + create_table "world_bets", force: :cascade do |t| + t.string "name" + t.string "bet" + t.integer "score" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "world_score" + t.integer "french_score" + end + + create_table "world_countries", force: :cascade do |t| + t.string "name", null: false + t.string "code", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "world_participants", force: :cascade do |t| + t.string "username", null: false + t.string "name", null: false + t.integer "world_country_id", null: false + t.integer "qualifier_result" + t.integer "semifinal_result" + t.integer "final_result" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "round", null: false + t.integer "score" + t.index ["world_country_id"], name: "index_world_participants_on_world_country_id" + end + 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" @@ -220,4 +251,5 @@ ActiveRecord::Schema[8.0].define(version: 2026_08_25_071844) do add_foreign_key "offlines", "contests" add_foreign_key "puzzles", "contests" add_foreign_key "sessions", "users" + add_foreign_key "world_participants", "world_countries" end diff --git a/spec/factories/world_bets.rb b/spec/factories/world_bets.rb new file mode 100644 index 0000000..fc1906a --- /dev/null +++ b/spec/factories/world_bets.rb @@ -0,0 +1,20 @@ +# == Schema Information +# +# Table name: world_bets +# +# id :integer not null, primary key +# bet :string +# french_score :integer +# name :string +# score :integer +# world_score :integer +# created_at :datetime not null +# updated_at :datetime not null +# +FactoryBot.define do + factory :world_bet do + name { "MyString" } + bet { "MyString" } + score { 1 } + end +end diff --git a/spec/factories/world_countries.rb b/spec/factories/world_countries.rb new file mode 100644 index 0000000..1e4c1bd --- /dev/null +++ b/spec/factories/world_countries.rb @@ -0,0 +1,16 @@ +# == Schema Information +# +# Table name: world_countries +# +# id :integer not null, primary key +# code :string not null +# name :string not null +# created_at :datetime not null +# updated_at :datetime not null +# +FactoryBot.define do + factory :world_country do + name { "MyString" } + code { "MyString" } + end +end diff --git a/spec/factories/world_participants.rb b/spec/factories/world_participants.rb new file mode 100644 index 0000000..3327b53 --- /dev/null +++ b/spec/factories/world_participants.rb @@ -0,0 +1,34 @@ +# == Schema Information +# +# Table name: world_participants +# +# id :integer not null, primary key +# final_result :integer +# name :string not null +# qualifier_result :integer +# round :string not null +# score :integer +# semifinal_result :integer +# username :string not null +# created_at :datetime not null +# updated_at :datetime not null +# world_country_id :integer not null +# +# Indexes +# +# index_world_participants_on_world_country_id (world_country_id) +# +# Foreign Keys +# +# world_country_id (world_country_id => world_countries.id) +# +FactoryBot.define do + factory :world_participant do + username { "MyString" } + name { "MyString" } + world_country { nil } + qualifier_result { 1 } + semifinal_result { 1 } + final_result { 1 } + end +end diff --git a/spec/helpers/worlds_helper_spec.rb b/spec/helpers/worlds_helper_spec.rb new file mode 100644 index 0000000..3659250 --- /dev/null +++ b/spec/helpers/worlds_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the WorldsHelper. For example: +# +# describe WorldsHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe WorldsHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/world_bet_spec.rb b/spec/models/world_bet_spec.rb new file mode 100644 index 0000000..05cbb96 --- /dev/null +++ b/spec/models/world_bet_spec.rb @@ -0,0 +1,18 @@ +require 'rails_helper' + +# == Schema Information +# +# Table name: world_bets +# +# id :integer not null, primary key +# bet :string +# french_score :integer +# name :string +# score :integer +# world_score :integer +# created_at :datetime not null +# updated_at :datetime not null +# +RSpec.describe WorldBet, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/world_country_spec.rb b/spec/models/world_country_spec.rb new file mode 100644 index 0000000..9079d10 --- /dev/null +++ b/spec/models/world_country_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# == Schema Information +# +# Table name: world_countries +# +# id :integer not null, primary key +# code :string not null +# name :string not null +# created_at :datetime not null +# updated_at :datetime not null +# +RSpec.describe WorldCountry, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/world_participant_spec.rb b/spec/models/world_participant_spec.rb new file mode 100644 index 0000000..df9d0e2 --- /dev/null +++ b/spec/models/world_participant_spec.rb @@ -0,0 +1,29 @@ +require 'rails_helper' + +# == Schema Information +# +# Table name: world_participants +# +# id :integer not null, primary key +# final_result :integer +# name :string not null +# qualifier_result :integer +# round :string not null +# score :integer +# semifinal_result :integer +# username :string not null +# created_at :datetime not null +# updated_at :datetime not null +# world_country_id :integer not null +# +# Indexes +# +# index_world_participants_on_world_country_id (world_country_id) +# +# Foreign Keys +# +# world_country_id (world_country_id => world_countries.id) +# +RSpec.describe WorldParticipant, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/requests/worlds_spec.rb b/spec/requests/worlds_spec.rb new file mode 100644 index 0000000..342bc30 --- /dev/null +++ b/spec/requests/worlds_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +RSpec.describe "Worlds", type: :request do + describe "GET /index" do + pending "add some examples (or delete) #{__FILE__}" + end +end