Compare commits
61 Commits
6afde8a971
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7db96cfab4 | |||
| bbd2cef168 | |||
| 1fa7bf10ec | |||
| 916c7af738 | |||
| 537f32ab8b | |||
| 5b9862c19c | |||
| 4ca711f5aa | |||
| b13ef30807 | |||
| 657c5ac47b | |||
| 502649620b | |||
| ee476ab81b | |||
| 0599def237 | |||
| b6da55723d | |||
| 9862f0c74b | |||
| 1b34d10dee | |||
| d28f888ee2 | |||
| 2b1a2c9296 | |||
| 1a8ea0afee | |||
| 2cadc8eca5 | |||
| c34b9654c8 | |||
| 341e626f6f | |||
| c22b529858 | |||
| 50050064c2 | |||
| 5aa69a108c | |||
| ef3c63ea67 | |||
| 6fb5ba5f3e | |||
| 6c16e5e232 | |||
| 2969a24cb0 | |||
| 4b5c09f63b | |||
| ca7399f490 | |||
| f27b43ef45 | |||
| 5b908fe37c | |||
| 2616cbaa71 | |||
| 70c0fed0c4 | |||
| 6c0f5167a4 | |||
| ac3b354480 | |||
| 71f2bb6b70 | |||
| ac83a599f3 | |||
| 67492cdd15 | |||
| 79fb1edfaf | |||
| 4645b45f5d | |||
| f78a082ad3 | |||
| b8674a126f | |||
| 67d2ef41b3 | |||
| 96b8553b1f | |||
| 194c126c90 | |||
| a33f3ff4de | |||
| 17a1af4e9f | |||
| baea71b312 | |||
| bc32387c21 | |||
| 55399d80fe | |||
| d7d90f0c91 | |||
| 7444a09046 | |||
| ec2201f9a8 | |||
| 939e2157ab | |||
| 5ec0e264ba | |||
| c4902d85d5 | |||
| e65d639ca6 | |||
| 1397ddce2f | |||
| 138fe67baa | |||
| 3a8517e637 |
2
Gemfile
2
Gemfile
@@ -44,6 +44,8 @@ gem "slim"
|
||||
gem "dartsass-rails"
|
||||
gem "bootstrap", "~> 5.3.3"
|
||||
gem "friendly_id", "~> 5.5.0"
|
||||
gem "csv"
|
||||
gem "damerau-levenshtein"
|
||||
|
||||
group :development, :test do
|
||||
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
|
||||
|
||||
204
Gemfile.lock
204
Gemfile.lock
@@ -74,22 +74,21 @@ GEM
|
||||
uri (>= 0.13.1)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
annotaterb (4.14.0)
|
||||
ast (2.4.2)
|
||||
autoprefixer-rails (10.4.19.0)
|
||||
execjs (~> 2)
|
||||
base64 (0.2.0)
|
||||
annotaterb (4.16.0)
|
||||
activerecord (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
ast (2.4.3)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.20)
|
||||
bcrypt_pbkdf (1.1.1)
|
||||
benchmark (0.4.0)
|
||||
bigdecimal (3.1.9)
|
||||
benchmark (0.4.1)
|
||||
bigdecimal (3.2.2)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.18.4)
|
||||
bootsnap (1.18.6)
|
||||
msgpack (~> 1.2)
|
||||
bootstrap (5.3.3)
|
||||
autoprefixer-rails (>= 9.1.0)
|
||||
bootstrap (5.3.5)
|
||||
popper_js (>= 2.11.8, < 3)
|
||||
brakeman (7.0.0)
|
||||
brakeman (7.0.2)
|
||||
racc
|
||||
builder (3.3.0)
|
||||
capybara (3.40.0)
|
||||
@@ -102,28 +101,30 @@ GEM
|
||||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.0)
|
||||
connection_pool (2.5.3)
|
||||
crass (1.0.6)
|
||||
csv (3.3.5)
|
||||
damerau-levenshtein (1.3.3)
|
||||
dartsass-rails (0.5.1)
|
||||
railties (>= 6.0.0)
|
||||
sass-embedded (~> 1.63)
|
||||
date (3.4.1)
|
||||
debug (1.10.0)
|
||||
debug (1.11.0)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
diff-lcs (1.6.1)
|
||||
dotenv (3.1.7)
|
||||
drb (2.2.1)
|
||||
ed25519 (1.3.0)
|
||||
diff-lcs (1.6.2)
|
||||
dotenv (3.1.8)
|
||||
drb (2.2.3)
|
||||
ed25519 (1.4.0)
|
||||
erb (5.0.1)
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
execjs (2.10.0)
|
||||
factory_bot (6.5.1)
|
||||
factory_bot (6.5.4)
|
||||
activesupport (>= 6.1.0)
|
||||
factory_bot_rails (6.4.4)
|
||||
factory_bot_rails (6.5.0)
|
||||
factory_bot (~> 6.5)
|
||||
railties (>= 5.0.0)
|
||||
railties (>= 6.1.0)
|
||||
faker (3.5.1)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
friendly_id (5.5.1)
|
||||
@@ -133,13 +134,19 @@ GEM
|
||||
raabro (~> 1.4)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
google-protobuf (4.30.0)
|
||||
google-protobuf (4.31.1)
|
||||
bigdecimal
|
||||
rake (>= 13)
|
||||
google-protobuf (4.30.0-aarch64-linux)
|
||||
google-protobuf (4.31.1-aarch64-linux-gnu)
|
||||
bigdecimal
|
||||
rake (>= 13)
|
||||
google-protobuf (4.30.0-x86_64-linux)
|
||||
google-protobuf (4.31.1-aarch64-linux-musl)
|
||||
bigdecimal
|
||||
rake (>= 13)
|
||||
google-protobuf (4.31.1-x86_64-linux-gnu)
|
||||
bigdecimal
|
||||
rake (>= 13)
|
||||
google-protobuf (4.31.1-x86_64-linux-musl)
|
||||
bigdecimal
|
||||
rake (>= 13)
|
||||
i18n (1.14.7)
|
||||
@@ -149,29 +156,29 @@ GEM
|
||||
activesupport (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
io-console (0.8.0)
|
||||
irb (1.15.1)
|
||||
irb (1.15.2)
|
||||
pp (>= 0.6.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jbuilder (2.13.0)
|
||||
actionview (>= 5.0.0)
|
||||
activesupport (>= 5.0.0)
|
||||
json (2.10.2)
|
||||
kamal (2.5.3)
|
||||
json (2.12.2)
|
||||
kamal (2.7.0)
|
||||
activesupport (>= 7.0)
|
||||
base64 (~> 0.2)
|
||||
bcrypt_pbkdf (~> 1.0)
|
||||
concurrent-ruby (~> 1.2)
|
||||
dotenv (~> 3.1)
|
||||
ed25519 (~> 1.2)
|
||||
ed25519 (~> 1.4)
|
||||
net-ssh (~> 7.3)
|
||||
sshkit (>= 1.23.0, < 2.0)
|
||||
thor (~> 1.3)
|
||||
zeitwerk (>= 2.6.18, < 3.0)
|
||||
language_server-protocol (3.17.0.4)
|
||||
language_server-protocol (3.17.0.5)
|
||||
lint_roller (1.1.0)
|
||||
logger (1.6.6)
|
||||
loofah (2.24.0)
|
||||
logger (1.7.0)
|
||||
loofah (2.24.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
@@ -180,11 +187,11 @@ GEM
|
||||
net-pop
|
||||
net-smtp
|
||||
marcel (1.0.4)
|
||||
matrix (0.4.2)
|
||||
matrix (0.4.3)
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.25.4)
|
||||
minitest (5.25.5)
|
||||
msgpack (1.8.0)
|
||||
net-imap (0.5.6)
|
||||
net-imap (0.5.9)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -199,44 +206,45 @@ GEM
|
||||
net-protocol
|
||||
net-ssh (7.3.0)
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.18.3-aarch64-linux-gnu)
|
||||
nokogiri (1.18.8-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.3-aarch64-linux-musl)
|
||||
nokogiri (1.18.8-aarch64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.3-arm-linux-gnu)
|
||||
nokogiri (1.18.8-arm-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.3-arm-linux-musl)
|
||||
nokogiri (1.18.8-arm-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.3-x86_64-linux-gnu)
|
||||
nokogiri (1.18.8-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.3-x86_64-linux-musl)
|
||||
nokogiri (1.18.8-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
ostruct (0.6.1)
|
||||
parallel (1.26.3)
|
||||
parser (3.3.7.1)
|
||||
ostruct (0.6.2)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.8.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
popper_js (2.11.8)
|
||||
pp (0.6.2)
|
||||
prettyprint
|
||||
prettyprint (0.2.0)
|
||||
prism (1.4.0)
|
||||
propshaft (1.1.0)
|
||||
actionpack (>= 7.0.0)
|
||||
activesupport (>= 7.0.0)
|
||||
rack
|
||||
railties (>= 7.0.0)
|
||||
psych (5.2.3)
|
||||
psych (5.2.6)
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.1)
|
||||
public_suffix (6.0.2)
|
||||
puma (6.6.0)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.5.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (3.1.12)
|
||||
rack-session (2.1.0)
|
||||
rack (3.1.16)
|
||||
rack-session (2.1.1)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.2.0)
|
||||
@@ -257,7 +265,7 @@ GEM
|
||||
activesupport (= 8.0.2)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 8.0.2)
|
||||
rails-dom-testing (2.2.0)
|
||||
rails-dom-testing (2.3.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
@@ -273,31 +281,32 @@ GEM
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.2.1)
|
||||
rdoc (6.12.0)
|
||||
rake (13.3.0)
|
||||
rdoc (6.14.1)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
regexp_parser (2.10.0)
|
||||
reline (0.6.0)
|
||||
reline (0.6.1)
|
||||
io-console (~> 0.5)
|
||||
rexml (3.4.1)
|
||||
rspec-core (3.13.3)
|
||||
rspec-core (3.13.4)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.3)
|
||||
rspec-expectations (3.13.5)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.2)
|
||||
rspec-mocks (3.13.5)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (7.1.1)
|
||||
actionpack (>= 7.0)
|
||||
activesupport (>= 7.0)
|
||||
railties (>= 7.0)
|
||||
rspec-rails (8.0.1)
|
||||
actionpack (>= 7.2)
|
||||
activesupport (>= 7.2)
|
||||
railties (>= 7.2)
|
||||
rspec-core (~> 3.13)
|
||||
rspec-expectations (~> 3.13)
|
||||
rspec-mocks (~> 3.13)
|
||||
rspec-support (~> 3.13)
|
||||
rspec-support (3.13.2)
|
||||
rubocop (1.73.2)
|
||||
rspec-support (3.13.4)
|
||||
rubocop (1.77.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
@@ -305,41 +314,42 @@ GEM
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
rubocop-ast (>= 1.45.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.38.1)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-performance (1.24.0)
|
||||
rubocop-ast (1.45.1)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.4)
|
||||
rubocop-performance (1.25.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (>= 1.72.1, < 2.0)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
rubocop-rails (2.30.3)
|
||||
rubocop-rails (2.32.0)
|
||||
activesupport (>= 4.2.0)
|
||||
lint_roller (~> 1.1)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.72.1, < 2.0)
|
||||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
rubocop-rails-omakase (1.1.0)
|
||||
rubocop (>= 1.72)
|
||||
rubocop-performance (>= 1.24)
|
||||
rubocop-rails (>= 2.30)
|
||||
ruby-progressbar (1.13.0)
|
||||
rubyzip (2.4.1)
|
||||
sass-embedded (1.85.1-aarch64-linux-gnu)
|
||||
google-protobuf (~> 4.29)
|
||||
sass-embedded (1.85.1-aarch64-linux-musl)
|
||||
google-protobuf (~> 4.29)
|
||||
sass-embedded (1.85.1-arm-linux-gnueabihf)
|
||||
google-protobuf (~> 4.29)
|
||||
sass-embedded (1.85.1-arm-linux-musleabihf)
|
||||
google-protobuf (~> 4.29)
|
||||
sass-embedded (1.85.1-x86_64-linux-gnu)
|
||||
google-protobuf (~> 4.29)
|
||||
sass-embedded (1.85.1-x86_64-linux-musl)
|
||||
google-protobuf (~> 4.29)
|
||||
sass-embedded (1.89.2-aarch64-linux-gnu)
|
||||
google-protobuf (~> 4.31)
|
||||
sass-embedded (1.89.2-aarch64-linux-musl)
|
||||
google-protobuf (~> 4.31)
|
||||
sass-embedded (1.89.2-arm-linux-gnueabihf)
|
||||
google-protobuf (~> 4.31)
|
||||
sass-embedded (1.89.2-arm-linux-musleabihf)
|
||||
google-protobuf (~> 4.31)
|
||||
sass-embedded (1.89.2-x86_64-linux-gnu)
|
||||
google-protobuf (~> 4.31)
|
||||
sass-embedded (1.89.2-x86_64-linux-musl)
|
||||
google-protobuf (~> 4.31)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.29.1)
|
||||
selenium-webdriver (4.33.0)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
@@ -348,7 +358,7 @@ GEM
|
||||
slim (5.2.1)
|
||||
temple (~> 0.10.0)
|
||||
tilt (>= 2.1.0)
|
||||
solid_cable (3.0.7)
|
||||
solid_cable (3.0.10)
|
||||
actioncable (>= 7.2)
|
||||
activejob (>= 7.2)
|
||||
activerecord (>= 7.2)
|
||||
@@ -357,19 +367,19 @@ GEM
|
||||
activejob (>= 7.2)
|
||||
activerecord (>= 7.2)
|
||||
railties (>= 7.2)
|
||||
solid_queue (1.1.3)
|
||||
solid_queue (1.1.5)
|
||||
activejob (>= 7.1)
|
||||
activerecord (>= 7.1)
|
||||
concurrent-ruby (>= 1.3.1)
|
||||
fugit (~> 1.11.0)
|
||||
railties (>= 7.1)
|
||||
thor (~> 1.3.1)
|
||||
sqlite3 (2.6.0-aarch64-linux-gnu)
|
||||
sqlite3 (2.6.0-aarch64-linux-musl)
|
||||
sqlite3 (2.6.0-arm-linux-gnu)
|
||||
sqlite3 (2.6.0-arm-linux-musl)
|
||||
sqlite3 (2.6.0-x86_64-linux-gnu)
|
||||
sqlite3 (2.6.0-x86_64-linux-musl)
|
||||
sqlite3 (2.7.0-aarch64-linux-gnu)
|
||||
sqlite3 (2.7.0-aarch64-linux-musl)
|
||||
sqlite3 (2.7.0-arm-linux-gnu)
|
||||
sqlite3 (2.7.0-arm-linux-musl)
|
||||
sqlite3 (2.7.0-x86_64-linux-gnu)
|
||||
sqlite3 (2.7.0-x86_64-linux-musl)
|
||||
sshkit (1.24.0)
|
||||
base64
|
||||
logger
|
||||
@@ -379,15 +389,15 @@ GEM
|
||||
ostruct
|
||||
stimulus-rails (1.3.4)
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.1.5)
|
||||
stringio (3.1.7)
|
||||
temple (0.10.3)
|
||||
thor (1.3.2)
|
||||
thruster (0.1.12)
|
||||
thruster (0.1.12-aarch64-linux)
|
||||
thruster (0.1.12-x86_64-linux)
|
||||
thruster (0.1.14)
|
||||
thruster (0.1.14-aarch64-linux)
|
||||
thruster (0.1.14-x86_64-linux)
|
||||
tilt (2.6.0)
|
||||
timeout (0.4.3)
|
||||
turbo-rails (2.0.13)
|
||||
turbo-rails (2.0.16)
|
||||
actionpack (>= 7.1.0)
|
||||
railties (>= 7.1.0)
|
||||
tzinfo (2.0.6)
|
||||
@@ -403,13 +413,13 @@ GEM
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
websocket (1.2.11)
|
||||
websocket-driver (0.7.7)
|
||||
websocket-driver (0.8.0)
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.7.2)
|
||||
zeitwerk (2.7.3)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
@@ -428,6 +438,8 @@ DEPENDENCIES
|
||||
bootstrap (~> 5.3.3)
|
||||
brakeman
|
||||
capybara
|
||||
csv
|
||||
damerau-levenshtein
|
||||
dartsass-rails
|
||||
debug
|
||||
factory_bot_rails
|
||||
|
||||
37
app/controllers/categories_controller.rb
Normal file
37
app/controllers/categories_controller.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
class CategoriesController < ApplicationController
|
||||
before_action :set_contest
|
||||
before_action :set_category, only: %i[ destroy]
|
||||
|
||||
def create
|
||||
authorize @contest
|
||||
|
||||
@category = Category.new(category_params)
|
||||
@category.contest_id = @contest.id
|
||||
if @category.save
|
||||
redirect_to edit_contest_path(@contest), notice: t("categories.new.notice")
|
||||
else
|
||||
redirect_to edit_contest_path(@contest), notice: t("categories.new.error")
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @contest
|
||||
|
||||
@category.destroy
|
||||
redirect_to edit_contest_path(@contest), notice: t("categories.destroy.notice")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_contest
|
||||
@contest = Contest.find(params[:contest_id])
|
||||
end
|
||||
|
||||
def set_category
|
||||
@category = Category.find(params[:id])
|
||||
end
|
||||
|
||||
def category_params
|
||||
params.expect(category: [ :name ])
|
||||
end
|
||||
end
|
||||
@@ -2,16 +2,27 @@ class CompletionsController < ApplicationController
|
||||
include CompletionsConcern
|
||||
|
||||
before_action :set_contest
|
||||
before_action :set_contestant
|
||||
before_action :set_data, only: %i[ create edit new update ]
|
||||
before_action :set_completion, only: %i[ destroy edit update ]
|
||||
|
||||
def edit
|
||||
authorize @contest
|
||||
|
||||
if @contestant
|
||||
@action_name = t("helpers.buttons.back_to_contestant")
|
||||
@action_path = edit_contest_contestant_path(@contest, @contestant)
|
||||
end
|
||||
end
|
||||
|
||||
def new
|
||||
authorize @contest
|
||||
|
||||
if @contestant
|
||||
@action_name = t("helpers.buttons.back_to_contestant")
|
||||
@action_path = edit_contest_contestant_path(@contest, @contestant)
|
||||
end
|
||||
|
||||
@completion = Completion.new
|
||||
if params[:contestant_id]
|
||||
@completion.contestant_id = params[:contestant_id]
|
||||
@@ -25,10 +36,20 @@ class CompletionsController < ApplicationController
|
||||
@completion.contest_id = @contest.id
|
||||
if @completion.save
|
||||
extend_completions!(@completion.contestant)
|
||||
redirect_to contest_path(@contest)
|
||||
if @contestant && !params[:completion].key?(:message_id)
|
||||
redirect_to edit_contest_contestant_path(@contest, @contestant), notice: t("completions.new.notice")
|
||||
else
|
||||
logger = Logger.new(STDOUT)
|
||||
logger.info(@completion.errors)
|
||||
redirect_to @contest, notice: t("completions.new.notice")
|
||||
end
|
||||
else
|
||||
if params[:completion].key?(:message_id)
|
||||
@message = Message.find(params[:completion][:message_id])
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
elsif @contestant
|
||||
@action_name = t("helpers.buttons.back_to_contestant")
|
||||
@action_path = edit_contest_contestant_path(@contest, @contestant)
|
||||
end
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
@@ -36,13 +57,19 @@ class CompletionsController < ApplicationController
|
||||
def update
|
||||
authorize @contest
|
||||
|
||||
if params[:contestant_id]
|
||||
@completion.contestant_id = params[:contestant_id]
|
||||
end
|
||||
@completion.contestant_id = params[:contestant_id] if params[:contestant_id]
|
||||
if @completion.update(completion_params)
|
||||
extend_completions!(@completion.contestant)
|
||||
redirect_to @contest
|
||||
if @contestant
|
||||
redirect_to edit_contest_contestant_path(@contest, @contestant), notice: t("completions.edit.notice")
|
||||
else
|
||||
redirect_to @contest, notice: t("completions.edit.notice")
|
||||
end
|
||||
else
|
||||
if @contestant
|
||||
@action_name = t("helpers.buttons.back_to_contestant")
|
||||
@action_path = edit_contest_contestant_path(@contest, @contestant)
|
||||
end
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
@@ -52,9 +79,9 @@ class CompletionsController < ApplicationController
|
||||
|
||||
@completion.destroy
|
||||
if params[:contestant_id]
|
||||
redirect_to contest_contestant_path(@contest, params[:contestant_id])
|
||||
redirect_to contest_contestant_path(@contest, params[:contestant_id]), notice: t("completions.destroy.notice")
|
||||
else
|
||||
redirect_to contest_path(@contest)
|
||||
redirect_to contest_path(@contest), notice: t("completions.destroy.notice")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -64,8 +91,16 @@ class CompletionsController < ApplicationController
|
||||
@contest = Contest.find(params[:contest_id])
|
||||
end
|
||||
|
||||
def set_contestant
|
||||
if params.key?(:contestant_id)
|
||||
@contestant = Contestant.find(params[:contestant_id])
|
||||
elsif params[:completion].key?(:contestant_id)
|
||||
@contestant = Contestant.find(params[:completion][:contestant_id])
|
||||
end
|
||||
end
|
||||
|
||||
def set_data
|
||||
@contestants = @contest.contestants
|
||||
@contestants = @contest.contestants.order(:name)
|
||||
@puzzles = @contest.puzzles
|
||||
end
|
||||
|
||||
@@ -74,6 +109,6 @@ class CompletionsController < ApplicationController
|
||||
end
|
||||
|
||||
def completion_params
|
||||
params.expect(completion: [ :time_seconds, :contestant_id, :puzzle_id ])
|
||||
params.expect(completion: [ :display_time_from_start, :remaining_pieces, :contestant_id, :message_id, :puzzle_id ])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,15 +8,16 @@ module CompletionsConcern
|
||||
"0" + n.to_s
|
||||
end
|
||||
|
||||
def display_time(seconds)
|
||||
if seconds > 3600
|
||||
hours = seconds / 3600
|
||||
return hours.to_s + ":" + display_time(seconds % 3600)
|
||||
elsif seconds > 60
|
||||
minutes = seconds / 60
|
||||
return pad(minutes) + ":" + display_time(seconds % 60)
|
||||
def display_time(time)
|
||||
h = time / 3600
|
||||
m = (time % 3600) / 60
|
||||
s = (time % 3600) % 60
|
||||
if h > 0
|
||||
return h.to_s + ":" + pad(m) + ":" + pad(s)
|
||||
elsif m > 0
|
||||
return m.to_s + ":" + pad(s)
|
||||
end
|
||||
pad(seconds)
|
||||
s.to_s
|
||||
end
|
||||
|
||||
def extend_completions!(contestant)
|
||||
@@ -26,6 +27,6 @@ module CompletionsConcern
|
||||
display_relative_time: display_time(completion.time_seconds - current_time_from_start))
|
||||
current_time_from_start = completion.time_seconds
|
||||
end
|
||||
contestant.update(display_time: display_time(current_time_from_start))
|
||||
contestant.update(display_time: display_time(current_time_from_start), time_seconds: current_time_from_start)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,11 +5,16 @@ class ContestantsController < ApplicationController
|
||||
|
||||
def edit
|
||||
authorize @contest
|
||||
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
end
|
||||
|
||||
def new
|
||||
authorize @contest
|
||||
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
@contestant = Contestant.new
|
||||
end
|
||||
|
||||
@@ -19,8 +24,11 @@ class ContestantsController < ApplicationController
|
||||
@contestant = Contestant.new(contestant_params)
|
||||
@contestant.contest_id = @contest.id
|
||||
if @contestant.save
|
||||
redirect_to contest_path(@contest)
|
||||
update_contestant_categories
|
||||
redirect_to contest_path(@contest), notice: t("contestants.new.notice")
|
||||
else
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
@@ -29,8 +37,11 @@ class ContestantsController < ApplicationController
|
||||
authorize @contest
|
||||
|
||||
if @contestant.update(contestant_params)
|
||||
redirect_to @contest
|
||||
update_contestant_categories
|
||||
redirect_to @contest, notice: t("contestants.edit.notice")
|
||||
else
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
@@ -39,7 +50,83 @@ class ContestantsController < ApplicationController
|
||||
authorize @contest
|
||||
|
||||
@contestant.destroy
|
||||
redirect_to contest_path(@contest)
|
||||
redirect_to contest_path(@contest), notice: t("contestants.destroy.notice")
|
||||
end
|
||||
|
||||
def import
|
||||
authorize @contest
|
||||
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
|
||||
@csv_import = CsvImport.new
|
||||
end
|
||||
|
||||
def upload_csv
|
||||
authorize @contest
|
||||
|
||||
@csv_import = CsvImport.new(params.require(:csv_import).permit(:file, :separator))
|
||||
if @csv_import.save
|
||||
redirect_to "/contests/#{@contest.id}/import/#{@csv_import.id}"
|
||||
else
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
render :import, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def convert_csv
|
||||
authorize @contest
|
||||
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
@csv_import = CsvImport.find(params[:id])
|
||||
@content = JSON.parse(@csv_import.content)
|
||||
@form = Forms::CsvConversionForm.new
|
||||
end
|
||||
|
||||
def finalize_import
|
||||
authorize @contest
|
||||
|
||||
@csv_import = CsvImport.find(params[:id])
|
||||
@content = JSON.parse(@csv_import.content)
|
||||
all_params = params.require(:forms_csv_conversion_form)
|
||||
@form = Forms::CsvConversionForm.new(params.require(:forms_csv_conversion_form).permit(:email_column, :name_column))
|
||||
if @form.valid?
|
||||
@content.each_with_index do |row, i|
|
||||
if all_params["row_#{i}".to_sym] == "1"
|
||||
if @form.email_column == -1
|
||||
Contestant.create(name: row[@form.name_column], contest: @contest)
|
||||
else
|
||||
logger.info("Email")
|
||||
Contestant.create(name: row[@form.name_column], email: row[@form.email_column], contest: @contest)
|
||||
end
|
||||
end
|
||||
end
|
||||
redirect_to contest_path(@contest), notice: t("contestants.import.notice")
|
||||
else
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
render :convert_csv, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def export
|
||||
authorize @contest
|
||||
|
||||
@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
|
||||
] }
|
||||
|
||||
respond_to do |format|
|
||||
format.csv do
|
||||
response.headers["Content-Type"] = "text/csv"
|
||||
response.headers["Content-Disposition"] = "attachment; filename=export.csv"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
@@ -59,4 +146,15 @@ class ContestantsController < ApplicationController
|
||||
def contestant_params
|
||||
params.expect(contestant: [ :email, :name ])
|
||||
end
|
||||
|
||||
def update_contestant_categories
|
||||
@contestant.categories.clear
|
||||
@contest.categories.each do |category|
|
||||
logger.info(params[:contestant]["category_#{category.id}".to_sym] == "1")
|
||||
if params[:contestant].key?("category_#{category.id}".to_sym) && params[:contestant]["category_#{category.id}".to_sym] == "1"
|
||||
@contestant.categories << category
|
||||
end
|
||||
end
|
||||
@contestant.save
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,13 +13,25 @@ class ContestsController < ApplicationController
|
||||
authorize @contest
|
||||
|
||||
@title = I18n.t("contests.show.title", name: @contest.name)
|
||||
@contestants = @contest.contestants.order(:name)
|
||||
@action_name = t("helpers.buttons.edit")
|
||||
@action_path = edit_contest_path(@contest)
|
||||
@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
|
||||
end
|
||||
|
||||
def edit
|
||||
authorize @contest
|
||||
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
end
|
||||
|
||||
def new
|
||||
@@ -34,7 +46,7 @@ class ContestsController < ApplicationController
|
||||
@contest = Contest.new(contest_params)
|
||||
@contest.user_id = current_user.id
|
||||
if @contest.save
|
||||
redirect_to @contest
|
||||
redirect_to @contest, notice: t("contests.new.notice")
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
@@ -44,27 +56,46 @@ class ContestsController < ApplicationController
|
||||
authorize @contest
|
||||
|
||||
if @contest.update(contest_params)
|
||||
redirect_to @contest
|
||||
redirect_to @contest, notice: t("contests.edit.notice")
|
||||
else
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @contest
|
||||
|
||||
@contest.destroy
|
||||
redirect_to contests_path, notice: t("contests.destroy.notice")
|
||||
end
|
||||
|
||||
def scoreboard
|
||||
@contest = Contest.find_by(slug: params[:id])
|
||||
unless @contest
|
||||
unless @contest && @contest.public
|
||||
skip_authorization
|
||||
not_found and return
|
||||
end
|
||||
authorize @contest
|
||||
|
||||
I18n.locale = @contest.lang
|
||||
|
||||
@title = I18n.t("contests.scoreboard.title", name: @contest.name)
|
||||
@contestants = @contest.contestants.order(: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
|
||||
@puzzles = @contest.puzzles.order(:id)
|
||||
@action_name = t("helpers.buttons.refresh")
|
||||
if params.key?(:category)
|
||||
@action_path = "/public/#{@contest.friendly_id}?category=#{params[:category]}"
|
||||
else
|
||||
@action_path = "/public/#{@contest.friendly_id}"
|
||||
end
|
||||
render :scoreboard
|
||||
end
|
||||
|
||||
@@ -72,8 +103,8 @@ class ContestsController < ApplicationController
|
||||
|
||||
def set_badges
|
||||
@badges = []
|
||||
@badges.push("team") if @contest.team
|
||||
@badges.push("registration") if @contest.allow_registration
|
||||
@badges.push(t("helpers.badges.team")) if @contest.team
|
||||
@badges.push(t("helpers.badges.registration")) if @contest.allow_registration
|
||||
end
|
||||
|
||||
def set_contest
|
||||
@@ -81,6 +112,16 @@ class ContestsController < ApplicationController
|
||||
end
|
||||
|
||||
def contest_params
|
||||
params.expect(contest: [ :name, :team, :allow_registration ])
|
||||
params.expect(contest: [ :lang, :name, :public, :team, :allow_registration ])
|
||||
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
|
||||
|
||||
@@ -1,25 +1,113 @@
|
||||
class MessagesController < ApplicationController
|
||||
allow_unauthenticated_access
|
||||
skip_before_action :verify_authenticity_token
|
||||
include CompletionsConcern
|
||||
|
||||
skip_before_action :verify_authenticity_token, only: %i[ create connect cors_preflight_check ]
|
||||
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_data, only: %i[ convert ]
|
||||
|
||||
def self.local_prefixes
|
||||
super + [ "completions" ]
|
||||
end
|
||||
|
||||
def cors_set_access_control_headers
|
||||
response.set_header("Access-Control-Allow-Origin", "*")
|
||||
response.set_header("Access-Control-Allow-Credentials", "true")
|
||||
response.set_header("Access-Control-Allow-Methods", "POST")
|
||||
response.set_header("Access-Control-Allow-Headers", "*")
|
||||
response.set_header("Access-Control-Max-Age", "86400")
|
||||
end
|
||||
|
||||
def cors_preflight_check
|
||||
skip_authorization
|
||||
end
|
||||
|
||||
def connect
|
||||
skip_authorization
|
||||
|
||||
if !params.key?(:token)
|
||||
respond_to do |format|
|
||||
format.json { render json: { error: "no token provided" }, status: 400 }
|
||||
end
|
||||
else
|
||||
@contest = Contest.find_by_token_for(:token, params[:token])
|
||||
if @contest
|
||||
respond_to do |format|
|
||||
format.json { render json: { name: @contest.name }, status: 200 }
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.json { render json: { error: "invalid token" }, status: 400 }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
skip_authorization
|
||||
|
||||
@message_params = message_params
|
||||
begin
|
||||
@contest = Contest.find_by_token_for(:token, params[:token])
|
||||
@message = Message.new(text: params[:text], time_seconds: params[:time_seconds], contest: @contest)
|
||||
@message = Message.new(text: params[:text], author: params[:author], time_seconds: params[:time_seconds],
|
||||
display_time: display_time(params[:time_seconds]), contest: @contest)
|
||||
if @contest && @message.save
|
||||
respond_to do |format|
|
||||
format.json { render json: {}, status: 200 }
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.json { render json: { error: "invalid contest token" }, status: 400 }
|
||||
format.json { render json: { error: "invalid token" }, status: 400 }
|
||||
end
|
||||
end
|
||||
rescue
|
||||
respond_to do |format|
|
||||
format.json { render json: { error: "invalid token" }, status: 400 }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def message_params
|
||||
params.expect(message: [ :text, :time_seconds, :token ])
|
||||
def convert
|
||||
authorize @contest
|
||||
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
|
||||
@completion = Completion.new()
|
||||
@completion.display_time_from_start = @message.display_time
|
||||
|
||||
render "completions/new"
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @contest
|
||||
|
||||
@message = Message.find(params[:id])
|
||||
@message.destroy
|
||||
redirect_to contest_path(@contest), notice: t("messages.destroy.notice")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_contest
|
||||
@contest = Contest.find(params[:contest_id])
|
||||
end
|
||||
|
||||
def set_data
|
||||
@message = Message.find(params[:message_id])
|
||||
@puzzles = @contest.puzzles
|
||||
@contestants = @contest.contestants.order(:name)
|
||||
if @contestants.size > 0
|
||||
@closest_contestant = @contestants.first
|
||||
closest_distance = 10000
|
||||
@contestants.each do |contestant|
|
||||
distance = DamerauLevenshtein.distance(@message.author, contestant.name)
|
||||
if distance < closest_distance
|
||||
closest_distance = distance
|
||||
@closest_contestant = contestant
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,11 +4,16 @@ class PuzzlesController < ApplicationController
|
||||
|
||||
def edit
|
||||
authorize @contest
|
||||
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
end
|
||||
|
||||
def new
|
||||
authorize @contest
|
||||
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
@puzzle = Puzzle.new
|
||||
end
|
||||
|
||||
@@ -18,8 +23,10 @@ class PuzzlesController < ApplicationController
|
||||
@puzzle = Puzzle.new(puzzle_params)
|
||||
@puzzle.contest_id = @contest.id
|
||||
if @puzzle.save
|
||||
redirect_to contest_path(@contest)
|
||||
redirect_to contest_path(@contest), notice: t("puzzles.new.notice")
|
||||
else
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
@@ -28,8 +35,10 @@ class PuzzlesController < ApplicationController
|
||||
authorize @contest
|
||||
|
||||
if @puzzle.update(puzzle_params)
|
||||
redirect_to @contest
|
||||
redirect_to @contest, notice: t("puzzles.edit.notice")
|
||||
else
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
@@ -38,7 +47,7 @@ class PuzzlesController < ApplicationController
|
||||
authorize @contest
|
||||
|
||||
@puzzle.destroy
|
||||
redirect_to contest_path(@contest)
|
||||
redirect_to contest_path(@contest), notice: t("puzzles.destroy.notice")
|
||||
end
|
||||
|
||||
private
|
||||
@@ -52,6 +61,6 @@ class PuzzlesController < ApplicationController
|
||||
end
|
||||
|
||||
def puzzle_params
|
||||
params.expect(puzzle: [ :brand, :name, :image ])
|
||||
params.expect(puzzle: [ :brand, :name, :image, :pieces ])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,7 +9,7 @@ class SessionsController < ApplicationController
|
||||
def create
|
||||
if user = User.authenticate_by(params.permit(:email_address, :password))
|
||||
start_new_session_for user
|
||||
redirect_to after_authentication_url
|
||||
redirect_to after_authentication_url, notice: t("sessions.new.notice")
|
||||
else
|
||||
redirect_to new_session_path, alert: "Try another email address or password."
|
||||
end
|
||||
|
||||
@@ -15,7 +15,7 @@ class UsersController < ApplicationController
|
||||
authorize @user
|
||||
|
||||
if @user.update(user_params)
|
||||
redirect_to contests_path
|
||||
redirect_to contests_path, notice: t("users.edit.notice")
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
@@ -38,7 +38,7 @@ class UsersController < ApplicationController
|
||||
|
||||
@user = User.new(user_params)
|
||||
if @user.save
|
||||
redirect_to users_path
|
||||
redirect_to users_path, notice: t("users.new.notice")
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
@@ -1,2 +1,20 @@
|
||||
module ContestsHelper
|
||||
def pad(n)
|
||||
if n > 9
|
||||
return n.to_s
|
||||
end
|
||||
"0" + n.to_s
|
||||
end
|
||||
|
||||
def display_time(time)
|
||||
h = time / 3600
|
||||
m = (time % 3600) / 60
|
||||
s = (time % 3600) % 60
|
||||
if h > 0
|
||||
return h.to_s + ":" + pad(m) + ":" + pad(s)
|
||||
elsif m > 0
|
||||
return m.to_s + ":" + pad(s)
|
||||
end
|
||||
"0:" + pad(s)
|
||||
end
|
||||
end
|
||||
|
||||
14
app/lib/forms.rb
Normal file
14
app/lib/forms.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
module Forms
|
||||
class CsvConversionForm
|
||||
include ActiveModel::Model
|
||||
include ActiveModel::Attributes
|
||||
include ActiveModel::Validations::Callbacks
|
||||
include ActiveRecord::Transactions
|
||||
|
||||
attribute :name_column, :integer
|
||||
attribute :email_column, :integer
|
||||
|
||||
validates :name_column, presence: true
|
||||
validates_numericality_of :name_column, greater_than: -1
|
||||
end
|
||||
end
|
||||
@@ -1,3 +1,3 @@
|
||||
module Languages
|
||||
AVAILABLE_LANGUAGES = [ { id: "en", name: "English" }, { id: "fr", name: "French" } ]
|
||||
AVAILABLE_LANGUAGES = [ { id: "en", name: "English" }, { id: "fr", name: "Français" } ]
|
||||
end
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: puzzles
|
||||
# Table name: categories
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# brand :string
|
||||
# name :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
@@ -11,16 +10,15 @@
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_puzzles_on_contest_id (contest_id)
|
||||
# index_categories_on_contest_id (contest_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# contest_id (contest_id => contests.id)
|
||||
#
|
||||
require "test_helper"
|
||||
class Category < ApplicationRecord
|
||||
belongs_to :contest
|
||||
has_and_belongs_to_many :contestants
|
||||
|
||||
class PuzzleTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
validates :name, presence: true
|
||||
end
|
||||
@@ -5,31 +5,64 @@
|
||||
# id :integer not null, primary key
|
||||
# display_relative_time :string
|
||||
# display_time_from_start :string
|
||||
# remaining_pieces :integer
|
||||
# time_seconds :integer
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# contest_id :integer not null
|
||||
# contestant_id :integer not null
|
||||
# message_id :integer
|
||||
# puzzle_id :integer not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_completions_on_contest_id (contest_id)
|
||||
# index_completions_on_contestant_id (contestant_id)
|
||||
# index_completions_on_message_id (message_id)
|
||||
# index_completions_on_puzzle_id (puzzle_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# contest_id (contest_id => contests.id)
|
||||
# contestant_id (contestant_id => contestants.id)
|
||||
# message_id (message_id => messages.id)
|
||||
# puzzle_id (puzzle_id => puzzles.id)
|
||||
#
|
||||
class Completion < ApplicationRecord
|
||||
belongs_to :contest
|
||||
belongs_to :contestant
|
||||
belongs_to :puzzle
|
||||
belongs_to :message, optional: true
|
||||
|
||||
validates :time_seconds, presence: true
|
||||
validates_numericality_of :time_seconds
|
||||
validates :puzzle_id, uniqueness: { scope: :contestant }
|
||||
before_save :add_time_seconds
|
||||
before_save :nullify_display_time
|
||||
|
||||
validates :display_time_from_start, presence: true, format: { with: /\A(((\d\d|\d):\d\d|\d\d|\d):\d\d|\d\d|\d)\z/ }, if: -> { remaining_pieces == nil }
|
||||
validates :contestant_id, uniqueness: { scope: :puzzle }, if: -> { contest.puzzles.size == 1 }
|
||||
validates :puzzle_id, uniqueness: { scope: :contestant }, if: -> { contest.puzzles.size > 1 }
|
||||
validate :remaining_pieces_is_correct
|
||||
|
||||
def remaining_pieces_is_correct
|
||||
if self.remaining_pieces && self.remaining_pieces > self.puzzle.pieces
|
||||
errors.add(:remaining_pieces, "Cannot be greater than the number of pieces for this puzzle")
|
||||
end
|
||||
end
|
||||
|
||||
def nullify_display_time
|
||||
if self.remaining_pieces
|
||||
self.display_time_from_start = nil
|
||||
self.display_relative_time = nil
|
||||
end
|
||||
end
|
||||
|
||||
def add_time_seconds
|
||||
arr = display_time_from_start.split(":")
|
||||
if arr.size == 3
|
||||
self.time_seconds = arr[0].to_i * 3600 + arr[1].to_i * 60 + arr[2].to_i
|
||||
elsif arr.size == 2
|
||||
self.time_seconds = arr[0].to_i * 60 + arr[1].to_i
|
||||
elsif arr.size == 1
|
||||
self.time_seconds = arr[0].to_i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# allow_registration :boolean default(FALSE)
|
||||
# lang :string default("en")
|
||||
# name :string
|
||||
# public :boolean default(FALSE)
|
||||
# slug :string
|
||||
# team :boolean default(FALSE)
|
||||
# created_at :datetime not null
|
||||
@@ -24,14 +26,16 @@ class Contest < ApplicationRecord
|
||||
extend FriendlyId
|
||||
|
||||
belongs_to :user
|
||||
has_many :categories
|
||||
has_many :completions, dependent: :destroy
|
||||
has_many :contestants, dependent: :destroy
|
||||
has_many :puzzles, dependent: :destroy
|
||||
has_many :messages
|
||||
has_many :messages, dependent: :destroy
|
||||
|
||||
friendly_id :name, use: :slugged
|
||||
|
||||
validates :name, presence: true
|
||||
validates :lang, inclusion: { in: Languages::AVAILABLE_LANGUAGES.map { |lang| lang[:id] } }
|
||||
|
||||
generates_token_for :token
|
||||
end
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
# display_time :string
|
||||
# email :string
|
||||
# name :string
|
||||
# time_seconds :integer
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# contest_id :integer not null
|
||||
@@ -20,7 +21,27 @@
|
||||
#
|
||||
class Contestant < ApplicationRecord
|
||||
belongs_to :contest
|
||||
has_many :completions
|
||||
has_many :completions, dependent: :destroy
|
||||
has_and_belongs_to_many :categories
|
||||
|
||||
before_validation :initialize_time_seconds_if_empty
|
||||
|
||||
validates :name, presence: true
|
||||
validates :time_seconds, presence: true
|
||||
|
||||
def form_name
|
||||
if email.present?
|
||||
"#{name} - #{email}"
|
||||
else
|
||||
name
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def initialize_time_seconds_if_empty
|
||||
if !self.time_seconds
|
||||
self.time_seconds = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
51
app/models/csv_import.rb
Normal file
51
app/models/csv_import.rb
Normal file
@@ -0,0 +1,51 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: csv_imports
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# content :string not null
|
||||
# separator :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
class CsvImport < ApplicationRecord
|
||||
enum :separator, { comma: ",", semicolon: ";" }, suffix: true, default: :comma
|
||||
|
||||
has_one_attached :file
|
||||
|
||||
validates :file, presence: true
|
||||
validate :acceptable_csv, on: :create
|
||||
|
||||
before_save :read_csv
|
||||
|
||||
def acceptable_csv
|
||||
return unless file.attached?
|
||||
|
||||
if file.blob.byte_size > 5 * 1024 * 1024
|
||||
errors.add(:file, "this csv file is too large, it must be under 5MB")
|
||||
return
|
||||
end
|
||||
|
||||
if file.content_type != "text/csv"
|
||||
errors.add(:file, :not_a_csv_file)
|
||||
return
|
||||
end
|
||||
|
||||
begin
|
||||
csv = CSV.read(attachment_changes["file"].attachable.path, col_sep: separator_for_database)
|
||||
|
||||
errors.add(:file, :empty) if csv.count < 1 || (csv.count == 1 && csv[0].count == 1 && csv[0][0] == "")
|
||||
rescue CSV::MalformedCSVError => e
|
||||
errors.add(:file, e.message)
|
||||
end
|
||||
end
|
||||
|
||||
def read_csv
|
||||
self.content = JSON.dump(CSV.read(attachment_changes["file"].attachable.path, col_sep: separator_for_database))
|
||||
end
|
||||
|
||||
def options_for_separator
|
||||
keys = self.class.separators.keys
|
||||
keys.map(&:humanize).zip(keys).to_h
|
||||
end
|
||||
end
|
||||
@@ -3,6 +3,8 @@
|
||||
# Table name: messages
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# author :string
|
||||
# display_time :string
|
||||
# text :string not null
|
||||
# time_seconds :integer not null
|
||||
# created_at :datetime not null
|
||||
@@ -19,6 +21,8 @@
|
||||
#
|
||||
class Message < ApplicationRecord
|
||||
belongs_to :contest
|
||||
has_many :completions, dependent: :nullify
|
||||
|
||||
validates :author, presence: true
|
||||
validates :text, presence: true
|
||||
end
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
# id :integer not null, primary key
|
||||
# brand :string
|
||||
# name :string
|
||||
# pieces :integer not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# contest_id :integer not null
|
||||
@@ -20,9 +21,9 @@
|
||||
class Puzzle < ApplicationRecord
|
||||
belongs_to :contest
|
||||
|
||||
has_many :completions
|
||||
has_many :completions, dependent: :destroy
|
||||
has_one_attached :image
|
||||
|
||||
validates :name, presence: true
|
||||
validates :brand, presence: true
|
||||
validates :pieces, presence: true
|
||||
end
|
||||
|
||||
@@ -23,5 +23,6 @@ class User < ApplicationRecord
|
||||
normalizes :email_address, with: ->(e) { e.strip.downcase }
|
||||
|
||||
validates :username, presence: true, uniqueness: true
|
||||
validates :email_address, presence: true, uniqueness: true
|
||||
validates :lang, inclusion: { in: Languages::AVAILABLE_LANGUAGES.map { |lang| lang[:id] } }
|
||||
end
|
||||
|
||||
@@ -15,10 +15,22 @@ class ContestPolicy < ApplicationPolicy
|
||||
true
|
||||
end
|
||||
|
||||
def convert?
|
||||
record.user.id == user.id || user.admin?
|
||||
end
|
||||
|
||||
def convert_csv?
|
||||
record.user.id == user.id || user.admin?
|
||||
end
|
||||
|
||||
def edit?
|
||||
record.user.id == user.id || user.admin?
|
||||
end
|
||||
|
||||
def finalize_import?
|
||||
record.user.id == user.id || user.admin?
|
||||
end
|
||||
|
||||
def update?
|
||||
record.user.id == user.id || user.admin?
|
||||
end
|
||||
@@ -27,7 +39,19 @@ class ContestPolicy < ApplicationPolicy
|
||||
record.user.id == user.id || user.admin?
|
||||
end
|
||||
|
||||
def import?
|
||||
record.user.id == user.id || user.admin?
|
||||
end
|
||||
|
||||
def export?
|
||||
record.user.id == user.id || user.admin?
|
||||
end
|
||||
|
||||
def scoreboard?
|
||||
true
|
||||
end
|
||||
|
||||
def upload_csv?
|
||||
record.user.id == user.id || user.admin?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,19 +1,48 @@
|
||||
= form_with model: completion, url: url, method: method do |form|
|
||||
- if @message
|
||||
= form.hidden_field :message_id, value: @message.id
|
||||
.row.mb-3
|
||||
.col
|
||||
h4 = t("messages.singular").capitalize
|
||||
.alert.alert-secondary
|
||||
b
|
||||
= @message.author
|
||||
br
|
||||
= @message.text
|
||||
.row
|
||||
.col
|
||||
h4 = t("completions.singular").capitalize
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-floating
|
||||
= form.text_field :time_seconds, autocomplete: "off", class: "form-control"
|
||||
= form.label :time_seconds, class: "required"
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-floating
|
||||
= form.select :contestant_id, @contestants.map { |contestant| [contestant.name, contestant.id] }, {}, class: "form-select"
|
||||
= form.select :contestant_id, @contestants.map { |contestant| [contestant.form_name, contestant.id] }, {}, class: "form-select"
|
||||
= form.label :contestant_id
|
||||
- if @closest_contestant
|
||||
javascript:
|
||||
el = document.querySelector('select[name="completion[contestant_id]"]');
|
||||
el.value = "#{@closest_contestant.id}"
|
||||
- if @puzzles.size > 1
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-floating
|
||||
= form.select :puzzle_id, @puzzles.map { |puzzle| ["#{puzzle.name} - #{puzzle.brand}", puzzle.id] }, {}, class: "form-select"
|
||||
= form.label :puzzle_id
|
||||
- elsif @puzzles.size == 1
|
||||
= form.hidden_field :puzzle_id, value: @puzzles.first.id
|
||||
- else
|
||||
= form.hidden_field :puzzle_id
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-floating
|
||||
= form.text_field :display_time_from_start, autocomplete: "off", class: "form-control"
|
||||
= form.label :display_time_from_start, class: "required"
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-floating
|
||||
= form.text_field :remaining_pieces, autocomplete: "off", class: "form-control"
|
||||
= form.label :remaining_pieces
|
||||
.form-text
|
||||
= t("activerecord.attributes.completion.remaining_pieces_description")
|
||||
.row
|
||||
.col
|
||||
= form.submit submit_text, class: "btn btn-primary"
|
||||
@@ -1 +1 @@
|
||||
= render "form", contest: @contest, completion: @completion, submit_text: "Save", method: :patch, url: "/contests/#{@contest.id}/completions/#{@completion.id}"
|
||||
= render "form", contest: @contest, completion: @completion, submit_text: t("helpers.buttons.save"), method: :patch, url: "/contests/#{@contest.id}/completions/#{@completion.id}"
|
||||
@@ -1 +1 @@
|
||||
= render "form", completion: @completion, submit_text: "Create", method: :post, url: "/contests/#{@contest.id}/completions"
|
||||
= render "form", completion: @completion, submit_text: t("helpers.buttons.create"), method: :post, url: "/contests/#{@contest.id}/completions"
|
||||
@@ -13,11 +13,20 @@
|
||||
.form-floating
|
||||
= form.text_field :email, autocomplete: "off", class: "form-control"
|
||||
= form.label :email
|
||||
.form-text Optional. Fill this only if you intend to send emails through this app.
|
||||
.form-text
|
||||
= t("activerecord.attributes.contestant.email_description")
|
||||
- if @contest.categories
|
||||
.row.mt-4
|
||||
.col
|
||||
- @contest.categories.each do |category|
|
||||
.form-check.form-switch
|
||||
= form.check_box "category_#{category.id}".to_sym, class: "form-check-input", checked: @contestant.categories.where(id: category.id).any?
|
||||
= form.label category.name
|
||||
|
||||
.row.mt-4
|
||||
.col
|
||||
- if method == :patch
|
||||
= link_to "Delete", contest_contestant_path(contest, contestant), data: { turbo_method: :delete }, class: "btn btn-danger me-2"
|
||||
= link_to t("helpers.buttons.delete"), contest_contestant_path(contest, contestant), data: { turbo_method: :delete }, class: "btn btn-danger me-2"
|
||||
= form.submit submit_text, class: "btn btn-primary"
|
||||
|
||||
- if method == :patch
|
||||
@@ -27,27 +36,39 @@
|
||||
table.table.table-striped.table-hover
|
||||
thead
|
||||
tr
|
||||
- if @contest.puzzles.size > 1
|
||||
th scope="col"
|
||||
| Time since start
|
||||
= t("activerecord.attributes.completion.display_time_from_start")
|
||||
th scope="col"
|
||||
| Relative time
|
||||
= t("activerecord.attributes.completion.display_relative_time")
|
||||
- else
|
||||
th scope="col"
|
||||
| Puzzle
|
||||
= t("activerecord.attributes.completion.display_time")
|
||||
th scope="col"
|
||||
= t("activerecord.attributes.completion.remaining_pieces")
|
||||
th scope="col"
|
||||
= t("activerecord.attributes.completion.puzzle")
|
||||
tbody
|
||||
- @completions.each do |completion|
|
||||
tr scope="row"
|
||||
td
|
||||
= completion.display_time_from_start
|
||||
- if @contest.puzzles.size > 1
|
||||
td
|
||||
= completion.display_relative_time
|
||||
td
|
||||
| #{completion.puzzle.name} - #{completion.puzzle.brand}
|
||||
= completion.remaining_pieces
|
||||
td
|
||||
a.btn.btn-sm.btn-secondary.me-2 href=edit_contest_completion_path(@contest, completion, contestant.id)
|
||||
| Edit
|
||||
= link_to "Delete", contest_completion_path(contest, completion, contestant_id: contestant.id),
|
||||
- if !completion.puzzle.brand.blank?
|
||||
| #{completion.puzzle.name} - #{completion.puzzle.brand}
|
||||
- else
|
||||
| #{completion.puzzle.name}
|
||||
td
|
||||
a.btn.btn-sm.btn-secondary.me-2 href=edit_contest_completion_path(@contest, completion, contestant_id: contestant.id)
|
||||
= t("helpers.buttons.edit")
|
||||
= link_to t("helpers.buttons.delete"), contest_completion_path(contest, completion, contestant_id: contestant.id),
|
||||
data: { turbo_method: :delete }, class: "btn btn-sm btn-secondary"
|
||||
.row
|
||||
.col
|
||||
a.btn.btn-primary href=new_contest_completion_path(@contest, contestant_id: contestant.id)
|
||||
| Add completion
|
||||
= t("helpers.buttons.add")
|
||||
|
||||
40
app/views/contestants/convert_csv.html.slim
Normal file
40
app/views/contestants/convert_csv.html.slim
Normal file
@@ -0,0 +1,40 @@
|
||||
= form_with model: @form, url: "/contests/#{@contest.id}/import/#{@csv_import.id}" do |form|
|
||||
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-floating
|
||||
= form.select :name_column, [[t("helpers.none"), -1]] + Array.new(@content[0].count) {|i| [t("helpers.field") + "_#{i}", i] }, {}, class: "form-select"
|
||||
= form.label :name_column
|
||||
= t("contestants.import.name_column")
|
||||
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-floating
|
||||
= form.select :email_column, [[t("helpers.none"), -1]] + Array.new(@content[0].count) {|i| [t("helpers.field") + "_#{i}", i] }, {}, class: "form-select"
|
||||
= form.label :email_column
|
||||
= t("contestants.import.email_column")
|
||||
|
||||
.row.mb-3
|
||||
.col
|
||||
= form.submit t("helpers.buttons.confirm"), class: "btn btn-primary"
|
||||
|
||||
.row.g-3
|
||||
.col
|
||||
table.table.table-striped.table-hover
|
||||
thead
|
||||
tr
|
||||
- @content[0].each_with_index do |_, i|
|
||||
th scope="col"
|
||||
= t("helpers.field") + "_#{i}"
|
||||
th scope="col" style="white-space: nowrap"
|
||||
= t("contestants.import.import_column")
|
||||
tbody
|
||||
- @content.each_with_index do |row, i|
|
||||
tr scope="row"
|
||||
- row.each do |value|
|
||||
td
|
||||
= value
|
||||
td
|
||||
.form-check.form-switch
|
||||
= form.check_box "row_#{i}".to_sym, class: "form-check-input", checked: true
|
||||
|
||||
@@ -1 +1 @@
|
||||
= render "form", contest: @contest, contestant: @contestant, submit_text: "Save", method: :patch, url: "/contests/#{@contest.id}/contestants/#{@contestant.id}"
|
||||
= render "form", contest: @contest, contestant: @contestant, submit_text: t("helpers.buttons.save"), method: :patch, url: "/contests/#{@contest.id}/contestants/#{@contestant.id}"
|
||||
4
app/views/contestants/export.csv.slim
Normal file
4
app/views/contestants/export.csv.slim
Normal file
@@ -0,0 +1,4 @@
|
||||
= CSV.generate_line [t("helpers.rank"), t("activerecord.attributes.contestant.name"), t("activerecord.attributes.contestant.display_time"), t("activerecord.attributes.contestant.completions")]
|
||||
|
||||
- @contestants.each_with_index do |contestant, index|
|
||||
= CSV.generate_line([index + 1, contestant.name, 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, contestant.completions.where(remaining_pieces: nil).length])
|
||||
19
app/views/contestants/import.html.slim
Normal file
19
app/views/contestants/import.html.slim
Normal file
@@ -0,0 +1,19 @@
|
||||
= form_with(model: @csv_import, url: contest_import_path(@contest), html: { novalidate: true }) do |form|
|
||||
.row.g-3
|
||||
.col
|
||||
.mb-3
|
||||
.form-floating
|
||||
= form.file_field :file, class: "form-control", accept: ".csv, text/csv"
|
||||
= form.label :file
|
||||
|
||||
.row.g-3
|
||||
.col
|
||||
.mb-3
|
||||
.form-floating
|
||||
= form.select :separator, @csv_import.options_for_separator, {}, class: "form-select"
|
||||
= form.label :separator
|
||||
|
||||
.row.g-3
|
||||
.col
|
||||
.mb-3
|
||||
= form.submit t("helpers.buttons.import"), class: "btn btn-primary"
|
||||
@@ -1 +1 @@
|
||||
= render "form", contest: @contest, contestant: @contestant, submit_text: "Add", method: :post, url: "/contests/#{@contest.id}/contestants"
|
||||
= render "form", contest: @contest, contestant: @contestant, submit_text: t("helpers.buttons.add"), method: :post, url: "/contests/#{@contest.id}/contestants"
|
||||
19
app/views/contests/_category_selector.html.slim
Normal file
19
app/views/contests/_category_selector.html.slim
Normal file
@@ -0,0 +1,19 @@
|
||||
- if @contest.categories.size > 0
|
||||
.row
|
||||
.col
|
||||
select.mb-2 id="categories" style="padding: 5px"
|
||||
option value=-1
|
||||
| Tous.tes les participant.e.s
|
||||
- @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(`/public/#{@contest.slug}?category=${e.target.value}`);
|
||||
})
|
||||
@@ -1,16 +1,27 @@
|
||||
h4.mt-5 = t("contests.form.general")
|
||||
= form_with model: contest do |form|
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-floating
|
||||
= form.text_field :name, autocomplete: "off", class: "form-control"
|
||||
= form.label :name, class: "required"
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-floating
|
||||
= form.select :lang, Languages::AVAILABLE_LANGUAGES.map { |lang| [ lang[:name], lang[:id] ] }, {}, class: "form-select"
|
||||
= form.label :lang
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-check.form-switch
|
||||
= form.check_box :public, class: "form-check-input"
|
||||
= form.label :public
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-check.form-switch
|
||||
= form.check_box :team, class: "form-check-input"
|
||||
= form.label :team
|
||||
.form-text = t("activerecord.attributes.contest.team_description")
|
||||
.row.mb-3
|
||||
.row.mb-3 style="display: none"
|
||||
.col
|
||||
.form-check.form-switch
|
||||
= form.check_box :allow_registration, class: "form-check-input"
|
||||
@@ -19,3 +30,36 @@
|
||||
.row
|
||||
.col
|
||||
= form.submit submit_text, class: "btn btn-primary"
|
||||
|
||||
|
||||
h4.mt-5 = t("contests.form.categories")
|
||||
|
||||
= form_with model: Category, url: "/contests/#{@contest.id}/categories" do |form|
|
||||
- if @contest.categories.size > 0
|
||||
.row
|
||||
.col-6
|
||||
table.table.table-striped.table-hover
|
||||
thead
|
||||
tr
|
||||
th
|
||||
= t("activerecord.attributes.category.name")
|
||||
th
|
||||
= t("activerecord.attributes.category.contestant_count")
|
||||
tbody
|
||||
- @contest.categories.each do |category|
|
||||
tr.align-middle scope="row"
|
||||
td
|
||||
= category.name
|
||||
td
|
||||
= category.contestants.size
|
||||
td
|
||||
= link_to t("helpers.buttons.delete"), contest_category_path(@contest, category), data: { turbo_method: :delete }, class: "btn btn-sm btn-danger ms-2"
|
||||
.row.mt-3
|
||||
.col-4
|
||||
.form-floating
|
||||
= form.text_field :name, autocomplete: "off", value: nil, class: "form-control"
|
||||
= form.label :name, class: "required"
|
||||
= t("activerecord.attributes.category.new")
|
||||
.row.mt-3
|
||||
.col
|
||||
= form.submit t("helpers.buttons.add"), class: "btn btn-primary"
|
||||
@@ -1,14 +1,38 @@
|
||||
css:
|
||||
@media (max-width: 800px) {
|
||||
a.btn { display: none; }
|
||||
.col-5 { display: none; }
|
||||
.col-6 { width: 100% !important; display: block !important; }
|
||||
.small-screen-image { display: block !important; }
|
||||
.container { margin-top: 2rem !important; }
|
||||
}
|
||||
|
||||
- if @contest.puzzles.size <= 1
|
||||
.row.small-screen-image style="display: none"
|
||||
- @contest.puzzles.each do |puzzle|
|
||||
.d-flex.flex-column.justify-content-center.mb-5
|
||||
= image_tag(puzzle.image, style: "max-height: 200px; object-fit: contain") if puzzle.image.attached?
|
||||
.mt-2.fs-6 style="text-align: center"
|
||||
=> "#{puzzle.name} -"
|
||||
= "#{puzzle.brand} #{puzzle.pieces}p"
|
||||
|
||||
= render "category_selector"
|
||||
|
||||
.row
|
||||
.col-6.d-flex.flex-column style="height: calc(100vh - 180px)"
|
||||
.d-flex.flex-column style="overflow-y: auto"
|
||||
table.table.table-striped.table-hover
|
||||
thead
|
||||
tr
|
||||
th scope="col"
|
||||
| Rank
|
||||
th scope="col"
|
||||
| Name
|
||||
th scope="col"
|
||||
| Completed puzzles
|
||||
th scope="col"
|
||||
| Total time
|
||||
th
|
||||
= t("helpers.rank")
|
||||
th
|
||||
= t("activerecord.attributes.contestant.name")
|
||||
- if @contest.puzzles.size > 1
|
||||
th
|
||||
= t("activerecord.attributes.contestant.completions")
|
||||
th style="width: 170px"
|
||||
= t("activerecord.attributes.contestant.display_time")
|
||||
tbody
|
||||
- @contestants.each_with_index do |contestant, index|
|
||||
tr scope="row"
|
||||
@@ -16,7 +40,55 @@ table.table.table-striped.table-hover
|
||||
= index + 1
|
||||
td
|
||||
= contestant.name
|
||||
- if @contest.puzzles.size > 1
|
||||
td
|
||||
= contestant.completions.length
|
||||
= contestant.completions.where(remaining_pieces: nil).length
|
||||
td style="position: relative"
|
||||
- if index > 0 && contestant.time_seconds > 0 && contestant.completions.where(remaining_pieces: nil).size > 0
|
||||
.relative-time style="position:absolute; margin: 1px 0 0 64px; font-size: 14px; color: grey"
|
||||
|> +
|
||||
= display_time(contestant.time_seconds - @contestants[index - 1].time_seconds)
|
||||
= 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
|
||||
.col-1
|
||||
.col-5
|
||||
- @contest.puzzles.each do |puzzle|
|
||||
= image_tag(puzzle.image, class: "img-fluid ms-3 me-3") if puzzle.image.attached?
|
||||
.mt-3.fs-4 style="margin-left: 15px"
|
||||
= puzzle.name
|
||||
.fs-6 style="margin-left: 15px"
|
||||
b
|
||||
= "#{puzzle.brand} - #{puzzle.pieces}p"
|
||||
|
||||
- else
|
||||
.d-flex.flex-column style="height: calc(100vh - 180px)"
|
||||
.d-flex.flex-row.justify-content-center.mb-5
|
||||
- @contest.puzzles.each do |puzzle|
|
||||
= image_tag(puzzle.image, class: "img-fluid ms-3 me-3", style: "max-height: 220px") if puzzle.image.attached?
|
||||
|
||||
= render "category_selector"
|
||||
|
||||
.d-flex.flex-column style="overflow-y: auto"
|
||||
table.table.table-striped.table-hover
|
||||
thead
|
||||
tr
|
||||
th scope="col"
|
||||
= t("helpers.rank")
|
||||
th scope="col"
|
||||
= t("activerecord.attributes.contestant.name")
|
||||
- if @contest.puzzles.size > 1
|
||||
th scope="col"
|
||||
= t("activerecord.attributes.contestant.completions")
|
||||
th scope="col"
|
||||
= t("activerecord.attributes.contestant.display_time")
|
||||
tbody
|
||||
- @contestants.each_with_index do |contestant, index|
|
||||
tr scope="row"
|
||||
td
|
||||
= contestant.display_time
|
||||
= index + 1
|
||||
td
|
||||
= contestant.name
|
||||
- if @contest.puzzles.size > 1
|
||||
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
|
||||
@@ -1,75 +1,170 @@
|
||||
- if @badges.size > 0 && false
|
||||
.row.mb-4
|
||||
.col
|
||||
css:
|
||||
.badges { margin-top: -18px; position: absolute; }
|
||||
.badges
|
||||
.badges style="margin-top: -18px; position: absolute"
|
||||
- @badges.each do |badge|
|
||||
span.badge.text-bg-info.me-2
|
||||
= badge
|
||||
|
||||
.row.mb-4
|
||||
.col
|
||||
.float-end
|
||||
a.btn.btn-primary href=edit_contest_path(@contest)
|
||||
| Edit contest
|
||||
p
|
||||
= t("contests.show.public_scoreboard")
|
||||
= link_to root_url + "public/#{@contest.slug}", root_url + "public/#{@contest.slug}"
|
||||
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-6
|
||||
.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")
|
||||
button.btn.btn-success.ms-3 onclick="copyExtensionUrlToClipboard()"
|
||||
css:
|
||||
button > svg {
|
||||
margin-right: 2px;
|
||||
margin-top: -3px;
|
||||
}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16">
|
||||
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1z"/>
|
||||
<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0z"/>
|
||||
</svg>
|
||||
=< 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("puzzles.plural").capitalize
|
||||
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 scope="col"
|
||||
| Image
|
||||
th scope="col"
|
||||
| Title
|
||||
th scope="col"
|
||||
| Brand
|
||||
th
|
||||
= t("helpers.rank")
|
||||
th
|
||||
= t("activerecord.attributes.contestant.name")
|
||||
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
|
||||
= index + 1
|
||||
td
|
||||
= contestant.name
|
||||
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-width: 140px;") if puzzle.image.attached?
|
||||
= 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)
|
||||
| Edit
|
||||
.row
|
||||
= t("helpers.buttons.edit")
|
||||
- if @messages
|
||||
.row.mt-5
|
||||
.col
|
||||
a.btn.btn-primary href=new_contest_puzzle_path(@contest)
|
||||
= t("contests.show.add_puzzle")
|
||||
.col-6
|
||||
h4 = t("messages.plural").capitalize
|
||||
- if @puzzles.size == 0
|
||||
.row
|
||||
.col
|
||||
h4 = t("contestants.plural").capitalize
|
||||
.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"
|
||||
| Name
|
||||
= t("activerecord.attributes.message.time")
|
||||
th scope="col"
|
||||
| Completed puzzles
|
||||
= t("activerecord.attributes.message.author")
|
||||
th.w-25 scope="col"
|
||||
= t("activerecord.attributes.message.text")
|
||||
th.w-25 scope="col"
|
||||
tbody
|
||||
- @contestants.each do |contestant|
|
||||
tr scope="row"
|
||||
- @messages.each do |message|
|
||||
tr.align-middle scope="row"
|
||||
td style="text-align: center"
|
||||
- if message.completions.size > 0
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-square" viewBox="0 0 16 16">
|
||||
<path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"/>
|
||||
<path d="M10.97 4.97a.75.75 0 0 1 1.071 1.05l-3.992 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425z"/>
|
||||
</svg>
|
||||
td
|
||||
= contestant.name
|
||||
= message.display_time
|
||||
td
|
||||
= contestant.completions.length
|
||||
= message.author
|
||||
td
|
||||
a.btn.btn-sm.btn-secondary href=edit_contest_contestant_path(@contest, contestant)
|
||||
| Open
|
||||
a.btn.btn-sm.btn-secondary.ms-2 href=new_contest_completion_path(@contest, contestant_id: contestant.id)
|
||||
| Add completion
|
||||
.row.mt-4
|
||||
.col
|
||||
a.btn.btn-primary href=new_contest_contestant_path(@contest)
|
||||
= t("contests.show.add_participant")
|
||||
= 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"
|
||||
|
||||
@@ -8,14 +8,45 @@ html
|
||||
.float-end style="margin-top: -8px;"
|
||||
nav.navbar.bg-body-primary
|
||||
- if @current_user.admin
|
||||
a.navbar-brand href=users_path
|
||||
a.navbar-brand href=users_path class="btn btn-light" style="margin-right: 0"
|
||||
= t("nav.users")
|
||||
a.navbar-brand href=contests_path
|
||||
a.navbar-brand href=contests_path class="btn btn-light" style="margin-right: 0"
|
||||
= t("nav.home")
|
||||
a.navbar-brand href=user_path(@current_user)
|
||||
a.navbar-brand href=user_path(@current_user) class="btn btn-light"
|
||||
= t("nav.settings")
|
||||
= button_to t("nav.log_out"), session_path, method: :delete
|
||||
= button_to t("nav.log_out"), session_path, method: :delete, class: "btn btn-danger"
|
||||
|
||||
h1.mb-4 = @title
|
||||
css:
|
||||
.toast {
|
||||
opacity: 0;
|
||||
animation: fadeInAndOut 6s linear;
|
||||
}
|
||||
@keyframes fadeInAndOut {
|
||||
0%, 5%, 100% { opacity: 0 }
|
||||
7%, 85% { opacity: 1 }
|
||||
}
|
||||
javascript:
|
||||
function closeToast(event) {
|
||||
event.target.parentElement.parentElement.style.display = 'none';
|
||||
}
|
||||
|
||||
.toast-container.position-fixed.p-3 style="right: 30px; top: 85px"
|
||||
- flash.each do |type, msg|
|
||||
.toast role="alert" aria-live="assertive" aria-atomic="true" style="display: block"
|
||||
.toast-header
|
||||
strong.me-auto
|
||||
i.bi-bell-fill.fs-6.text-primary
|
||||
=< type.humanize
|
||||
small.text-body-secondary
|
||||
| Just now
|
||||
button.btn-close type="button" data-bs-dismiss="toast" aria-label="Close" onclick="closeToast(event)"
|
||||
.toast-body
|
||||
= msg
|
||||
|
||||
h1.mb-4
|
||||
= @title
|
||||
- if @action_path
|
||||
a.ms-4.btn.btn-primary href=@action_path style="margin-top: -6px"
|
||||
= @action_name
|
||||
|
||||
= yield
|
||||
@@ -1,4 +1,10 @@
|
||||
= form_with model: puzzle, url: url, method: method do |form|
|
||||
.row.mb-3
|
||||
.col.alert.alert-warning
|
||||
= t("puzzles.form.fake_data_recommendation")
|
||||
.row.mb-3
|
||||
.col
|
||||
= image_tag(puzzle.image, class: "img-fluid", style: "max-height: 256px") if puzzle.image.attached?
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-floating
|
||||
@@ -11,10 +17,32 @@
|
||||
= form.label :brand, class: "required"
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-text Select an image
|
||||
.form-floating
|
||||
= form.number_field :pieces, autocomplete: "off", class: "form-control"
|
||||
= form.label :pieces, class: "required"
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-text.mb-1
|
||||
= t("puzzles.image_select")
|
||||
= form.file_field :image, accept: "image/*", class: "form-control"
|
||||
.form-text.error-message style="display: none;" id="image-error-message"
|
||||
= t("puzzles.form.file_too_big")
|
||||
javascript:
|
||||
function setMaxUploadSize() {
|
||||
const el = document.querySelector('input[type="file"]');
|
||||
el.onchange = function() {
|
||||
if(this.files[0].size > 2 * 1024 * 1024) {
|
||||
document.getElementById('image-error-message').style.display = 'block';
|
||||
this.value = "";
|
||||
} else {
|
||||
document.getElementById('image-error-message').style.display = 'none';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
setMaxUploadSize();
|
||||
.row.mt-4
|
||||
.col
|
||||
- if method == :patch
|
||||
= link_to "Delete", contest_puzzle_path(contest, puzzle), data: { turbo_method: :delete }, class: "btn btn-danger me-2"
|
||||
= link_to t("helpers.buttons.delete"), contest_puzzle_path(contest, puzzle), data: { turbo_method: :delete }, class: "btn btn-danger me-2"
|
||||
= form.submit submit_text, class: "btn btn-primary"
|
||||
@@ -1 +1 @@
|
||||
= render "form", contest: @contest, puzzle: @puzzle, submit_text: "Save", method: :patch, url: "/contests/#{@contest.id}/puzzles/#{@puzzle.id}"
|
||||
= render "form", contest: @contest, puzzle: @puzzle, submit_text: t("helpers.buttons.save"), method: :patch, url: "/contests/#{@contest.id}/puzzles/#{@puzzle.id}"
|
||||
@@ -1 +1 @@
|
||||
= render "form", contest: @contest, puzzle: @puzzle, submit_text: "Add", method: :post, url: "/contests/#{@contest.id}/puzzles"
|
||||
= render "form", contest: @contest, puzzle: @puzzle, submit_text: t("helpers.buttons.add"), method: :post, url: "/contests/#{@contest.id}/puzzles"
|
||||
@@ -4,10 +4,12 @@
|
||||
.form-floating
|
||||
= form.email_field :email_address, autocomplete: "username", required: true, autofocus: true, class: "form-control"
|
||||
= form.label :email_address, class: "required"
|
||||
= t("activerecord.attributes.session.email_address")
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-floating
|
||||
= form.password_field :password, autocomplete: "current-password", required: true, autofocus: true, class: "form-control", maxlength: 72
|
||||
= form.label :password, class: "required"
|
||||
= t("activerecord.attributes.session.password")
|
||||
|
||||
= form.submit "Sign in"
|
||||
= form.submit t("helpers.buttons.sign_in")
|
||||
@@ -28,77 +28,233 @@
|
||||
# enabled: "ON"
|
||||
|
||||
en:
|
||||
activemodel:
|
||||
errors:
|
||||
models:
|
||||
forms/csv_conversion_form:
|
||||
attributes:
|
||||
name_column:
|
||||
blank: "Participant names are required"
|
||||
greater_than: "Participant names are required"
|
||||
activerecord:
|
||||
attributes:
|
||||
category:
|
||||
contestant_count: Contestants count
|
||||
new: New category
|
||||
name: Category
|
||||
completion:
|
||||
contestant: Participant
|
||||
display_time: Time
|
||||
display_time_from_start: Time since start
|
||||
display_relative_time: Time for this puzzle
|
||||
puzzle: Puzzle
|
||||
remaining_pieces: Remaining pieces
|
||||
remaining_pieces_description: When this field is filled, the above time will not be taken into account
|
||||
contest:
|
||||
name: "Name"
|
||||
team: "Team contest"
|
||||
team_description: "For UI display purposes mainly"
|
||||
allow_registration: "Allow registration"
|
||||
allow_registration_description: "Generates a shareable registration form for this contest"
|
||||
lang: Language for the public scoreboard
|
||||
name: Name
|
||||
public: Enable the public scoreboard
|
||||
team: Team contest
|
||||
team_description: For UI display purposes mainly
|
||||
allow_registration: Allow registration
|
||||
allow_registration_description: Generates a shareable registration form for this contest
|
||||
contestant:
|
||||
completions: completions
|
||||
display_time: Time
|
||||
email: Email
|
||||
name: Name
|
||||
email_description: Optional. Used for sending emails through this app, or for identifying participants whose gmeet handle doesn't match their registered name.
|
||||
csv_import:
|
||||
file: File
|
||||
separator: Separator
|
||||
message:
|
||||
author: Author
|
||||
processed: Processed?
|
||||
text: Content
|
||||
time: Time
|
||||
puzzle:
|
||||
brand: Brand
|
||||
image: Image
|
||||
name: Name
|
||||
pieces: Number of pieces
|
||||
session:
|
||||
email_address: Email address
|
||||
password: Password
|
||||
user:
|
||||
username: "Username"
|
||||
email_address: "Email address"
|
||||
lang: "Language"
|
||||
password: "New password"
|
||||
username: Username
|
||||
email_address: Email address
|
||||
lang: Language
|
||||
password: New password
|
||||
errors:
|
||||
models:
|
||||
completion:
|
||||
attributes:
|
||||
contestant_id:
|
||||
taken: "This contestant has already completed the puzzle"
|
||||
display_time_from_start:
|
||||
blank: Mandatory
|
||||
invalid: "Allowed formats: xx:xx:xx, x:xx:xx, xx:xx, x:xx, xx"
|
||||
puzzle_id:
|
||||
taken: "This contestant has already completed this puzzle"
|
||||
contest:
|
||||
attributes:
|
||||
name:
|
||||
blank: The contest name cannot be empty
|
||||
contestant:
|
||||
attributes:
|
||||
name:
|
||||
blank: The participant name cannot be empty
|
||||
csv_import:
|
||||
attributes:
|
||||
file:
|
||||
blank: "No file selected"
|
||||
empty: "This file is empty"
|
||||
not_a_csv_file: "it must be a CSV file"
|
||||
puzzle:
|
||||
attributes:
|
||||
name:
|
||||
blank: The puzzle name cannot be empty
|
||||
pieces:
|
||||
blank: It's mandatory to provide the number of pieces
|
||||
user:
|
||||
attributes:
|
||||
email_address:
|
||||
blank: Your email cannot be empty
|
||||
username:
|
||||
blank: Your username cannot be empty
|
||||
categories:
|
||||
destroy:
|
||||
notice: Category deleted
|
||||
new:
|
||||
error: The category name can't be empty
|
||||
notice: Category added
|
||||
completions:
|
||||
destroy:
|
||||
notice: Completion deleted
|
||||
edit:
|
||||
title: "Edit completion"
|
||||
notice: Completion updated
|
||||
title: Edit completion
|
||||
new:
|
||||
title: "New completion"
|
||||
notice: Completion added
|
||||
title: New completion
|
||||
singular: completion
|
||||
contests:
|
||||
destroy:
|
||||
notice: Contest deleted
|
||||
edit:
|
||||
title: "Edit contest settings"
|
||||
notice: Contest updated
|
||||
title: Edit contest settings
|
||||
form:
|
||||
categories: Participant categories
|
||||
general: General parameters
|
||||
index:
|
||||
title: "Welcome %{username}!"
|
||||
manage_contests: "Manage my contests"
|
||||
new_contest: "Create a new contest"
|
||||
title: Welcome %{username}!
|
||||
manage_contests: Manage my contests
|
||||
new_contest: Create a new contest
|
||||
new:
|
||||
title: "New jigsaw puzzle contest"
|
||||
notice: Contest added
|
||||
title: New jigsaw puzzle contest
|
||||
scoreboard:
|
||||
refresh: Activate auto-refresh (every 5s)
|
||||
title: "%{name}"
|
||||
show:
|
||||
title: "%{name}"
|
||||
add_participant: "Add contestant"
|
||||
add_puzzle: "Add puzzle"
|
||||
public_scoreboard: "Public scoreboard: "
|
||||
add_participant: Add participant
|
||||
add_puzzle: Add puzzle
|
||||
copy_extension_url: Copy the URL for connecting from the browser extension
|
||||
open_public_scoreboard: Open public scoreboard
|
||||
public_scoreboard_disabled: The public scoreboard is disabled
|
||||
url_copied: URL copied to the clipboard
|
||||
contestants:
|
||||
convert_csv:
|
||||
title: Import participants
|
||||
destroy:
|
||||
notice: Participant deleted
|
||||
edit:
|
||||
title: "Participant"
|
||||
team_title: "Teams"
|
||||
notice: Participant updated
|
||||
title: Participant
|
||||
team_title: Teams
|
||||
finalize_import:
|
||||
title: Import participants
|
||||
import:
|
||||
email_column: Participant email
|
||||
import_column: Import?
|
||||
name_column: Participant name
|
||||
notice: Participants imported
|
||||
title: Import participants
|
||||
new:
|
||||
title: "New participant"
|
||||
team_title: "New team"
|
||||
singular: "participant"
|
||||
plural: "participants"
|
||||
notice: Participant added
|
||||
title: New participant
|
||||
team_title: New team
|
||||
singular: participant
|
||||
plural: participants
|
||||
teams:
|
||||
singular: "team"
|
||||
plural: "teams"
|
||||
singular: team
|
||||
plural: teams
|
||||
upload_csv:
|
||||
title: Import participants
|
||||
helpers:
|
||||
badges:
|
||||
registration: "registration"
|
||||
team: "team"
|
||||
buttons:
|
||||
add: "Add"
|
||||
add_completion: "Add completion"
|
||||
back: "⬅ Back to the contest"
|
||||
back_to_contestant: "⬅ Back to the participant"
|
||||
confirm: "Confirm"
|
||||
create: "Create"
|
||||
save: "Save"
|
||||
delete: "Delete"
|
||||
edit: "Edit"
|
||||
export: Export
|
||||
import: CSV Import
|
||||
open: Open
|
||||
refresh: Refresh
|
||||
sign_in: Sign in
|
||||
save: Save
|
||||
field: Field
|
||||
none: No field selected
|
||||
rank: Rank
|
||||
messages:
|
||||
convert:
|
||||
title: New completion
|
||||
destroy:
|
||||
notice: Message deleted
|
||||
plural: "messages"
|
||||
singular: "message"
|
||||
warning: "You first need to add a puzzle before converting messages to completions."
|
||||
nav:
|
||||
users: "Users"
|
||||
home: "Home"
|
||||
settings: "Settings"
|
||||
log_out: "Log out"
|
||||
users: Users
|
||||
home: My contests
|
||||
settings: Settings
|
||||
log_out: Log out
|
||||
puzzles:
|
||||
destroy:
|
||||
notice: Puzzle deleted
|
||||
edit:
|
||||
title: "Edit contest puzzle"
|
||||
notice: Puzzle updated
|
||||
title: Edit contest puzzle
|
||||
form:
|
||||
fake_data_recommendation: It is recommended to first enter a fake name and image, and to use the real ones only once the contest starts.
|
||||
file_too_big: File too big! Maximum allowed size is 2M
|
||||
image_select: Select an image
|
||||
new:
|
||||
title: "New contest puzzle"
|
||||
singular: "puzzle"
|
||||
plural: "puzzles"
|
||||
notice: Puzzle added
|
||||
title: New contest puzzle
|
||||
singular: puzzle
|
||||
plural: puzzles
|
||||
sessions:
|
||||
new:
|
||||
notice: Login successful
|
||||
title: "Login to the Public Scoreboard app"
|
||||
users:
|
||||
edit:
|
||||
notice: Settings updated
|
||||
title: "My settings"
|
||||
general_section: "General settings"
|
||||
password_section: "Change password"
|
||||
index:
|
||||
title: "All users"
|
||||
new:
|
||||
notice: User created
|
||||
title: "New user"
|
||||
|
||||
@@ -1,75 +1,231 @@
|
||||
fr:
|
||||
activemodel:
|
||||
errors:
|
||||
models:
|
||||
forms/csv_conversion_form:
|
||||
attributes:
|
||||
name_column:
|
||||
blank: "Choisir une colonne pour les noms des participant.e.s est nécessaire"
|
||||
greater_than: "Choisir une colonne pour les noms des participant.e.s est nécessaire"
|
||||
activerecord:
|
||||
attributes:
|
||||
category:
|
||||
contestant_count: Nombre de participant.e.s
|
||||
new: Nouvelle catégorie
|
||||
name: Catégorie
|
||||
completion:
|
||||
contestant_id: Participant.e
|
||||
display_time: Temps
|
||||
display_time_from_start: Temps depuis le début
|
||||
display_relative_time: Temps pour ce puzzle
|
||||
puzzle: Puzzle
|
||||
remaining_pieces: Nombre de pièces restantes
|
||||
remaining_pieces_description: Si ce champ est rempli, le temps ci-dessus ne sera pas pris en compte
|
||||
contest:
|
||||
name: "Nom"
|
||||
team: "Concours par équipes"
|
||||
team_description: "Principalement pour des raisons d'affichage"
|
||||
allow_registration: "Autoriser l'inscription via l'interface"
|
||||
allow_registration_description: "Génère un formulaire d'inscription pour ce concours"
|
||||
lang: Langue pour le classement public
|
||||
name: Nom
|
||||
public: Activer le classement public
|
||||
team: Concours par équipes
|
||||
team_description: Principalement pour des raisons d'affichage
|
||||
allow_registration: Autoriser l'inscription via l'interface
|
||||
allow_registration_description: Génère un formulaire d'inscription pour ce concours
|
||||
contestant:
|
||||
completions: Complétions
|
||||
display_time: Temps
|
||||
email: Email
|
||||
name: Nom
|
||||
email_description: Optionnel. Utile pour envoyer des emails aux participant.e.s depuis cette app, ou pour reconnaître les pseudos gmeet quand ils ne correspondent pas au nom préalablement entré.
|
||||
csv_import:
|
||||
file: Fichier
|
||||
separator: Délimiteur
|
||||
message:
|
||||
author: Auteur.ice
|
||||
processed: Traité ?
|
||||
text: Contenu
|
||||
time: Temps
|
||||
puzzle:
|
||||
brand: Marque
|
||||
image: Image
|
||||
name: Nom
|
||||
pieces: Nombre de pièces
|
||||
session:
|
||||
email_address: Adresse email
|
||||
password: Mot de passe
|
||||
user:
|
||||
username: "Nom d'utilisateur.ice"
|
||||
email_address: "Adresse email"
|
||||
lang: "Langue de l'interface"
|
||||
password: "Nouveau mot de passe"
|
||||
username: Nom d'utilisateur.ice
|
||||
email_address: Adresse email
|
||||
lang: Langue de l'interface
|
||||
password: Nouveau mot de passe
|
||||
errors:
|
||||
models:
|
||||
completion:
|
||||
attributes:
|
||||
contestant_id:
|
||||
taken: "Ce.tte participant.e a déjà complété le puzzle"
|
||||
display_time_from_start:
|
||||
blank: Obligatoire
|
||||
invalid: "Formats autorisés: xx:xx:xx, x:xx:xx, xx:xx, x:xx, xx"
|
||||
puzzle_id:
|
||||
taken: "Ce.tte participant.e a déjà complété ce puzzle"
|
||||
contest:
|
||||
attributes:
|
||||
name:
|
||||
blank: Le nom du concours ne peut pas être vide
|
||||
contestant:
|
||||
attributes:
|
||||
name:
|
||||
blank: Le nom du ou de la participant.e ne peut pas être vide
|
||||
csv_import:
|
||||
attributes:
|
||||
file:
|
||||
blank: "Aucun fichier sélectionné"
|
||||
empty: "Ce fichier est vide"
|
||||
not_a_csv_file: "Le fichier doit être au format CSV"
|
||||
puzzle:
|
||||
attributes:
|
||||
name:
|
||||
blank: Le nom du puzzle est obligatoire
|
||||
pieces:
|
||||
blank: Il est obligatoire d'indiquer le nombre de pièces
|
||||
user:
|
||||
attributes:
|
||||
email_address:
|
||||
blank: L'email est obligatoire
|
||||
username:
|
||||
blank: Le nom d'utilisateur.ice est obligatoire
|
||||
categories:
|
||||
destroy:
|
||||
notice: Catégorie supprimée
|
||||
new:
|
||||
error: Le nom de la catégorie ne peut pas être vide
|
||||
notice: Catégorie ajoutée
|
||||
completions:
|
||||
destroy:
|
||||
notice: Complétion supprimée
|
||||
edit:
|
||||
title: "Modifier la complétion"
|
||||
notice: Complétion modifiée
|
||||
title: Modifier la complétion
|
||||
new:
|
||||
title: "Nouvelle complétion"
|
||||
notice: Complétion ajoutée
|
||||
title: Ajout d'une complétion
|
||||
singular: complétion
|
||||
contests:
|
||||
destroy:
|
||||
notice: Concours supprimé
|
||||
edit:
|
||||
title: "Paramètres du concours"
|
||||
notice: Concours modifié
|
||||
title: Paramètres du concours
|
||||
form:
|
||||
categories: Catégories de participant.e.s
|
||||
general: Paramètres généraux
|
||||
index:
|
||||
title: "Bienvenue %{username} !"
|
||||
manage_contests: "Mes concours de puzzle"
|
||||
new_contest: "Créer un nouveau concours"
|
||||
title: Bienvenue %{username} !
|
||||
manage_contests: Mes concours de puzzle
|
||||
new_contest: Créer un nouveau concours
|
||||
new:
|
||||
title: "Nouveau concours"
|
||||
notice: Concours ajouté
|
||||
title: Nouveau concours
|
||||
scoreboard:
|
||||
refresh: Activer le rafraichissement automatique de la page (toutes les 5s)
|
||||
title: "%{name}"
|
||||
show:
|
||||
title: "%{name}"
|
||||
add_participant: "Ajouter un.e participant.e"
|
||||
add_puzzle: "Ajouter un puzzle"
|
||||
public_scoreboard: "Classement public : "
|
||||
add_participant: Ajouter un.e participant.e
|
||||
add_puzzle: Ajouter un puzzle
|
||||
copy_extension_url: Copier l'URL pour la connexion depuis l'extension web
|
||||
open_public_scoreboard: Ouvrir le classement public
|
||||
public_scoreboard_disabled: Le classement public n'est pas activé
|
||||
url_copied: L’URL a été copiée dans le presse-papier
|
||||
contestants:
|
||||
convert_csv:
|
||||
title: Importer des participant.e.s
|
||||
destroy:
|
||||
notice: Participant.e supprimé.e
|
||||
edit:
|
||||
title: "Participant.e"
|
||||
team_title: "Équipe"
|
||||
notice: Participant.e modifié.e
|
||||
title: Participant.e
|
||||
team_title: Équipe
|
||||
finalize_import:
|
||||
title: Importer des participant.e.s
|
||||
import:
|
||||
email_column: Email des participant.e.s
|
||||
import_column: Importer ?
|
||||
name_column: Noms des participant.e.s
|
||||
notice: Participant.e.s importé.e.s
|
||||
title: Importer des participant.e.s
|
||||
new:
|
||||
title: "Nouveau.elle participant.e"
|
||||
team_title: "Nouvelle équipe"
|
||||
singular: "participant.e"
|
||||
plural: "participant.e.s"
|
||||
notice: Participant.e ajouté.e
|
||||
title: Nouveau.elle participant.e
|
||||
team_title: Nouvelle équipe
|
||||
singular: participant.e
|
||||
plural: participant.e.s
|
||||
teams:
|
||||
singular: "équipe"
|
||||
plural: "équipes"
|
||||
singular: équipe
|
||||
plural: équipes
|
||||
upload_csv:
|
||||
title: Importer des participant.e.s
|
||||
helpers:
|
||||
badges:
|
||||
registration: "auto-inscription"
|
||||
team: "équipes"
|
||||
buttons:
|
||||
add: "Ajouter"
|
||||
add_completion: "Convertir"
|
||||
back: "⬅ Revenir au concours"
|
||||
back_to_contestant: "⬅ Revenir au/à la participant.e"
|
||||
confirm: "Confirmer"
|
||||
create: "Créer"
|
||||
save: "Modifier"
|
||||
delete: "Supprimer"
|
||||
edit: "Modifier"
|
||||
export: Exporter
|
||||
import: Importer un CSV
|
||||
open: Détails
|
||||
refresh: Rafraîchir
|
||||
sign_in: Se connecter
|
||||
save: Modifier
|
||||
field: Champ
|
||||
none: Aucun champ sélectionné
|
||||
rank: Rang
|
||||
messages:
|
||||
convert:
|
||||
title: Ajout d'une complétion
|
||||
destroy:
|
||||
notice: Message supprimé
|
||||
plural: "messages"
|
||||
singular: "message"
|
||||
warning: "Au moins un puzzle doit être ajouté avant de pouvoir convertir des messages en complétions."
|
||||
nav:
|
||||
users: "Utilisateur.ices"
|
||||
home: "Accueil"
|
||||
settings: "Paramètres"
|
||||
log_out: "Déconnexion"
|
||||
users: Utilisateur.ices
|
||||
home: Mes concours
|
||||
settings: Paramètres
|
||||
log_out: Déconnexion
|
||||
puzzles:
|
||||
destroy:
|
||||
notice: Puzzle supprimé
|
||||
edit:
|
||||
title: "Modifier le puzzle"
|
||||
notice: Puzzle modifié
|
||||
title: Modifier le puzzle
|
||||
form:
|
||||
fake_data_recommendation: Il est recommendé d'entrer de faux noms et images, et de mettre les vrais uniquement quand le concours démarre.
|
||||
file_too_big: La taille de l'image dépasse la taille maximum autorisée de 2M
|
||||
image_select: Choisis une image
|
||||
new:
|
||||
title: "Nouveau puzzle"
|
||||
singular: "puzzle"
|
||||
plural: "puzzles"
|
||||
notice: Puzzle ajouté
|
||||
title: Nouveau puzzle
|
||||
singular: puzzle
|
||||
plural: puzzles
|
||||
sessions:
|
||||
new:
|
||||
notice: Connection réussie
|
||||
title: "Se connecter à l'app Public Scoreboard"
|
||||
users:
|
||||
edit:
|
||||
notice: Paramètres modifiés
|
||||
title: "Mes paramètres"
|
||||
general_section: "Paramètres globaux"
|
||||
password_section: "Modifier mon mot de passe"
|
||||
index:
|
||||
title: "Tous.tes les utilisateur.ices"
|
||||
new:
|
||||
notice: Utilisateur.ice ajouté.e
|
||||
title: "Nouveau.elle utilisateur.ice"
|
||||
|
||||
@@ -9,14 +9,26 @@ Rails.application.routes.draw do
|
||||
root "contests#index"
|
||||
|
||||
resources :contests do
|
||||
resources :categories, only: [ :create, :destroy ]
|
||||
resources :completions
|
||||
resources :contestants
|
||||
resources :puzzles
|
||||
resources :messages, only: :destroy do
|
||||
get "convert", to: "messages#convert"
|
||||
end
|
||||
get "import", to: "contestants#import"
|
||||
post "import", to: "contestants#upload_csv"
|
||||
get "import/:id", to: "contestants#convert_csv"
|
||||
post "import/:id", to: "contestants#finalize_import"
|
||||
get "export", to: "contestants#export"
|
||||
end
|
||||
resources :passwords, param: :token
|
||||
resource :session
|
||||
resources :users
|
||||
|
||||
options "connect", to: "messages#cors_preflight_check"
|
||||
options "message", to: "messages#cors_preflight_check"
|
||||
post "connect", to: "messages#connect"
|
||||
post "message", to: "messages#create"
|
||||
|
||||
get "public/:id", to: "contests#scoreboard"
|
||||
|
||||
12
db/migrate/20250515061619_add_author_to_message.rb
Normal file
12
db/migrate/20250515061619_add_author_to_message.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
class AddAuthorToMessage < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :messages, :author, :string
|
||||
|
||||
Message.find_each do |message|
|
||||
message.author = "Unknown"
|
||||
message.save
|
||||
end
|
||||
|
||||
change_column_null :messages, :author, true
|
||||
end
|
||||
end
|
||||
12
db/migrate/20250515062154_add_display_time_to_message.rb
Normal file
12
db/migrate/20250515062154_add_display_time_to_message.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
class AddDisplayTimeToMessage < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :messages, :display_time, :string
|
||||
|
||||
Message.find_each do |message|
|
||||
message.display_time = "12:30"
|
||||
message.save
|
||||
end
|
||||
|
||||
change_column_null :messages, :display_time, true
|
||||
end
|
||||
end
|
||||
9
db/migrate/20250517083830_create_csv_imports.rb
Normal file
9
db/migrate/20250517083830_create_csv_imports.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
class CreateCsvImports < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :csv_imports do |t|
|
||||
t.string :separator, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
5
db/migrate/20250517131707_add_content_to_csv_import.rb
Normal file
5
db/migrate/20250517131707_add_content_to_csv_import.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class AddContentToCsvImport < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :csv_imports, :content, :string, null: false
|
||||
end
|
||||
end
|
||||
15
db/migrate/20250618122655_add_time_seconds_to_contestant.rb
Normal file
15
db/migrate/20250618122655_add_time_seconds_to_contestant.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
class AddTimeSecondsToContestant < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :contestants, :time_seconds, :integer
|
||||
|
||||
Contestant.find_each do |contestant|
|
||||
contestant.time_seconds = 0
|
||||
contestant.completions.each do |completion|
|
||||
contestant.time_seconds += completion.time_seconds
|
||||
end
|
||||
contestant.save
|
||||
end
|
||||
|
||||
change_column_null :contestants, :time_seconds, true
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddMessageRefToCompletion < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_reference :completions, :message, foreign_key: true
|
||||
end
|
||||
end
|
||||
5
db/migrate/20250620051905_add_lang_to_contest.rb
Normal file
5
db/migrate/20250620051905_add_lang_to_contest.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class AddLangToContest < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :contests, :lang, :string, default: 'en'
|
||||
end
|
||||
end
|
||||
5
db/migrate/20250625075513_add_public_to_contest.rb
Normal file
5
db/migrate/20250625075513_add_public_to_contest.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class AddPublicToContest < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :contests, :public, :boolean, default: false
|
||||
end
|
||||
end
|
||||
12
db/migrate/20250627070407_add_pieces_to_puzzle.rb
Normal file
12
db/migrate/20250627070407_add_pieces_to_puzzle.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
class AddPiecesToPuzzle < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :puzzles, :pieces, :integer
|
||||
|
||||
Puzzle.find_each do |puzzle|
|
||||
puzzle.pieces = 500
|
||||
puzzle.save
|
||||
end
|
||||
|
||||
change_column_null :puzzles, :pieces, false
|
||||
end
|
||||
end
|
||||
15
db/migrate/20250714115208_create_categories.rb
Normal file
15
db/migrate/20250714115208_create_categories.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
class CreateCategories < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :categories do |t|
|
||||
t.string :name
|
||||
t.belongs_to :contest, null: false, foreign_key: true
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
create_join_table :categories, :contestants do |t|
|
||||
t.index :category_id
|
||||
t.index :contestant_id
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddRemainingPiecesToCompletion < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :completions, :remaining_pieces, :integer
|
||||
end
|
||||
end
|
||||
35
db/schema.rb
generated
35
db/schema.rb
generated
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_05_11_173749) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_10_28_131431) do
|
||||
create_table "active_storage_attachments", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "record_type", null: false
|
||||
@@ -39,6 +39,21 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_11_173749) do
|
||||
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
|
||||
end
|
||||
|
||||
create_table "categories", force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.integer "contest_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["contest_id"], name: "index_categories_on_contest_id"
|
||||
end
|
||||
|
||||
create_table "categories_contestants", id: false, force: :cascade do |t|
|
||||
t.integer "category_id", null: false
|
||||
t.integer "contestant_id", null: false
|
||||
t.index ["category_id"], name: "index_categories_contestants_on_category_id"
|
||||
t.index ["contestant_id"], name: "index_categories_contestants_on_contestant_id"
|
||||
end
|
||||
|
||||
create_table "completions", force: :cascade do |t|
|
||||
t.integer "time_seconds"
|
||||
t.integer "contestant_id", null: false
|
||||
@@ -48,8 +63,11 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_11_173749) do
|
||||
t.integer "contest_id", null: false
|
||||
t.string "display_time_from_start"
|
||||
t.string "display_relative_time"
|
||||
t.integer "message_id"
|
||||
t.integer "remaining_pieces"
|
||||
t.index ["contest_id"], name: "index_completions_on_contest_id"
|
||||
t.index ["contestant_id"], name: "index_completions_on_contestant_id"
|
||||
t.index ["message_id"], name: "index_completions_on_message_id"
|
||||
t.index ["puzzle_id"], name: "index_completions_on_puzzle_id"
|
||||
end
|
||||
|
||||
@@ -60,6 +78,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_11_173749) do
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "display_time"
|
||||
t.integer "time_seconds"
|
||||
t.index ["contest_id"], name: "index_contestants_on_contest_id"
|
||||
end
|
||||
|
||||
@@ -71,10 +90,19 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_11_173749) do
|
||||
t.boolean "team", default: false
|
||||
t.boolean "allow_registration", default: false
|
||||
t.string "slug"
|
||||
t.string "lang", default: "en"
|
||||
t.boolean "public", default: false
|
||||
t.index ["slug"], name: "index_contests_on_slug", unique: true
|
||||
t.index ["user_id"], name: "index_contests_on_user_id"
|
||||
end
|
||||
|
||||
create_table "csv_imports", force: :cascade do |t|
|
||||
t.string "separator", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "content", null: false
|
||||
end
|
||||
|
||||
create_table "friendly_id_slugs", force: :cascade do |t|
|
||||
t.string "slug", null: false
|
||||
t.integer "sluggable_id", null: false
|
||||
@@ -92,6 +120,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_11_173749) do
|
||||
t.string "text", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "author"
|
||||
t.string "display_time"
|
||||
t.index ["contest_id"], name: "index_messages_on_contest_id"
|
||||
end
|
||||
|
||||
@@ -101,6 +131,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_11_173749) do
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "contest_id", null: false
|
||||
t.string "brand"
|
||||
t.integer "pieces", null: false
|
||||
t.index ["contest_id"], name: "index_puzzles_on_contest_id"
|
||||
end
|
||||
|
||||
@@ -126,8 +157,10 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_11_173749) do
|
||||
|
||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "categories", "contests"
|
||||
add_foreign_key "completions", "contestants"
|
||||
add_foreign_key "completions", "contests"
|
||||
add_foreign_key "completions", "messages"
|
||||
add_foreign_key "completions", "puzzles"
|
||||
add_foreign_key "contestants", "contests"
|
||||
add_foreign_key "contests", "users"
|
||||
|
||||
23
spec/factories/categories.rb
Normal file
23
spec/factories/categories.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: categories
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# name :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# contest_id :integer not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_categories_on_contest_id (contest_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# contest_id (contest_id => contests.id)
|
||||
#
|
||||
FactoryBot.define do
|
||||
factory :category do
|
||||
name { "MyString" }
|
||||
end
|
||||
end
|
||||
@@ -4,7 +4,9 @@
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# allow_registration :boolean default(FALSE)
|
||||
# lang :string default("en")
|
||||
# name :string
|
||||
# public :boolean default(FALSE)
|
||||
# slug :string
|
||||
# team :boolean default(FALSE)
|
||||
# created_at :datetime not null
|
||||
|
||||
15
spec/factories/csv_imports.rb
Normal file
15
spec/factories/csv_imports.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: csv_imports
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# content :string not null
|
||||
# separator :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
FactoryBot.define do
|
||||
factory :csv_import do
|
||||
separator { 1 }
|
||||
end
|
||||
end
|
||||
@@ -3,6 +3,8 @@
|
||||
# Table name: messages
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# author :string
|
||||
# display_time :string
|
||||
# text :string not null
|
||||
# time_seconds :integer not null
|
||||
# created_at :datetime not null
|
||||
|
||||
@@ -1,17 +1,78 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.feature "Contests", type: :feature do
|
||||
context "visiting the home page" do
|
||||
let!(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
login(user)
|
||||
end
|
||||
|
||||
it "should display the username" do
|
||||
visit root_path
|
||||
context "index" do
|
||||
let!(:first_contest) { create(:contest, user: user) }
|
||||
let!(:second_contest) { create(:contest, user: user) }
|
||||
|
||||
expect(page).to have_content(user.username)
|
||||
it "should list existing contests and offer to open them" do
|
||||
visit contests_path
|
||||
|
||||
expect(page).to have_content(I18n.t("contests.index.title", username: user.username))
|
||||
expect(page).to have_content(first_contest.name)
|
||||
expect(page).to have_content(second_contest.name)
|
||||
|
||||
find(".stretched-link[href=\"/contests/#{first_contest.friendly_id}\"]").click
|
||||
|
||||
expect(page).to have_current_path("/contests/#{first_contest.friendly_id}")
|
||||
end
|
||||
|
||||
it "should offer to create a new contest" do
|
||||
visit contests_path
|
||||
|
||||
click_link I18n.t("contests.index.new_contest")
|
||||
|
||||
expect(page).to have_current_path("/contests/new")
|
||||
end
|
||||
end
|
||||
|
||||
context "new" do
|
||||
it "should prevent creating contests without a name" do
|
||||
visit new_contest_path
|
||||
|
||||
click_button I18n.t("helpers.buttons.create")
|
||||
|
||||
expect(page).to have_content(I18n.t("activerecord.errors.models.contest.attributes.name.blank"))
|
||||
end
|
||||
|
||||
it "should allow creating new contests with valid parameters" do
|
||||
visit new_contest_path
|
||||
|
||||
fill_in I18n.t("activerecord.attributes.contest.name"), with: "Contest name"
|
||||
|
||||
click_button I18n.t("helpers.buttons.create")
|
||||
|
||||
expect(page).to have_current_path(contest_path(Contest.find_by(name: "Contest name")))
|
||||
end
|
||||
end
|
||||
|
||||
context "edit" do
|
||||
let!(:contest) { create(:contest, user: user) }
|
||||
|
||||
it "should prevent editing contests without a name" do
|
||||
visit edit_contest_path(contest)
|
||||
|
||||
fill_in I18n.t("activerecord.attributes.contest.name"), with: ""
|
||||
|
||||
click_button I18n.t("helpers.buttons.save")
|
||||
|
||||
expect(page).to have_content(I18n.t("activerecord.errors.models.contest.attributes.name.blank"))
|
||||
end
|
||||
|
||||
it "should allow editing contests with valid parameters" do
|
||||
visit edit_contest_path(contest)
|
||||
|
||||
fill_in I18n.t("activerecord.attributes.contest.name"), with: "Contest name"
|
||||
|
||||
click_button I18n.t("helpers.buttons.save")
|
||||
|
||||
expect(page).to have_current_path(contest_path(contest))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
23
spec/models/category_spec.rb
Normal file
23
spec/models/category_spec.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: categories
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# name :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# contest_id :integer not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_categories_on_contest_id (contest_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# contest_id (contest_id => contests.id)
|
||||
#
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Category, type: :model do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
||||
15
spec/models/csv_import_spec.rb
Normal file
15
spec/models/csv_import_spec.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: csv_imports
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# content :string not null
|
||||
# separator :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe CsvImport, type: :model do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
||||
@@ -3,6 +3,8 @@
|
||||
# Table name: messages
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# author :string
|
||||
# display_time :string
|
||||
# text :string not null
|
||||
# time_seconds :integer not null
|
||||
# created_at :datetime not null
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
|
||||
driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ]
|
||||
end
|
||||
@@ -1,7 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class CompletionsControllerTest < ActionDispatch::IntegrationTest
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
@@ -1,7 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class ContestantsControllerTest < ActionDispatch::IntegrationTest
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
@@ -1,4 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class ContestsControllerTest < ActionDispatch::IntegrationTest
|
||||
end
|
||||
@@ -1,7 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class PuzzlesControllerTest < ActionDispatch::IntegrationTest
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
@@ -1,4 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class UsersControllerTest < ActionDispatch::IntegrationTest
|
||||
end
|
||||
39
test/fixtures/completions.yml
vendored
39
test/fixtures/completions.yml
vendored
@@ -1,39 +0,0 @@
|
||||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: completions
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# display_relative_time :string
|
||||
# display_time_from_start :string
|
||||
# time_seconds :integer
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# contest_id :integer not null
|
||||
# contestant_id :integer not null
|
||||
# puzzle_id :integer not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_completions_on_contest_id (contest_id)
|
||||
# index_completions_on_contestant_id (contestant_id)
|
||||
# index_completions_on_puzzle_id (puzzle_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# contest_id (contest_id => contests.id)
|
||||
# contestant_id (contestant_id => contestants.id)
|
||||
# puzzle_id (puzzle_id => puzzles.id)
|
||||
#
|
||||
completion_one:
|
||||
time_seconds: 1
|
||||
contestant: team_one
|
||||
puzzle: puzzle_one
|
||||
contest: team_contest
|
||||
|
||||
completion_two:
|
||||
time_seconds: 2
|
||||
contestant: solo_one
|
||||
puzzle: puzzle_two
|
||||
contest: solo_contest
|
||||
33
test/fixtures/contestants.yml
vendored
33
test/fixtures/contestants.yml
vendored
@@ -1,33 +0,0 @@
|
||||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: contestants
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# display_time :string
|
||||
# email :string
|
||||
# name :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# contest_id :integer not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_contestants_on_contest_id (contest_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# contest_id (contest_id => contests.id)
|
||||
#
|
||||
team_one:
|
||||
name: Team one
|
||||
contest: team_contest
|
||||
|
||||
team_two:
|
||||
name: Team two
|
||||
contest: team_contest
|
||||
|
||||
solo_one:
|
||||
name: Solo one
|
||||
contest: solo_contest
|
||||
36
test/fixtures/contests.yml
vendored
36
test/fixtures/contests.yml
vendored
@@ -1,36 +0,0 @@
|
||||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: contests
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# allow_registration :boolean default(FALSE)
|
||||
# name :string
|
||||
# slug :string
|
||||
# team :boolean default(FALSE)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# user_id :integer not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_contests_on_slug (slug) UNIQUE
|
||||
# index_contests_on_user_id (user_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# user_id (user_id => users.id)
|
||||
#
|
||||
|
||||
team_contest:
|
||||
name: Team contest
|
||||
user: user
|
||||
slug: team-contest
|
||||
team: true
|
||||
|
||||
solo_contest:
|
||||
name: Solo contest
|
||||
user: user
|
||||
slug: solo-contest
|
||||
team: false
|
||||
0
test/fixtures/files/.keep
vendored
0
test/fixtures/files/.keep
vendored
30
test/fixtures/puzzles.yml
vendored
30
test/fixtures/puzzles.yml
vendored
@@ -1,30 +0,0 @@
|
||||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: puzzles
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# brand :string
|
||||
# name :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# contest_id :integer not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_puzzles_on_contest_id (contest_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# contest_id (contest_id => contests.id)
|
||||
#
|
||||
puzzle_one:
|
||||
name: Puzzle one
|
||||
brand: Brand one
|
||||
contest: team_contest
|
||||
|
||||
puzzle_two:
|
||||
name: Puzzle two
|
||||
brand: Brand two
|
||||
contest: solo_contest
|
||||
31
test/fixtures/users.yml
vendored
31
test/fixtures/users.yml
vendored
@@ -1,31 +0,0 @@
|
||||
<% admin_password_digest = BCrypt::Password.create("admin") %>
|
||||
<% user_password_digest = BCrypt::Password.create("user") %>
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: users
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# admin :boolean default(FALSE), not null
|
||||
# email_address :string not null
|
||||
# lang :string default("en")
|
||||
# password_digest :string not null
|
||||
# username :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_users_on_email_address (email_address) UNIQUE
|
||||
#
|
||||
admin_user:
|
||||
email_address: admin@admin.org
|
||||
password_digest: <%= admin_password_digest %>
|
||||
username: admin
|
||||
admin: true
|
||||
|
||||
user:
|
||||
email_address: user@user.org
|
||||
password_digest: <%= user_password_digest %>
|
||||
username: user
|
||||
admin: false
|
||||
@@ -1,7 +0,0 @@
|
||||
# Preview all emails at http://localhost:3000/rails/mailers/passwords_mailer
|
||||
class PasswordsMailerPreview < ActionMailer::Preview
|
||||
# Preview this email at http://localhost:3000/rails/mailers/passwords_mailer/reset
|
||||
def reset
|
||||
PasswordsMailer.reset(User.take)
|
||||
end
|
||||
end
|
||||
@@ -1,33 +0,0 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: completions
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# display_relative_time :string
|
||||
# display_time_from_start :string
|
||||
# time_seconds :integer
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# contest_id :integer not null
|
||||
# contestant_id :integer not null
|
||||
# puzzle_id :integer not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_completions_on_contest_id (contest_id)
|
||||
# index_completions_on_contestant_id (contestant_id)
|
||||
# index_completions_on_puzzle_id (puzzle_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# contest_id (contest_id => contests.id)
|
||||
# contestant_id (contestant_id => contestants.id)
|
||||
# puzzle_id (puzzle_id => puzzles.id)
|
||||
#
|
||||
require "test_helper"
|
||||
|
||||
class CompletionTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
@@ -1,29 +0,0 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: contests
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# allow_registration :boolean default(FALSE)
|
||||
# name :string
|
||||
# slug :string
|
||||
# team :boolean default(FALSE)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# user_id :integer not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_contests_on_slug (slug) UNIQUE
|
||||
# index_contests_on_user_id (user_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# user_id (user_id => users.id)
|
||||
#
|
||||
require "test_helper"
|
||||
|
||||
class ContestTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
@@ -1,27 +0,0 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: contestants
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# display_time :string
|
||||
# email :string
|
||||
# name :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# contest_id :integer not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_contestants_on_contest_id (contest_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# contest_id (contest_id => contests.id)
|
||||
#
|
||||
require "test_helper"
|
||||
|
||||
class ContestantTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
@@ -1,24 +0,0 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: users
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# admin :boolean default(FALSE), not null
|
||||
# email_address :string not null
|
||||
# lang :string default("en")
|
||||
# password_digest :string not null
|
||||
# username :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_users_on_email_address (email_address) UNIQUE
|
||||
#
|
||||
require "test_helper"
|
||||
|
||||
class UserTest < ActiveSupport::TestCase
|
||||
test "the truth" do
|
||||
assert true
|
||||
end
|
||||
end
|
||||
@@ -1,15 +0,0 @@
|
||||
ENV["RAILS_ENV"] ||= "test"
|
||||
require_relative "../config/environment"
|
||||
require "rails/test_help"
|
||||
|
||||
module ActiveSupport
|
||||
class TestCase
|
||||
# Run tests in parallel with specified workers
|
||||
parallelize(workers: :number_of_processors)
|
||||
|
||||
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
|
||||
fixtures :all
|
||||
|
||||
# Add more helper methods to be used by all tests here...
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user