Worlds Bets
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled

This commit is contained in:
sto
2026-09-06 10:07:47 +02:00
parent 49e49dc93d
commit dae86bd1ea
33 changed files with 1337 additions and 1 deletions

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"editor.tabSize": 2
}

View File

@@ -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

View File

@@ -0,0 +1,2 @@
module WorldsHelper
end

261
app/lib/flags.rb Normal file
View File

@@ -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

93
app/models/world_bet.rb Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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}

View File

@@ -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

View File

@@ -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")

View File

@@ -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} <i class="ms-2" style="color: cadetblue;">(x#{WorldBet.multiplier(i - 1)})</i>
= 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} <i class="ms-2" style="color: cadetblue;">(x#{WorldBet.multiplier(i - 1)})</i>
= 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"

View File

@@ -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 = "<span style='color: orange'>#{p.qualifier_result || "?"}</span> > <span style='color: green'>#{WorldBet.points(p.qualifier_result)}p</span>".html_safe
td = "<span style='color: orange'>#{p.semifinal_result || "?"}</span> > <span style='color: green'>#{WorldBet.points(p.semifinal_result) * 2}p</span>".html_safe
td = "<span style='color: orange'>#{p.final_result || "?"}</span> > <span style='color: green'>#{WorldBet.points(p.final_result) * 4}p</span>".html_safe
td = "<span style='color: green'>#{p.score || 0}</span> x <span style='color: cadetblue'>#{WorldBet.multiplier(i)}</span> = <span style='color: green'>#{(p.score || 0) * WorldBet.multiplier(i)}p</span>".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 = "<span style='color: orange'>#{p.qualifier_result || "?"}</span> > <span style='color: green'>#{WorldBet.points(p.qualifier_result)}p</span>".html_safe
td = "<span style='color: orange'>#{p.semifinal_result || "?"}</span> > <span style='color: green'>#{WorldBet.points(p.semifinal_result) * 2}p</span>".html_safe
td = "<span style='color: orange'>#{p.final_result || "?"}</span> > <span style='color: green'>#{WorldBet.points(p.final_result) * 4}p</span>".html_safe
td = "<span style='color: green'>#{p.score || 0}</span> x <span style='color: cadetblue'>#{WorldBet.multiplier(i)}</span> = <span style='color: green'>#{(p.score || 0) * WorldBet.multiplier(i)}p</span>".html_safe

View File

@@ -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: "<b>%{name}</b>'s bet"
total: Total
world_participants: On all participants

View File

@@ -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 <b>%{name}</b>"
total: Total
world_participants: Sur tout le monde

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,5 @@
class AddRoundToWorldParticipant < ActiveRecord::Migration[8.0]
def change
add_column :world_participants, :round, :string, null: false
end
end

View File

@@ -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

View File

@@ -0,0 +1,5 @@
class AddScoreToWorldParticipant < ActiveRecord::Migration[8.0]
def change
add_column :world_participants, :score, :integer
end
end

View File

@@ -0,0 +1,5 @@
class AddWorldScoreToWorldBet < ActiveRecord::Migration[8.0]
def change
add_column :world_bets, :world_score, :integer
end
end

View File

@@ -0,0 +1,5 @@
class AddFrenchScoreToWorldBet < ActiveRecord::Migration[8.0]
def change
add_column :world_bets, :french_score, :integer
end
end

34
db/schema.rb generated
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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