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 "dartsass-rails"
|
||||||
gem "bootstrap", "~> 5.3.3"
|
gem "bootstrap", "~> 5.3.3"
|
||||||
gem "friendly_id", "~> 5.5.0"
|
gem "friendly_id", "~> 5.5.0"
|
||||||
|
gem "csv"
|
||||||
|
gem "damerau-levenshtein"
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
|
# 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)
|
uri (>= 0.13.1)
|
||||||
addressable (2.8.7)
|
addressable (2.8.7)
|
||||||
public_suffix (>= 2.0.2, < 7.0)
|
public_suffix (>= 2.0.2, < 7.0)
|
||||||
annotaterb (4.14.0)
|
annotaterb (4.16.0)
|
||||||
ast (2.4.2)
|
activerecord (>= 6.0.0)
|
||||||
autoprefixer-rails (10.4.19.0)
|
activesupport (>= 6.0.0)
|
||||||
execjs (~> 2)
|
ast (2.4.3)
|
||||||
base64 (0.2.0)
|
base64 (0.3.0)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.20)
|
||||||
bcrypt_pbkdf (1.1.1)
|
bcrypt_pbkdf (1.1.1)
|
||||||
benchmark (0.4.0)
|
benchmark (0.4.1)
|
||||||
bigdecimal (3.1.9)
|
bigdecimal (3.2.2)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.18.4)
|
bootsnap (1.18.6)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
bootstrap (5.3.3)
|
bootstrap (5.3.5)
|
||||||
autoprefixer-rails (>= 9.1.0)
|
|
||||||
popper_js (>= 2.11.8, < 3)
|
popper_js (>= 2.11.8, < 3)
|
||||||
brakeman (7.0.0)
|
brakeman (7.0.2)
|
||||||
racc
|
racc
|
||||||
builder (3.3.0)
|
builder (3.3.0)
|
||||||
capybara (3.40.0)
|
capybara (3.40.0)
|
||||||
@@ -102,28 +101,30 @@ GEM
|
|||||||
regexp_parser (>= 1.5, < 3.0)
|
regexp_parser (>= 1.5, < 3.0)
|
||||||
xpath (~> 3.2)
|
xpath (~> 3.2)
|
||||||
concurrent-ruby (1.3.5)
|
concurrent-ruby (1.3.5)
|
||||||
connection_pool (2.5.0)
|
connection_pool (2.5.3)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
|
csv (3.3.5)
|
||||||
|
damerau-levenshtein (1.3.3)
|
||||||
dartsass-rails (0.5.1)
|
dartsass-rails (0.5.1)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
sass-embedded (~> 1.63)
|
sass-embedded (~> 1.63)
|
||||||
date (3.4.1)
|
date (3.4.1)
|
||||||
debug (1.10.0)
|
debug (1.11.0)
|
||||||
irb (~> 1.10)
|
irb (~> 1.10)
|
||||||
reline (>= 0.3.8)
|
reline (>= 0.3.8)
|
||||||
diff-lcs (1.6.1)
|
diff-lcs (1.6.2)
|
||||||
dotenv (3.1.7)
|
dotenv (3.1.8)
|
||||||
drb (2.2.1)
|
drb (2.2.3)
|
||||||
ed25519 (1.3.0)
|
ed25519 (1.4.0)
|
||||||
|
erb (5.0.1)
|
||||||
erubi (1.13.1)
|
erubi (1.13.1)
|
||||||
et-orbi (1.2.11)
|
et-orbi (1.2.11)
|
||||||
tzinfo
|
tzinfo
|
||||||
execjs (2.10.0)
|
factory_bot (6.5.4)
|
||||||
factory_bot (6.5.1)
|
|
||||||
activesupport (>= 6.1.0)
|
activesupport (>= 6.1.0)
|
||||||
factory_bot_rails (6.4.4)
|
factory_bot_rails (6.5.0)
|
||||||
factory_bot (~> 6.5)
|
factory_bot (~> 6.5)
|
||||||
railties (>= 5.0.0)
|
railties (>= 6.1.0)
|
||||||
faker (3.5.1)
|
faker (3.5.1)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
friendly_id (5.5.1)
|
friendly_id (5.5.1)
|
||||||
@@ -133,13 +134,19 @@ GEM
|
|||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
globalid (1.2.1)
|
globalid (1.2.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
google-protobuf (4.30.0)
|
google-protobuf (4.31.1)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
rake (>= 13)
|
rake (>= 13)
|
||||||
google-protobuf (4.30.0-aarch64-linux)
|
google-protobuf (4.31.1-aarch64-linux-gnu)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
rake (>= 13)
|
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
|
bigdecimal
|
||||||
rake (>= 13)
|
rake (>= 13)
|
||||||
i18n (1.14.7)
|
i18n (1.14.7)
|
||||||
@@ -149,29 +156,29 @@ GEM
|
|||||||
activesupport (>= 6.0.0)
|
activesupport (>= 6.0.0)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
io-console (0.8.0)
|
io-console (0.8.0)
|
||||||
irb (1.15.1)
|
irb (1.15.2)
|
||||||
pp (>= 0.6.0)
|
pp (>= 0.6.0)
|
||||||
rdoc (>= 4.0.0)
|
rdoc (>= 4.0.0)
|
||||||
reline (>= 0.4.2)
|
reline (>= 0.4.2)
|
||||||
jbuilder (2.13.0)
|
jbuilder (2.13.0)
|
||||||
actionview (>= 5.0.0)
|
actionview (>= 5.0.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
json (2.10.2)
|
json (2.12.2)
|
||||||
kamal (2.5.3)
|
kamal (2.7.0)
|
||||||
activesupport (>= 7.0)
|
activesupport (>= 7.0)
|
||||||
base64 (~> 0.2)
|
base64 (~> 0.2)
|
||||||
bcrypt_pbkdf (~> 1.0)
|
bcrypt_pbkdf (~> 1.0)
|
||||||
concurrent-ruby (~> 1.2)
|
concurrent-ruby (~> 1.2)
|
||||||
dotenv (~> 3.1)
|
dotenv (~> 3.1)
|
||||||
ed25519 (~> 1.2)
|
ed25519 (~> 1.4)
|
||||||
net-ssh (~> 7.3)
|
net-ssh (~> 7.3)
|
||||||
sshkit (>= 1.23.0, < 2.0)
|
sshkit (>= 1.23.0, < 2.0)
|
||||||
thor (~> 1.3)
|
thor (~> 1.3)
|
||||||
zeitwerk (>= 2.6.18, < 3.0)
|
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)
|
lint_roller (1.1.0)
|
||||||
logger (1.6.6)
|
logger (1.7.0)
|
||||||
loofah (2.24.0)
|
loofah (2.24.1)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
mail (2.8.1)
|
mail (2.8.1)
|
||||||
@@ -180,11 +187,11 @@ GEM
|
|||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
marcel (1.0.4)
|
marcel (1.0.4)
|
||||||
matrix (0.4.2)
|
matrix (0.4.3)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
minitest (5.25.4)
|
minitest (5.25.5)
|
||||||
msgpack (1.8.0)
|
msgpack (1.8.0)
|
||||||
net-imap (0.5.6)
|
net-imap (0.5.9)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
@@ -199,44 +206,45 @@ GEM
|
|||||||
net-protocol
|
net-protocol
|
||||||
net-ssh (7.3.0)
|
net-ssh (7.3.0)
|
||||||
nio4r (2.7.4)
|
nio4r (2.7.4)
|
||||||
nokogiri (1.18.3-aarch64-linux-gnu)
|
nokogiri (1.18.8-aarch64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.3-aarch64-linux-musl)
|
nokogiri (1.18.8-aarch64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.3-arm-linux-gnu)
|
nokogiri (1.18.8-arm-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.3-arm-linux-musl)
|
nokogiri (1.18.8-arm-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.3-x86_64-linux-gnu)
|
nokogiri (1.18.8-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.3-x86_64-linux-musl)
|
nokogiri (1.18.8-x86_64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
ostruct (0.6.1)
|
ostruct (0.6.2)
|
||||||
parallel (1.26.3)
|
parallel (1.27.0)
|
||||||
parser (3.3.7.1)
|
parser (3.3.8.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
popper_js (2.11.8)
|
popper_js (2.11.8)
|
||||||
pp (0.6.2)
|
pp (0.6.2)
|
||||||
prettyprint
|
prettyprint
|
||||||
prettyprint (0.2.0)
|
prettyprint (0.2.0)
|
||||||
|
prism (1.4.0)
|
||||||
propshaft (1.1.0)
|
propshaft (1.1.0)
|
||||||
actionpack (>= 7.0.0)
|
actionpack (>= 7.0.0)
|
||||||
activesupport (>= 7.0.0)
|
activesupport (>= 7.0.0)
|
||||||
rack
|
rack
|
||||||
railties (>= 7.0.0)
|
railties (>= 7.0.0)
|
||||||
psych (5.2.3)
|
psych (5.2.6)
|
||||||
date
|
date
|
||||||
stringio
|
stringio
|
||||||
public_suffix (6.0.1)
|
public_suffix (6.0.2)
|
||||||
puma (6.6.0)
|
puma (6.6.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.5.0)
|
pundit (2.5.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.1.12)
|
rack (3.1.16)
|
||||||
rack-session (2.1.0)
|
rack-session (2.1.1)
|
||||||
base64 (>= 0.1.0)
|
base64 (>= 0.1.0)
|
||||||
rack (>= 3.0.0)
|
rack (>= 3.0.0)
|
||||||
rack-test (2.2.0)
|
rack-test (2.2.0)
|
||||||
@@ -257,7 +265,7 @@ GEM
|
|||||||
activesupport (= 8.0.2)
|
activesupport (= 8.0.2)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 8.0.2)
|
railties (= 8.0.2)
|
||||||
rails-dom-testing (2.2.0)
|
rails-dom-testing (2.3.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
@@ -273,31 +281,32 @@ GEM
|
|||||||
thor (~> 1.0, >= 1.2.2)
|
thor (~> 1.0, >= 1.2.2)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.2.1)
|
rake (13.3.0)
|
||||||
rdoc (6.12.0)
|
rdoc (6.14.1)
|
||||||
|
erb
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
regexp_parser (2.10.0)
|
regexp_parser (2.10.0)
|
||||||
reline (0.6.0)
|
reline (0.6.1)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
rexml (3.4.1)
|
rexml (3.4.1)
|
||||||
rspec-core (3.13.3)
|
rspec-core (3.13.4)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-expectations (3.13.3)
|
rspec-expectations (3.13.5)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-mocks (3.13.2)
|
rspec-mocks (3.13.5)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-rails (7.1.1)
|
rspec-rails (8.0.1)
|
||||||
actionpack (>= 7.0)
|
actionpack (>= 7.2)
|
||||||
activesupport (>= 7.0)
|
activesupport (>= 7.2)
|
||||||
railties (>= 7.0)
|
railties (>= 7.2)
|
||||||
rspec-core (~> 3.13)
|
rspec-core (~> 3.13)
|
||||||
rspec-expectations (~> 3.13)
|
rspec-expectations (~> 3.13)
|
||||||
rspec-mocks (~> 3.13)
|
rspec-mocks (~> 3.13)
|
||||||
rspec-support (~> 3.13)
|
rspec-support (~> 3.13)
|
||||||
rspec-support (3.13.2)
|
rspec-support (3.13.4)
|
||||||
rubocop (1.73.2)
|
rubocop (1.77.0)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (~> 3.17.0.2)
|
language_server-protocol (~> 3.17.0.2)
|
||||||
lint_roller (~> 1.1.0)
|
lint_roller (~> 1.1.0)
|
||||||
@@ -305,41 +314,42 @@ GEM
|
|||||||
parser (>= 3.3.0.2)
|
parser (>= 3.3.0.2)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 2.9.3, < 3.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)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 4.0)
|
unicode-display_width (>= 2.4.0, < 4.0)
|
||||||
rubocop-ast (1.38.1)
|
rubocop-ast (1.45.1)
|
||||||
parser (>= 3.3.1.0)
|
parser (>= 3.3.7.2)
|
||||||
rubocop-performance (1.24.0)
|
prism (~> 1.4)
|
||||||
|
rubocop-performance (1.25.0)
|
||||||
lint_roller (~> 1.1)
|
lint_roller (~> 1.1)
|
||||||
rubocop (>= 1.72.1, < 2.0)
|
rubocop (>= 1.75.0, < 2.0)
|
||||||
rubocop-ast (>= 1.38.0, < 2.0)
|
rubocop-ast (>= 1.38.0, < 2.0)
|
||||||
rubocop-rails (2.30.3)
|
rubocop-rails (2.32.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
lint_roller (~> 1.1)
|
lint_roller (~> 1.1)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.72.1, < 2.0)
|
rubocop (>= 1.75.0, < 2.0)
|
||||||
rubocop-ast (>= 1.38.0, < 2.0)
|
rubocop-ast (>= 1.44.0, < 2.0)
|
||||||
rubocop-rails-omakase (1.1.0)
|
rubocop-rails-omakase (1.1.0)
|
||||||
rubocop (>= 1.72)
|
rubocop (>= 1.72)
|
||||||
rubocop-performance (>= 1.24)
|
rubocop-performance (>= 1.24)
|
||||||
rubocop-rails (>= 2.30)
|
rubocop-rails (>= 2.30)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
rubyzip (2.4.1)
|
rubyzip (2.4.1)
|
||||||
sass-embedded (1.85.1-aarch64-linux-gnu)
|
sass-embedded (1.89.2-aarch64-linux-gnu)
|
||||||
google-protobuf (~> 4.29)
|
google-protobuf (~> 4.31)
|
||||||
sass-embedded (1.85.1-aarch64-linux-musl)
|
sass-embedded (1.89.2-aarch64-linux-musl)
|
||||||
google-protobuf (~> 4.29)
|
google-protobuf (~> 4.31)
|
||||||
sass-embedded (1.85.1-arm-linux-gnueabihf)
|
sass-embedded (1.89.2-arm-linux-gnueabihf)
|
||||||
google-protobuf (~> 4.29)
|
google-protobuf (~> 4.31)
|
||||||
sass-embedded (1.85.1-arm-linux-musleabihf)
|
sass-embedded (1.89.2-arm-linux-musleabihf)
|
||||||
google-protobuf (~> 4.29)
|
google-protobuf (~> 4.31)
|
||||||
sass-embedded (1.85.1-x86_64-linux-gnu)
|
sass-embedded (1.89.2-x86_64-linux-gnu)
|
||||||
google-protobuf (~> 4.29)
|
google-protobuf (~> 4.31)
|
||||||
sass-embedded (1.85.1-x86_64-linux-musl)
|
sass-embedded (1.89.2-x86_64-linux-musl)
|
||||||
google-protobuf (~> 4.29)
|
google-protobuf (~> 4.31)
|
||||||
securerandom (0.4.1)
|
securerandom (0.4.1)
|
||||||
selenium-webdriver (4.29.1)
|
selenium-webdriver (4.33.0)
|
||||||
base64 (~> 0.2)
|
base64 (~> 0.2)
|
||||||
logger (~> 1.4)
|
logger (~> 1.4)
|
||||||
rexml (~> 3.2, >= 3.2.5)
|
rexml (~> 3.2, >= 3.2.5)
|
||||||
@@ -348,7 +358,7 @@ GEM
|
|||||||
slim (5.2.1)
|
slim (5.2.1)
|
||||||
temple (~> 0.10.0)
|
temple (~> 0.10.0)
|
||||||
tilt (>= 2.1.0)
|
tilt (>= 2.1.0)
|
||||||
solid_cable (3.0.7)
|
solid_cable (3.0.10)
|
||||||
actioncable (>= 7.2)
|
actioncable (>= 7.2)
|
||||||
activejob (>= 7.2)
|
activejob (>= 7.2)
|
||||||
activerecord (>= 7.2)
|
activerecord (>= 7.2)
|
||||||
@@ -357,19 +367,19 @@ GEM
|
|||||||
activejob (>= 7.2)
|
activejob (>= 7.2)
|
||||||
activerecord (>= 7.2)
|
activerecord (>= 7.2)
|
||||||
railties (>= 7.2)
|
railties (>= 7.2)
|
||||||
solid_queue (1.1.3)
|
solid_queue (1.1.5)
|
||||||
activejob (>= 7.1)
|
activejob (>= 7.1)
|
||||||
activerecord (>= 7.1)
|
activerecord (>= 7.1)
|
||||||
concurrent-ruby (>= 1.3.1)
|
concurrent-ruby (>= 1.3.1)
|
||||||
fugit (~> 1.11.0)
|
fugit (~> 1.11.0)
|
||||||
railties (>= 7.1)
|
railties (>= 7.1)
|
||||||
thor (~> 1.3.1)
|
thor (~> 1.3.1)
|
||||||
sqlite3 (2.6.0-aarch64-linux-gnu)
|
sqlite3 (2.7.0-aarch64-linux-gnu)
|
||||||
sqlite3 (2.6.0-aarch64-linux-musl)
|
sqlite3 (2.7.0-aarch64-linux-musl)
|
||||||
sqlite3 (2.6.0-arm-linux-gnu)
|
sqlite3 (2.7.0-arm-linux-gnu)
|
||||||
sqlite3 (2.6.0-arm-linux-musl)
|
sqlite3 (2.7.0-arm-linux-musl)
|
||||||
sqlite3 (2.6.0-x86_64-linux-gnu)
|
sqlite3 (2.7.0-x86_64-linux-gnu)
|
||||||
sqlite3 (2.6.0-x86_64-linux-musl)
|
sqlite3 (2.7.0-x86_64-linux-musl)
|
||||||
sshkit (1.24.0)
|
sshkit (1.24.0)
|
||||||
base64
|
base64
|
||||||
logger
|
logger
|
||||||
@@ -379,15 +389,15 @@ GEM
|
|||||||
ostruct
|
ostruct
|
||||||
stimulus-rails (1.3.4)
|
stimulus-rails (1.3.4)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
stringio (3.1.5)
|
stringio (3.1.7)
|
||||||
temple (0.10.3)
|
temple (0.10.3)
|
||||||
thor (1.3.2)
|
thor (1.3.2)
|
||||||
thruster (0.1.12)
|
thruster (0.1.14)
|
||||||
thruster (0.1.12-aarch64-linux)
|
thruster (0.1.14-aarch64-linux)
|
||||||
thruster (0.1.12-x86_64-linux)
|
thruster (0.1.14-x86_64-linux)
|
||||||
tilt (2.6.0)
|
tilt (2.6.0)
|
||||||
timeout (0.4.3)
|
timeout (0.4.3)
|
||||||
turbo-rails (2.0.13)
|
turbo-rails (2.0.16)
|
||||||
actionpack (>= 7.1.0)
|
actionpack (>= 7.1.0)
|
||||||
railties (>= 7.1.0)
|
railties (>= 7.1.0)
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
@@ -403,13 +413,13 @@ GEM
|
|||||||
bindex (>= 0.4.0)
|
bindex (>= 0.4.0)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
websocket (1.2.11)
|
websocket (1.2.11)
|
||||||
websocket-driver (0.7.7)
|
websocket-driver (0.8.0)
|
||||||
base64
|
base64
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.7.2)
|
zeitwerk (2.7.3)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
aarch64-linux
|
aarch64-linux
|
||||||
@@ -428,6 +438,8 @@ DEPENDENCIES
|
|||||||
bootstrap (~> 5.3.3)
|
bootstrap (~> 5.3.3)
|
||||||
brakeman
|
brakeman
|
||||||
capybara
|
capybara
|
||||||
|
csv
|
||||||
|
damerau-levenshtein
|
||||||
dartsass-rails
|
dartsass-rails
|
||||||
debug
|
debug
|
||||||
factory_bot_rails
|
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
|
include CompletionsConcern
|
||||||
|
|
||||||
before_action :set_contest
|
before_action :set_contest
|
||||||
|
before_action :set_contestant
|
||||||
before_action :set_data, only: %i[ create edit new update ]
|
before_action :set_data, only: %i[ create edit new update ]
|
||||||
before_action :set_completion, only: %i[ destroy edit update ]
|
before_action :set_completion, only: %i[ destroy edit update ]
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
authorize @contest
|
authorize @contest
|
||||||
|
|
||||||
|
if @contestant
|
||||||
|
@action_name = t("helpers.buttons.back_to_contestant")
|
||||||
|
@action_path = edit_contest_contestant_path(@contest, @contestant)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
authorize @contest
|
authorize @contest
|
||||||
|
|
||||||
|
if @contestant
|
||||||
|
@action_name = t("helpers.buttons.back_to_contestant")
|
||||||
|
@action_path = edit_contest_contestant_path(@contest, @contestant)
|
||||||
|
end
|
||||||
|
|
||||||
@completion = Completion.new
|
@completion = Completion.new
|
||||||
if params[:contestant_id]
|
if params[:contestant_id]
|
||||||
@completion.contestant_id = params[:contestant_id]
|
@completion.contestant_id = params[:contestant_id]
|
||||||
@@ -25,10 +36,20 @@ class CompletionsController < ApplicationController
|
|||||||
@completion.contest_id = @contest.id
|
@completion.contest_id = @contest.id
|
||||||
if @completion.save
|
if @completion.save
|
||||||
extend_completions!(@completion.contestant)
|
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
|
||||||
|
redirect_to @contest, notice: t("completions.new.notice")
|
||||||
|
end
|
||||||
else
|
else
|
||||||
logger = Logger.new(STDOUT)
|
if params[:completion].key?(:message_id)
|
||||||
logger.info(@completion.errors)
|
@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
|
render :new, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -36,13 +57,19 @@ class CompletionsController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
authorize @contest
|
authorize @contest
|
||||||
|
|
||||||
if params[:contestant_id]
|
@completion.contestant_id = params[:contestant_id] if params[:contestant_id]
|
||||||
@completion.contestant_id = params[:contestant_id]
|
|
||||||
end
|
|
||||||
if @completion.update(completion_params)
|
if @completion.update(completion_params)
|
||||||
extend_completions!(@completion.contestant)
|
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
|
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
|
render :edit, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -52,9 +79,9 @@ class CompletionsController < ApplicationController
|
|||||||
|
|
||||||
@completion.destroy
|
@completion.destroy
|
||||||
if params[:contestant_id]
|
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
|
else
|
||||||
redirect_to contest_path(@contest)
|
redirect_to contest_path(@contest), notice: t("completions.destroy.notice")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -64,8 +91,16 @@ class CompletionsController < ApplicationController
|
|||||||
@contest = Contest.find(params[:contest_id])
|
@contest = Contest.find(params[:contest_id])
|
||||||
end
|
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
|
def set_data
|
||||||
@contestants = @contest.contestants
|
@contestants = @contest.contestants.order(:name)
|
||||||
@puzzles = @contest.puzzles
|
@puzzles = @contest.puzzles
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -74,6 +109,6 @@ class CompletionsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def completion_params
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,15 +8,16 @@ module CompletionsConcern
|
|||||||
"0" + n.to_s
|
"0" + n.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def display_time(seconds)
|
def display_time(time)
|
||||||
if seconds > 3600
|
h = time / 3600
|
||||||
hours = seconds / 3600
|
m = (time % 3600) / 60
|
||||||
return hours.to_s + ":" + display_time(seconds % 3600)
|
s = (time % 3600) % 60
|
||||||
elsif seconds > 60
|
if h > 0
|
||||||
minutes = seconds / 60
|
return h.to_s + ":" + pad(m) + ":" + pad(s)
|
||||||
return pad(minutes) + ":" + display_time(seconds % 60)
|
elsif m > 0
|
||||||
|
return m.to_s + ":" + pad(s)
|
||||||
end
|
end
|
||||||
pad(seconds)
|
s.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def extend_completions!(contestant)
|
def extend_completions!(contestant)
|
||||||
@@ -26,6 +27,6 @@ module CompletionsConcern
|
|||||||
display_relative_time: display_time(completion.time_seconds - current_time_from_start))
|
display_relative_time: display_time(completion.time_seconds - current_time_from_start))
|
||||||
current_time_from_start = completion.time_seconds
|
current_time_from_start = completion.time_seconds
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,11 +5,16 @@ class ContestantsController < ApplicationController
|
|||||||
|
|
||||||
def edit
|
def edit
|
||||||
authorize @contest
|
authorize @contest
|
||||||
|
|
||||||
|
@action_name = t("helpers.buttons.back")
|
||||||
|
@action_path = contest_path(@contest)
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
authorize @contest
|
authorize @contest
|
||||||
|
|
||||||
|
@action_name = t("helpers.buttons.back")
|
||||||
|
@action_path = contest_path(@contest)
|
||||||
@contestant = Contestant.new
|
@contestant = Contestant.new
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -19,8 +24,11 @@ class ContestantsController < ApplicationController
|
|||||||
@contestant = Contestant.new(contestant_params)
|
@contestant = Contestant.new(contestant_params)
|
||||||
@contestant.contest_id = @contest.id
|
@contestant.contest_id = @contest.id
|
||||||
if @contestant.save
|
if @contestant.save
|
||||||
redirect_to contest_path(@contest)
|
update_contestant_categories
|
||||||
|
redirect_to contest_path(@contest), notice: t("contestants.new.notice")
|
||||||
else
|
else
|
||||||
|
@action_name = t("helpers.buttons.back")
|
||||||
|
@action_path = contest_path(@contest)
|
||||||
render :new, status: :unprocessable_entity
|
render :new, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -29,8 +37,11 @@ class ContestantsController < ApplicationController
|
|||||||
authorize @contest
|
authorize @contest
|
||||||
|
|
||||||
if @contestant.update(contestant_params)
|
if @contestant.update(contestant_params)
|
||||||
redirect_to @contest
|
update_contestant_categories
|
||||||
|
redirect_to @contest, notice: t("contestants.edit.notice")
|
||||||
else
|
else
|
||||||
|
@action_name = t("helpers.buttons.back")
|
||||||
|
@action_path = contest_path(@contest)
|
||||||
render :edit, status: :unprocessable_entity
|
render :edit, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -39,7 +50,83 @@ class ContestantsController < ApplicationController
|
|||||||
authorize @contest
|
authorize @contest
|
||||||
|
|
||||||
@contestant.destroy
|
@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
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -59,4 +146,15 @@ class ContestantsController < ApplicationController
|
|||||||
def contestant_params
|
def contestant_params
|
||||||
params.expect(contestant: [ :email, :name ])
|
params.expect(contestant: [ :email, :name ])
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -13,13 +13,25 @@ class ContestsController < ApplicationController
|
|||||||
authorize @contest
|
authorize @contest
|
||||||
|
|
||||||
@title = I18n.t("contests.show.title", name: @contest.name)
|
@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)
|
@puzzles = @contest.puzzles.order(:id)
|
||||||
|
@messages = @contest.messages.order(:time_seconds)
|
||||||
set_badges
|
set_badges
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
authorize @contest
|
authorize @contest
|
||||||
|
|
||||||
|
@action_name = t("helpers.buttons.back")
|
||||||
|
@action_path = contest_path(@contest)
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@@ -34,7 +46,7 @@ class ContestsController < ApplicationController
|
|||||||
@contest = Contest.new(contest_params)
|
@contest = Contest.new(contest_params)
|
||||||
@contest.user_id = current_user.id
|
@contest.user_id = current_user.id
|
||||||
if @contest.save
|
if @contest.save
|
||||||
redirect_to @contest
|
redirect_to @contest, notice: t("contests.new.notice")
|
||||||
else
|
else
|
||||||
render :new, status: :unprocessable_entity
|
render :new, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -44,27 +56,46 @@ class ContestsController < ApplicationController
|
|||||||
authorize @contest
|
authorize @contest
|
||||||
|
|
||||||
if @contest.update(contest_params)
|
if @contest.update(contest_params)
|
||||||
redirect_to @contest
|
redirect_to @contest, notice: t("contests.edit.notice")
|
||||||
else
|
else
|
||||||
|
@action_name = t("helpers.buttons.back")
|
||||||
|
@action_path = contest_path(@contest)
|
||||||
render :edit, status: :unprocessable_entity
|
render :edit, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
authorize @contest
|
authorize @contest
|
||||||
|
|
||||||
|
@contest.destroy
|
||||||
|
redirect_to contests_path, notice: t("contests.destroy.notice")
|
||||||
end
|
end
|
||||||
|
|
||||||
def scoreboard
|
def scoreboard
|
||||||
@contest = Contest.find_by(slug: params[:id])
|
@contest = Contest.find_by(slug: params[:id])
|
||||||
unless @contest
|
unless @contest && @contest.public
|
||||||
skip_authorization
|
skip_authorization
|
||||||
not_found and return
|
not_found and return
|
||||||
end
|
end
|
||||||
authorize @contest
|
authorize @contest
|
||||||
|
|
||||||
|
I18n.locale = @contest.lang
|
||||||
|
|
||||||
@title = I18n.t("contests.scoreboard.title", name: @contest.name)
|
@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)
|
@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
|
render :scoreboard
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -72,8 +103,8 @@ class ContestsController < ApplicationController
|
|||||||
|
|
||||||
def set_badges
|
def set_badges
|
||||||
@badges = []
|
@badges = []
|
||||||
@badges.push("team") if @contest.team
|
@badges.push(t("helpers.badges.team")) if @contest.team
|
||||||
@badges.push("registration") if @contest.allow_registration
|
@badges.push(t("helpers.badges.registration")) if @contest.allow_registration
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_contest
|
def set_contest
|
||||||
@@ -81,6 +112,16 @@ class ContestsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def contest_params
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,25 +1,113 @@
|
|||||||
class MessagesController < ApplicationController
|
class MessagesController < ApplicationController
|
||||||
allow_unauthenticated_access
|
include CompletionsConcern
|
||||||
skip_before_action :verify_authenticity_token
|
|
||||||
|
|
||||||
def create
|
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
|
skip_authorization
|
||||||
|
|
||||||
@message_params = message_params
|
if !params.key?(:token)
|
||||||
@contest = Contest.find_by_token_for(:token, params[:token])
|
|
||||||
@message = Message.new(text: params[:text], time_seconds: params[:time_seconds], contest: @contest)
|
|
||||||
if @contest && @message.save
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.json { render json: {}, status: 200 }
|
format.json { render json: { error: "no token provided" }, status: 400 }
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
respond_to do |format|
|
@contest = Contest.find_by_token_for(:token, params[:token])
|
||||||
format.json { render json: { error: "invalid contest token" }, status: 400 }
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def message_params
|
def create
|
||||||
params.expect(message: [ :text, :time_seconds, :token ])
|
skip_authorization
|
||||||
|
|
||||||
|
begin
|
||||||
|
@contest = Contest.find_by_token_for(:token, params[:token])
|
||||||
|
@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 token" }, status: 400 }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue
|
||||||
|
respond_to do |format|
|
||||||
|
format.json { render json: { error: "invalid token" }, status: 400 }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,11 +4,16 @@ class PuzzlesController < ApplicationController
|
|||||||
|
|
||||||
def edit
|
def edit
|
||||||
authorize @contest
|
authorize @contest
|
||||||
|
|
||||||
|
@action_name = t("helpers.buttons.back")
|
||||||
|
@action_path = contest_path(@contest)
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
authorize @contest
|
authorize @contest
|
||||||
|
|
||||||
|
@action_name = t("helpers.buttons.back")
|
||||||
|
@action_path = contest_path(@contest)
|
||||||
@puzzle = Puzzle.new
|
@puzzle = Puzzle.new
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -18,8 +23,10 @@ class PuzzlesController < ApplicationController
|
|||||||
@puzzle = Puzzle.new(puzzle_params)
|
@puzzle = Puzzle.new(puzzle_params)
|
||||||
@puzzle.contest_id = @contest.id
|
@puzzle.contest_id = @contest.id
|
||||||
if @puzzle.save
|
if @puzzle.save
|
||||||
redirect_to contest_path(@contest)
|
redirect_to contest_path(@contest), notice: t("puzzles.new.notice")
|
||||||
else
|
else
|
||||||
|
@action_name = t("helpers.buttons.back")
|
||||||
|
@action_path = contest_path(@contest)
|
||||||
render :new, status: :unprocessable_entity
|
render :new, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -28,8 +35,10 @@ class PuzzlesController < ApplicationController
|
|||||||
authorize @contest
|
authorize @contest
|
||||||
|
|
||||||
if @puzzle.update(puzzle_params)
|
if @puzzle.update(puzzle_params)
|
||||||
redirect_to @contest
|
redirect_to @contest, notice: t("puzzles.edit.notice")
|
||||||
else
|
else
|
||||||
|
@action_name = t("helpers.buttons.back")
|
||||||
|
@action_path = contest_path(@contest)
|
||||||
render :edit, status: :unprocessable_entity
|
render :edit, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -38,7 +47,7 @@ class PuzzlesController < ApplicationController
|
|||||||
authorize @contest
|
authorize @contest
|
||||||
|
|
||||||
@puzzle.destroy
|
@puzzle.destroy
|
||||||
redirect_to contest_path(@contest)
|
redirect_to contest_path(@contest), notice: t("puzzles.destroy.notice")
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -52,6 +61,6 @@ class PuzzlesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def puzzle_params
|
def puzzle_params
|
||||||
params.expect(puzzle: [ :brand, :name, :image ])
|
params.expect(puzzle: [ :brand, :name, :image, :pieces ])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class SessionsController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
if user = User.authenticate_by(params.permit(:email_address, :password))
|
if user = User.authenticate_by(params.permit(:email_address, :password))
|
||||||
start_new_session_for user
|
start_new_session_for user
|
||||||
redirect_to after_authentication_url
|
redirect_to after_authentication_url, notice: t("sessions.new.notice")
|
||||||
else
|
else
|
||||||
redirect_to new_session_path, alert: "Try another email address or password."
|
redirect_to new_session_path, alert: "Try another email address or password."
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class UsersController < ApplicationController
|
|||||||
authorize @user
|
authorize @user
|
||||||
|
|
||||||
if @user.update(user_params)
|
if @user.update(user_params)
|
||||||
redirect_to contests_path
|
redirect_to contests_path, notice: t("users.edit.notice")
|
||||||
else
|
else
|
||||||
render :edit, status: :unprocessable_entity
|
render :edit, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -38,7 +38,7 @@ class UsersController < ApplicationController
|
|||||||
|
|
||||||
@user = User.new(user_params)
|
@user = User.new(user_params)
|
||||||
if @user.save
|
if @user.save
|
||||||
redirect_to users_path
|
redirect_to users_path, notice: t("users.new.notice")
|
||||||
else
|
else
|
||||||
render :new, status: :unprocessable_entity
|
render :new, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,2 +1,20 @@
|
|||||||
module ContestsHelper
|
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
|
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
|
module Languages
|
||||||
AVAILABLE_LANGUAGES = [ { id: "en", name: "English" }, { id: "fr", name: "French" } ]
|
AVAILABLE_LANGUAGES = [ { id: "en", name: "English" }, { id: "fr", name: "Français" } ]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
# == Schema Information
|
# == Schema Information
|
||||||
#
|
#
|
||||||
# Table name: puzzles
|
# Table name: categories
|
||||||
#
|
#
|
||||||
# id :integer not null, primary key
|
# id :integer not null, primary key
|
||||||
# brand :string
|
|
||||||
# name :string
|
# name :string
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
@@ -11,16 +10,15 @@
|
|||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_puzzles_on_contest_id (contest_id)
|
# index_categories_on_contest_id (contest_id)
|
||||||
#
|
#
|
||||||
# Foreign Keys
|
# Foreign Keys
|
||||||
#
|
#
|
||||||
# contest_id (contest_id => contests.id)
|
# 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
|
validates :name, presence: true
|
||||||
# test "the truth" do
|
|
||||||
# assert true
|
|
||||||
# end
|
|
||||||
end
|
end
|
||||||
@@ -5,31 +5,64 @@
|
|||||||
# id :integer not null, primary key
|
# id :integer not null, primary key
|
||||||
# display_relative_time :string
|
# display_relative_time :string
|
||||||
# display_time_from_start :string
|
# display_time_from_start :string
|
||||||
|
# remaining_pieces :integer
|
||||||
# time_seconds :integer
|
# time_seconds :integer
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# contest_id :integer not null
|
# contest_id :integer not null
|
||||||
# contestant_id :integer not null
|
# contestant_id :integer not null
|
||||||
|
# message_id :integer
|
||||||
# puzzle_id :integer not null
|
# puzzle_id :integer not null
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_completions_on_contest_id (contest_id)
|
# index_completions_on_contest_id (contest_id)
|
||||||
# index_completions_on_contestant_id (contestant_id)
|
# index_completions_on_contestant_id (contestant_id)
|
||||||
|
# index_completions_on_message_id (message_id)
|
||||||
# index_completions_on_puzzle_id (puzzle_id)
|
# index_completions_on_puzzle_id (puzzle_id)
|
||||||
#
|
#
|
||||||
# Foreign Keys
|
# Foreign Keys
|
||||||
#
|
#
|
||||||
# contest_id (contest_id => contests.id)
|
# contest_id (contest_id => contests.id)
|
||||||
# contestant_id (contestant_id => contestants.id)
|
# contestant_id (contestant_id => contestants.id)
|
||||||
|
# message_id (message_id => messages.id)
|
||||||
# puzzle_id (puzzle_id => puzzles.id)
|
# puzzle_id (puzzle_id => puzzles.id)
|
||||||
#
|
#
|
||||||
class Completion < ApplicationRecord
|
class Completion < ApplicationRecord
|
||||||
belongs_to :contest
|
belongs_to :contest
|
||||||
belongs_to :contestant
|
belongs_to :contestant
|
||||||
belongs_to :puzzle
|
belongs_to :puzzle
|
||||||
|
belongs_to :message, optional: true
|
||||||
|
|
||||||
validates :time_seconds, presence: true
|
before_save :add_time_seconds
|
||||||
validates_numericality_of :time_seconds
|
before_save :nullify_display_time
|
||||||
validates :puzzle_id, uniqueness: { scope: :contestant }
|
|
||||||
|
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
|
end
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
#
|
#
|
||||||
# id :integer not null, primary key
|
# id :integer not null, primary key
|
||||||
# allow_registration :boolean default(FALSE)
|
# allow_registration :boolean default(FALSE)
|
||||||
|
# lang :string default("en")
|
||||||
# name :string
|
# name :string
|
||||||
|
# public :boolean default(FALSE)
|
||||||
# slug :string
|
# slug :string
|
||||||
# team :boolean default(FALSE)
|
# team :boolean default(FALSE)
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
@@ -24,14 +26,16 @@ class Contest < ApplicationRecord
|
|||||||
extend FriendlyId
|
extend FriendlyId
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
has_many :categories
|
||||||
has_many :completions, dependent: :destroy
|
has_many :completions, dependent: :destroy
|
||||||
has_many :contestants, dependent: :destroy
|
has_many :contestants, dependent: :destroy
|
||||||
has_many :puzzles, dependent: :destroy
|
has_many :puzzles, dependent: :destroy
|
||||||
has_many :messages
|
has_many :messages, dependent: :destroy
|
||||||
|
|
||||||
friendly_id :name, use: :slugged
|
friendly_id :name, use: :slugged
|
||||||
|
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
|
validates :lang, inclusion: { in: Languages::AVAILABLE_LANGUAGES.map { |lang| lang[:id] } }
|
||||||
|
|
||||||
generates_token_for :token
|
generates_token_for :token
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
# display_time :string
|
# display_time :string
|
||||||
# email :string
|
# email :string
|
||||||
# name :string
|
# name :string
|
||||||
|
# time_seconds :integer
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# contest_id :integer not null
|
# contest_id :integer not null
|
||||||
@@ -20,7 +21,27 @@
|
|||||||
#
|
#
|
||||||
class Contestant < ApplicationRecord
|
class Contestant < ApplicationRecord
|
||||||
belongs_to :contest
|
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 :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
|
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
|
# Table name: messages
|
||||||
#
|
#
|
||||||
# id :integer not null, primary key
|
# id :integer not null, primary key
|
||||||
|
# author :string
|
||||||
|
# display_time :string
|
||||||
# text :string not null
|
# text :string not null
|
||||||
# time_seconds :integer not null
|
# time_seconds :integer not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
@@ -19,6 +21,8 @@
|
|||||||
#
|
#
|
||||||
class Message < ApplicationRecord
|
class Message < ApplicationRecord
|
||||||
belongs_to :contest
|
belongs_to :contest
|
||||||
|
has_many :completions, dependent: :nullify
|
||||||
|
|
||||||
|
validates :author, presence: true
|
||||||
validates :text, presence: true
|
validates :text, presence: true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
# id :integer not null, primary key
|
# id :integer not null, primary key
|
||||||
# brand :string
|
# brand :string
|
||||||
# name :string
|
# name :string
|
||||||
|
# pieces :integer not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# contest_id :integer not null
|
# contest_id :integer not null
|
||||||
@@ -20,9 +21,9 @@
|
|||||||
class Puzzle < ApplicationRecord
|
class Puzzle < ApplicationRecord
|
||||||
belongs_to :contest
|
belongs_to :contest
|
||||||
|
|
||||||
has_many :completions
|
has_many :completions, dependent: :destroy
|
||||||
has_one_attached :image
|
has_one_attached :image
|
||||||
|
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
validates :brand, presence: true
|
validates :pieces, presence: true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -23,5 +23,6 @@ class User < ApplicationRecord
|
|||||||
normalizes :email_address, with: ->(e) { e.strip.downcase }
|
normalizes :email_address, with: ->(e) { e.strip.downcase }
|
||||||
|
|
||||||
validates :username, presence: true, uniqueness: true
|
validates :username, presence: true, uniqueness: true
|
||||||
|
validates :email_address, presence: true, uniqueness: true
|
||||||
validates :lang, inclusion: { in: Languages::AVAILABLE_LANGUAGES.map { |lang| lang[:id] } }
|
validates :lang, inclusion: { in: Languages::AVAILABLE_LANGUAGES.map { |lang| lang[:id] } }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,10 +15,22 @@ class ContestPolicy < ApplicationPolicy
|
|||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def convert?
|
||||||
|
record.user.id == user.id || user.admin?
|
||||||
|
end
|
||||||
|
|
||||||
|
def convert_csv?
|
||||||
|
record.user.id == user.id || user.admin?
|
||||||
|
end
|
||||||
|
|
||||||
def edit?
|
def edit?
|
||||||
record.user.id == user.id || user.admin?
|
record.user.id == user.id || user.admin?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def finalize_import?
|
||||||
|
record.user.id == user.id || user.admin?
|
||||||
|
end
|
||||||
|
|
||||||
def update?
|
def update?
|
||||||
record.user.id == user.id || user.admin?
|
record.user.id == user.id || user.admin?
|
||||||
end
|
end
|
||||||
@@ -27,7 +39,19 @@ class ContestPolicy < ApplicationPolicy
|
|||||||
record.user.id == user.id || user.admin?
|
record.user.id == user.id || user.admin?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def import?
|
||||||
|
record.user.id == user.id || user.admin?
|
||||||
|
end
|
||||||
|
|
||||||
|
def export?
|
||||||
|
record.user.id == user.id || user.admin?
|
||||||
|
end
|
||||||
|
|
||||||
def scoreboard?
|
def scoreboard?
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def upload_csv?
|
||||||
|
record.user.id == user.id || user.admin?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,19 +1,48 @@
|
|||||||
= form_with model: completion, url: url, method: method do |form|
|
= 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
|
.row.mb-3
|
||||||
.col
|
.col
|
||||||
.form-floating
|
.form-floating
|
||||||
= form.text_field :time_seconds, autocomplete: "off", class: "form-control"
|
= form.select :contestant_id, @contestants.map { |contestant| [contestant.form_name, contestant.id] }, {}, class: "form-select"
|
||||||
= 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.label :contestant_id
|
= 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
|
.row.mb-3
|
||||||
.col
|
.col
|
||||||
.form-floating
|
.form-floating
|
||||||
= form.select :puzzle_id, @puzzles.map { |puzzle| ["#{puzzle.name} - #{puzzle.brand}", puzzle.id] }, {}, class: "form-select"
|
= form.text_field :display_time_from_start, autocomplete: "off", class: "form-control"
|
||||||
= form.label :puzzle_id
|
= 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
|
.row
|
||||||
.col
|
.col
|
||||||
= form.submit submit_text, class: "btn btn-primary"
|
= 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-floating
|
||||||
= form.text_field :email, autocomplete: "off", class: "form-control"
|
= form.text_field :email, autocomplete: "off", class: "form-control"
|
||||||
= form.label :email
|
= 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
|
.row.mt-4
|
||||||
.col
|
.col
|
||||||
- if method == :patch
|
- 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"
|
= form.submit submit_text, class: "btn btn-primary"
|
||||||
|
|
||||||
- if method == :patch
|
- if method == :patch
|
||||||
@@ -27,27 +36,39 @@
|
|||||||
table.table.table-striped.table-hover
|
table.table.table-striped.table-hover
|
||||||
thead
|
thead
|
||||||
tr
|
tr
|
||||||
|
- if @contest.puzzles.size > 1
|
||||||
|
th scope="col"
|
||||||
|
= t("activerecord.attributes.completion.display_time_from_start")
|
||||||
|
th scope="col"
|
||||||
|
= t("activerecord.attributes.completion.display_relative_time")
|
||||||
|
- else
|
||||||
|
th scope="col"
|
||||||
|
= t("activerecord.attributes.completion.display_time")
|
||||||
th scope="col"
|
th scope="col"
|
||||||
| Time since start
|
= t("activerecord.attributes.completion.remaining_pieces")
|
||||||
th scope="col"
|
th scope="col"
|
||||||
| Relative time
|
= t("activerecord.attributes.completion.puzzle")
|
||||||
th scope="col"
|
|
||||||
| Puzzle
|
|
||||||
tbody
|
tbody
|
||||||
- @completions.each do |completion|
|
- @completions.each do |completion|
|
||||||
tr scope="row"
|
tr scope="row"
|
||||||
td
|
td
|
||||||
= completion.display_time_from_start
|
= completion.display_time_from_start
|
||||||
|
- if @contest.puzzles.size > 1
|
||||||
|
td
|
||||||
|
= completion.display_relative_time
|
||||||
td
|
td
|
||||||
= completion.display_relative_time
|
= completion.remaining_pieces
|
||||||
td
|
td
|
||||||
| #{completion.puzzle.name} - #{completion.puzzle.brand}
|
- if !completion.puzzle.brand.blank?
|
||||||
|
| #{completion.puzzle.name} - #{completion.puzzle.brand}
|
||||||
|
- else
|
||||||
|
| #{completion.puzzle.name}
|
||||||
td
|
td
|
||||||
a.btn.btn-sm.btn-secondary.me-2 href=edit_contest_completion_path(@contest, completion, contestant.id)
|
a.btn.btn-sm.btn-secondary.me-2 href=edit_contest_completion_path(@contest, completion, contestant_id: contestant.id)
|
||||||
| Edit
|
= t("helpers.buttons.edit")
|
||||||
= link_to "Delete", contest_completion_path(contest, completion, contestant_id: contestant.id),
|
= 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"
|
data: { turbo_method: :delete }, class: "btn btn-sm btn-secondary"
|
||||||
.row
|
.row
|
||||||
.col
|
.col
|
||||||
a.btn.btn-primary href=new_contest_completion_path(@contest, contestant_id: contestant.id)
|
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|
|
= form_with model: contest do |form|
|
||||||
.row.mb-3
|
.row.mb-3
|
||||||
.col
|
.col
|
||||||
.form-floating
|
.form-floating
|
||||||
= form.text_field :name, autocomplete: "off", class: "form-control"
|
= form.text_field :name, autocomplete: "off", class: "form-control"
|
||||||
= form.label :name, class: "required"
|
= 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
|
.row.mb-3
|
||||||
.col
|
.col
|
||||||
.form-check.form-switch
|
.form-check.form-switch
|
||||||
= form.check_box :team, class: "form-check-input"
|
= form.check_box :team, class: "form-check-input"
|
||||||
= form.label :team
|
= form.label :team
|
||||||
.form-text = t("activerecord.attributes.contest.team_description")
|
.form-text = t("activerecord.attributes.contest.team_description")
|
||||||
.row.mb-3
|
.row.mb-3 style="display: none"
|
||||||
.col
|
.col
|
||||||
.form-check.form-switch
|
.form-check.form-switch
|
||||||
= form.check_box :allow_registration, class: "form-check-input"
|
= form.check_box :allow_registration, class: "form-check-input"
|
||||||
@@ -18,4 +29,37 @@
|
|||||||
.form-text = t("activerecord.attributes.contest.allow_registration_description")
|
.form-text = t("activerecord.attributes.contest.allow_registration_description")
|
||||||
.row
|
.row
|
||||||
.col
|
.col
|
||||||
= form.submit submit_text, class: "btn btn-primary"
|
= 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,22 +1,94 @@
|
|||||||
table.table.table-striped.table-hover
|
css:
|
||||||
thead
|
@media (max-width: 800px) {
|
||||||
tr
|
a.btn { display: none; }
|
||||||
th scope="col"
|
.col-5 { display: none; }
|
||||||
| Rank
|
.col-6 { width: 100% !important; display: block !important; }
|
||||||
th scope="col"
|
.small-screen-image { display: block !important; }
|
||||||
| Name
|
.container { margin-top: 2rem !important; }
|
||||||
th scope="col"
|
}
|
||||||
| Completed puzzles
|
|
||||||
th scope="col"
|
- if @contest.puzzles.size <= 1
|
||||||
| Total time
|
.row.small-screen-image style="display: none"
|
||||||
tbody
|
- @contest.puzzles.each do |puzzle|
|
||||||
- @contestants.each_with_index do |contestant, index|
|
.d-flex.flex-column.justify-content-center.mb-5
|
||||||
tr scope="row"
|
= image_tag(puzzle.image, style: "max-height: 200px; object-fit: contain") if puzzle.image.attached?
|
||||||
td
|
.mt-2.fs-6 style="text-align: center"
|
||||||
= index + 1
|
=> "#{puzzle.name} -"
|
||||||
td
|
= "#{puzzle.brand} #{puzzle.pieces}p"
|
||||||
= contestant.name
|
|
||||||
td
|
= render "category_selector"
|
||||||
= contestant.completions.length
|
|
||||||
td
|
.row
|
||||||
= contestant.display_time
|
.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
|
||||||
|
= 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"
|
||||||
|
td
|
||||||
|
= index + 1
|
||||||
|
td
|
||||||
|
= contestant.name
|
||||||
|
- if @contest.puzzles.size > 1
|
||||||
|
td
|
||||||
|
= 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
|
||||||
|
= 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 @@
|
|||||||
.row.mb-4
|
- if @badges.size > 0 && false
|
||||||
.col
|
.row.mb-4
|
||||||
css:
|
.col
|
||||||
.badges { margin-top: -18px; position: absolute; }
|
.badges style="margin-top: -18px; position: absolute"
|
||||||
.badges
|
- @badges.each do |badge|
|
||||||
- @badges.each do |badge|
|
span.badge.text-bg-info.me-2
|
||||||
span.badge.text-bg-info.me-2
|
= badge
|
||||||
= badge
|
|
||||||
|
|
||||||
.row.mb-4
|
javascript:
|
||||||
|
async function copyExtensionUrlToClipboard() {
|
||||||
|
await navigator.clipboard.writeText("#{message_url}?token=#{@contest.generate_token_for(:token)}");
|
||||||
|
alert("#{t("contests.show.url_copied")}");
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.mb-5
|
||||||
.col
|
.col
|
||||||
.float-end
|
- if @contest.public
|
||||||
a.btn.btn-primary href=edit_contest_path(@contest)
|
a.btn.btn-success href="/public/#{@contest.slug}"
|
||||||
| Edit contest
|
= t("contests.show.open_public_scoreboard")
|
||||||
p
|
- else
|
||||||
= t("contests.show.public_scoreboard")
|
a.btn.btn-success.disabled
|
||||||
= link_to root_url + "public/#{@contest.slug}", root_url + "public/#{@contest.slug}"
|
= 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
|
.row.mb-4 style="height: calc(100vh - 280px)"
|
||||||
.col-6
|
.col-6.d-flex.flex-column style="height: 100%"
|
||||||
.row
|
.row
|
||||||
.col
|
.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
|
||||||
|
= 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
|
table.table.table-striped.table-hover
|
||||||
thead
|
thead
|
||||||
tr
|
tr
|
||||||
th scope="col"
|
th
|
||||||
| Image
|
= t("activerecord.attributes.puzzle.image")
|
||||||
th scope="col"
|
th
|
||||||
| Title
|
= t("activerecord.attributes.puzzle.name")
|
||||||
th scope="col"
|
th
|
||||||
| Brand
|
= t("activerecord.attributes.puzzle.brand")
|
||||||
|
th
|
||||||
|
= t("activerecord.attributes.puzzle.pieces")
|
||||||
tbody
|
tbody
|
||||||
- @puzzles.each do |puzzle|
|
- @puzzles.each do |puzzle|
|
||||||
tr.align-middle scope="row"
|
tr.align-middle scope="row"
|
||||||
td
|
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
|
td
|
||||||
= puzzle.name
|
= puzzle.name
|
||||||
td
|
td
|
||||||
= puzzle.brand
|
= puzzle.brand
|
||||||
|
td
|
||||||
|
= puzzle.pieces
|
||||||
td
|
td
|
||||||
a.btn.btn-sm.btn-secondary href=edit_contest_puzzle_path(@contest, puzzle)
|
a.btn.btn-sm.btn-secondary href=edit_contest_puzzle_path(@contest, puzzle)
|
||||||
| Edit
|
= t("helpers.buttons.edit")
|
||||||
.row
|
- if @messages
|
||||||
.col
|
.row.mt-5
|
||||||
a.btn.btn-primary href=new_contest_puzzle_path(@contest)
|
.col
|
||||||
= t("contests.show.add_puzzle")
|
h4 = t("messages.plural").capitalize
|
||||||
.col-6
|
- if @puzzles.size == 0
|
||||||
.row
|
.row
|
||||||
.col
|
.col.alert.alert-danger
|
||||||
h4 = t("contestants.plural").capitalize
|
= t("messages.warning")
|
||||||
table.table.table-striped.table-hover
|
.d-flex.flex-column style="overflow-y: auto"
|
||||||
thead
|
table.table.table-striped.table-hover
|
||||||
tr
|
thead
|
||||||
th scope="col"
|
tr
|
||||||
| Name
|
th scope="col" style="white-space: nowrap"
|
||||||
th scope="col"
|
= t("activerecord.attributes.message.processed")
|
||||||
| Completed puzzles
|
th scope="col"
|
||||||
tbody
|
= t("activerecord.attributes.message.time")
|
||||||
- @contestants.each do |contestant|
|
th scope="col"
|
||||||
tr scope="row"
|
= t("activerecord.attributes.message.author")
|
||||||
td
|
th.w-25 scope="col"
|
||||||
= contestant.name
|
= t("activerecord.attributes.message.text")
|
||||||
td
|
th.w-25 scope="col"
|
||||||
= contestant.completions.length
|
tbody
|
||||||
td
|
- @messages.each do |message|
|
||||||
a.btn.btn-sm.btn-secondary href=edit_contest_contestant_path(@contest, contestant)
|
tr.align-middle scope="row"
|
||||||
| Open
|
td style="text-align: center"
|
||||||
a.btn.btn-sm.btn-secondary.ms-2 href=new_contest_completion_path(@contest, contestant_id: contestant.id)
|
- if message.completions.size > 0
|
||||||
| Add completion
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-square" viewBox="0 0 16 16">
|
||||||
.row.mt-4
|
<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"/>
|
||||||
.col
|
<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"/>
|
||||||
a.btn.btn-primary href=new_contest_contestant_path(@contest)
|
</svg>
|
||||||
= t("contests.show.add_participant")
|
td
|
||||||
|
= message.display_time
|
||||||
|
td
|
||||||
|
= message.author
|
||||||
|
td
|
||||||
|
= message.text
|
||||||
|
td
|
||||||
|
.d-inline-flex
|
||||||
|
- if @puzzles.size > 0
|
||||||
|
a.btn.btn-sm.btn-secondary href=contest_message_convert_path(@contest, message) style="white-space: nowrap;"
|
||||||
|
= t("helpers.buttons.add_completion")
|
||||||
|
- else
|
||||||
|
a.btn.btn-sm.btn-secondary.disabled href=contest_message_convert_path(@contest, message) style="white-space: nowrap;"
|
||||||
|
= t("helpers.buttons.add_completion")
|
||||||
|
= link_to "x", contest_message_path(@contest, message), data: { turbo_method: :delete }, class: "btn btn-sm btn-danger ms-2"
|
||||||
|
|||||||
@@ -8,14 +8,45 @@ html
|
|||||||
.float-end style="margin-top: -8px;"
|
.float-end style="margin-top: -8px;"
|
||||||
nav.navbar.bg-body-primary
|
nav.navbar.bg-body-primary
|
||||||
- if @current_user.admin
|
- 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")
|
= 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")
|
= 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")
|
= 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
|
= yield
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
= form_with model: puzzle, url: url, method: method do |form|
|
= 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
|
.row.mb-3
|
||||||
.col
|
.col
|
||||||
.form-floating
|
.form-floating
|
||||||
@@ -11,10 +17,32 @@
|
|||||||
= form.label :brand, class: "required"
|
= form.label :brand, class: "required"
|
||||||
.row.mb-3
|
.row.mb-3
|
||||||
.col
|
.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.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
|
.row.mt-4
|
||||||
.col
|
.col
|
||||||
- if method == :patch
|
- 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"
|
= 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-floating
|
||||||
= form.email_field :email_address, autocomplete: "username", required: true, autofocus: true, class: "form-control"
|
= form.email_field :email_address, autocomplete: "username", required: true, autofocus: true, class: "form-control"
|
||||||
= form.label :email_address, class: "required"
|
= form.label :email_address, class: "required"
|
||||||
|
= t("activerecord.attributes.session.email_address")
|
||||||
.row.mb-3
|
.row.mb-3
|
||||||
.col
|
.col
|
||||||
.form-floating
|
.form-floating
|
||||||
= form.password_field :password, autocomplete: "current-password", required: true, autofocus: true, class: "form-control", maxlength: 72
|
= form.password_field :password, autocomplete: "current-password", required: true, autofocus: true, class: "form-control", maxlength: 72
|
||||||
= form.label :password, class: "required"
|
= 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"
|
# enabled: "ON"
|
||||||
|
|
||||||
en:
|
en:
|
||||||
|
activemodel:
|
||||||
|
errors:
|
||||||
|
models:
|
||||||
|
forms/csv_conversion_form:
|
||||||
|
attributes:
|
||||||
|
name_column:
|
||||||
|
blank: "Participant names are required"
|
||||||
|
greater_than: "Participant names are required"
|
||||||
activerecord:
|
activerecord:
|
||||||
attributes:
|
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:
|
contest:
|
||||||
name: "Name"
|
lang: Language for the public scoreboard
|
||||||
team: "Team contest"
|
name: Name
|
||||||
team_description: "For UI display purposes mainly"
|
public: Enable the public scoreboard
|
||||||
allow_registration: "Allow registration"
|
team: Team contest
|
||||||
allow_registration_description: "Generates a shareable registration form for this 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:
|
user:
|
||||||
username: "Username"
|
username: Username
|
||||||
email_address: "Email address"
|
email_address: Email address
|
||||||
lang: "Language"
|
lang: Language
|
||||||
password: "New password"
|
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:
|
completions:
|
||||||
|
destroy:
|
||||||
|
notice: Completion deleted
|
||||||
edit:
|
edit:
|
||||||
title: "Edit completion"
|
notice: Completion updated
|
||||||
|
title: Edit completion
|
||||||
new:
|
new:
|
||||||
title: "New completion"
|
notice: Completion added
|
||||||
|
title: New completion
|
||||||
|
singular: completion
|
||||||
contests:
|
contests:
|
||||||
|
destroy:
|
||||||
|
notice: Contest deleted
|
||||||
edit:
|
edit:
|
||||||
title: "Edit contest settings"
|
notice: Contest updated
|
||||||
|
title: Edit contest settings
|
||||||
|
form:
|
||||||
|
categories: Participant categories
|
||||||
|
general: General parameters
|
||||||
index:
|
index:
|
||||||
title: "Welcome %{username}!"
|
title: Welcome %{username}!
|
||||||
manage_contests: "Manage my contests"
|
manage_contests: Manage my contests
|
||||||
new_contest: "Create a new contest"
|
new_contest: Create a new contest
|
||||||
new:
|
new:
|
||||||
title: "New jigsaw puzzle contest"
|
notice: Contest added
|
||||||
|
title: New jigsaw puzzle contest
|
||||||
scoreboard:
|
scoreboard:
|
||||||
|
refresh: Activate auto-refresh (every 5s)
|
||||||
title: "%{name}"
|
title: "%{name}"
|
||||||
show:
|
show:
|
||||||
title: "%{name}"
|
title: "%{name}"
|
||||||
add_participant: "Add contestant"
|
add_participant: Add participant
|
||||||
add_puzzle: "Add puzzle"
|
add_puzzle: Add puzzle
|
||||||
public_scoreboard: "Public scoreboard: "
|
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:
|
contestants:
|
||||||
|
convert_csv:
|
||||||
|
title: Import participants
|
||||||
|
destroy:
|
||||||
|
notice: Participant deleted
|
||||||
edit:
|
edit:
|
||||||
title: "Participant"
|
notice: Participant updated
|
||||||
team_title: "Teams"
|
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:
|
new:
|
||||||
title: "New participant"
|
notice: Participant added
|
||||||
team_title: "New team"
|
title: New participant
|
||||||
singular: "participant"
|
team_title: New team
|
||||||
plural: "participants"
|
singular: participant
|
||||||
|
plural: participants
|
||||||
teams:
|
teams:
|
||||||
singular: "team"
|
singular: team
|
||||||
plural: "teams"
|
plural: teams
|
||||||
|
upload_csv:
|
||||||
|
title: Import participants
|
||||||
helpers:
|
helpers:
|
||||||
|
badges:
|
||||||
|
registration: "registration"
|
||||||
|
team: "team"
|
||||||
buttons:
|
buttons:
|
||||||
|
add: "Add"
|
||||||
|
add_completion: "Add completion"
|
||||||
|
back: "⬅ Back to the contest"
|
||||||
|
back_to_contestant: "⬅ Back to the participant"
|
||||||
|
confirm: "Confirm"
|
||||||
create: "Create"
|
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:
|
nav:
|
||||||
users: "Users"
|
users: Users
|
||||||
home: "Home"
|
home: My contests
|
||||||
settings: "Settings"
|
settings: Settings
|
||||||
log_out: "Log out"
|
log_out: Log out
|
||||||
puzzles:
|
puzzles:
|
||||||
|
destroy:
|
||||||
|
notice: Puzzle deleted
|
||||||
edit:
|
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:
|
new:
|
||||||
title: "New contest puzzle"
|
notice: Puzzle added
|
||||||
singular: "puzzle"
|
title: New contest puzzle
|
||||||
plural: "puzzles"
|
singular: puzzle
|
||||||
|
plural: puzzles
|
||||||
sessions:
|
sessions:
|
||||||
new:
|
new:
|
||||||
|
notice: Login successful
|
||||||
title: "Login to the Public Scoreboard app"
|
title: "Login to the Public Scoreboard app"
|
||||||
users:
|
users:
|
||||||
edit:
|
edit:
|
||||||
|
notice: Settings updated
|
||||||
title: "My settings"
|
title: "My settings"
|
||||||
general_section: "General settings"
|
general_section: "General settings"
|
||||||
password_section: "Change password"
|
password_section: "Change password"
|
||||||
index:
|
index:
|
||||||
title: "All users"
|
title: "All users"
|
||||||
new:
|
new:
|
||||||
|
notice: User created
|
||||||
title: "New user"
|
title: "New user"
|
||||||
|
|||||||
@@ -1,75 +1,231 @@
|
|||||||
fr:
|
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:
|
activerecord:
|
||||||
attributes:
|
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:
|
contest:
|
||||||
name: "Nom"
|
lang: Langue pour le classement public
|
||||||
team: "Concours par équipes"
|
name: Nom
|
||||||
team_description: "Principalement pour des raisons d'affichage"
|
public: Activer le classement public
|
||||||
allow_registration: "Autoriser l'inscription via l'interface"
|
team: Concours par équipes
|
||||||
allow_registration_description: "Génère un formulaire d'inscription pour ce concours"
|
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:
|
user:
|
||||||
username: "Nom d'utilisateur.ice"
|
username: Nom d'utilisateur.ice
|
||||||
email_address: "Adresse email"
|
email_address: Adresse email
|
||||||
lang: "Langue de l'interface"
|
lang: Langue de l'interface
|
||||||
password: "Nouveau mot de passe"
|
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:
|
completions:
|
||||||
|
destroy:
|
||||||
|
notice: Complétion supprimée
|
||||||
edit:
|
edit:
|
||||||
title: "Modifier la complétion"
|
notice: Complétion modifiée
|
||||||
|
title: Modifier la complétion
|
||||||
new:
|
new:
|
||||||
title: "Nouvelle complétion"
|
notice: Complétion ajoutée
|
||||||
|
title: Ajout d'une complétion
|
||||||
|
singular: complétion
|
||||||
contests:
|
contests:
|
||||||
|
destroy:
|
||||||
|
notice: Concours supprimé
|
||||||
edit:
|
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:
|
index:
|
||||||
title: "Bienvenue %{username} !"
|
title: Bienvenue %{username} !
|
||||||
manage_contests: "Mes concours de puzzle"
|
manage_contests: Mes concours de puzzle
|
||||||
new_contest: "Créer un nouveau concours"
|
new_contest: Créer un nouveau concours
|
||||||
new:
|
new:
|
||||||
title: "Nouveau concours"
|
notice: Concours ajouté
|
||||||
|
title: Nouveau concours
|
||||||
scoreboard:
|
scoreboard:
|
||||||
|
refresh: Activer le rafraichissement automatique de la page (toutes les 5s)
|
||||||
title: "%{name}"
|
title: "%{name}"
|
||||||
show:
|
show:
|
||||||
title: "%{name}"
|
title: "%{name}"
|
||||||
add_participant: "Ajouter un.e participant.e"
|
add_participant: Ajouter un.e participant.e
|
||||||
add_puzzle: "Ajouter un puzzle"
|
add_puzzle: Ajouter un puzzle
|
||||||
public_scoreboard: "Classement public : "
|
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:
|
contestants:
|
||||||
|
convert_csv:
|
||||||
|
title: Importer des participant.e.s
|
||||||
|
destroy:
|
||||||
|
notice: Participant.e supprimé.e
|
||||||
edit:
|
edit:
|
||||||
title: "Participant.e"
|
notice: Participant.e modifié.e
|
||||||
team_title: "Équipe"
|
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:
|
new:
|
||||||
title: "Nouveau.elle participant.e"
|
notice: Participant.e ajouté.e
|
||||||
team_title: "Nouvelle équipe"
|
title: Nouveau.elle participant.e
|
||||||
singular: "participant.e"
|
team_title: Nouvelle équipe
|
||||||
plural: "participant.e.s"
|
singular: participant.e
|
||||||
|
plural: participant.e.s
|
||||||
teams:
|
teams:
|
||||||
singular: "équipe"
|
singular: équipe
|
||||||
plural: "équipes"
|
plural: équipes
|
||||||
|
upload_csv:
|
||||||
|
title: Importer des participant.e.s
|
||||||
helpers:
|
helpers:
|
||||||
|
badges:
|
||||||
|
registration: "auto-inscription"
|
||||||
|
team: "équipes"
|
||||||
buttons:
|
buttons:
|
||||||
|
add: "Ajouter"
|
||||||
|
add_completion: "Convertir"
|
||||||
|
back: "⬅ Revenir au concours"
|
||||||
|
back_to_contestant: "⬅ Revenir au/à la participant.e"
|
||||||
|
confirm: "Confirmer"
|
||||||
create: "Créer"
|
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:
|
nav:
|
||||||
users: "Utilisateur.ices"
|
users: Utilisateur.ices
|
||||||
home: "Accueil"
|
home: Mes concours
|
||||||
settings: "Paramètres"
|
settings: Paramètres
|
||||||
log_out: "Déconnexion"
|
log_out: Déconnexion
|
||||||
puzzles:
|
puzzles:
|
||||||
|
destroy:
|
||||||
|
notice: Puzzle supprimé
|
||||||
edit:
|
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:
|
new:
|
||||||
title: "Nouveau puzzle"
|
notice: Puzzle ajouté
|
||||||
singular: "puzzle"
|
title: Nouveau puzzle
|
||||||
plural: "puzzles"
|
singular: puzzle
|
||||||
|
plural: puzzles
|
||||||
sessions:
|
sessions:
|
||||||
new:
|
new:
|
||||||
|
notice: Connection réussie
|
||||||
title: "Se connecter à l'app Public Scoreboard"
|
title: "Se connecter à l'app Public Scoreboard"
|
||||||
users:
|
users:
|
||||||
edit:
|
edit:
|
||||||
|
notice: Paramètres modifiés
|
||||||
title: "Mes paramètres"
|
title: "Mes paramètres"
|
||||||
general_section: "Paramètres globaux"
|
general_section: "Paramètres globaux"
|
||||||
password_section: "Modifier mon mot de passe"
|
password_section: "Modifier mon mot de passe"
|
||||||
index:
|
index:
|
||||||
title: "Tous.tes les utilisateur.ices"
|
title: "Tous.tes les utilisateur.ices"
|
||||||
new:
|
new:
|
||||||
|
notice: Utilisateur.ice ajouté.e
|
||||||
title: "Nouveau.elle utilisateur.ice"
|
title: "Nouveau.elle utilisateur.ice"
|
||||||
|
|||||||
@@ -9,14 +9,26 @@ Rails.application.routes.draw do
|
|||||||
root "contests#index"
|
root "contests#index"
|
||||||
|
|
||||||
resources :contests do
|
resources :contests do
|
||||||
|
resources :categories, only: [ :create, :destroy ]
|
||||||
resources :completions
|
resources :completions
|
||||||
resources :contestants
|
resources :contestants
|
||||||
resources :puzzles
|
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
|
end
|
||||||
resources :passwords, param: :token
|
resources :passwords, param: :token
|
||||||
resource :session
|
resource :session
|
||||||
resources :users
|
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"
|
post "message", to: "messages#create"
|
||||||
|
|
||||||
get "public/:id", to: "contests#scoreboard"
|
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.
|
# 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|
|
create_table "active_storage_attachments", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "record_type", 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
|
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
|
||||||
end
|
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|
|
create_table "completions", force: :cascade do |t|
|
||||||
t.integer "time_seconds"
|
t.integer "time_seconds"
|
||||||
t.integer "contestant_id", null: false
|
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.integer "contest_id", null: false
|
||||||
t.string "display_time_from_start"
|
t.string "display_time_from_start"
|
||||||
t.string "display_relative_time"
|
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 ["contest_id"], name: "index_completions_on_contest_id"
|
||||||
t.index ["contestant_id"], name: "index_completions_on_contestant_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"
|
t.index ["puzzle_id"], name: "index_completions_on_puzzle_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -60,6 +78,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_11_173749) do
|
|||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.string "display_time"
|
t.string "display_time"
|
||||||
|
t.integer "time_seconds"
|
||||||
t.index ["contest_id"], name: "index_contestants_on_contest_id"
|
t.index ["contest_id"], name: "index_contestants_on_contest_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -71,10 +90,19 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_11_173749) do
|
|||||||
t.boolean "team", default: false
|
t.boolean "team", default: false
|
||||||
t.boolean "allow_registration", default: false
|
t.boolean "allow_registration", default: false
|
||||||
t.string "slug"
|
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 ["slug"], name: "index_contests_on_slug", unique: true
|
||||||
t.index ["user_id"], name: "index_contests_on_user_id"
|
t.index ["user_id"], name: "index_contests_on_user_id"
|
||||||
end
|
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|
|
create_table "friendly_id_slugs", force: :cascade do |t|
|
||||||
t.string "slug", null: false
|
t.string "slug", null: false
|
||||||
t.integer "sluggable_id", 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.string "text", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_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"
|
t.index ["contest_id"], name: "index_messages_on_contest_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -101,6 +131,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_11_173749) do
|
|||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.integer "contest_id", null: false
|
t.integer "contest_id", null: false
|
||||||
t.string "brand"
|
t.string "brand"
|
||||||
|
t.integer "pieces", null: false
|
||||||
t.index ["contest_id"], name: "index_puzzles_on_contest_id"
|
t.index ["contest_id"], name: "index_puzzles_on_contest_id"
|
||||||
end
|
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_attachments", "active_storage_blobs", column: "blob_id"
|
||||||
add_foreign_key "active_storage_variant_records", "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", "contestants"
|
||||||
add_foreign_key "completions", "contests"
|
add_foreign_key "completions", "contests"
|
||||||
|
add_foreign_key "completions", "messages"
|
||||||
add_foreign_key "completions", "puzzles"
|
add_foreign_key "completions", "puzzles"
|
||||||
add_foreign_key "contestants", "contests"
|
add_foreign_key "contestants", "contests"
|
||||||
add_foreign_key "contests", "users"
|
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
|
# id :integer not null, primary key
|
||||||
# allow_registration :boolean default(FALSE)
|
# allow_registration :boolean default(FALSE)
|
||||||
|
# lang :string default("en")
|
||||||
# name :string
|
# name :string
|
||||||
|
# public :boolean default(FALSE)
|
||||||
# slug :string
|
# slug :string
|
||||||
# team :boolean default(FALSE)
|
# team :boolean default(FALSE)
|
||||||
# created_at :datetime not null
|
# 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
|
# Table name: messages
|
||||||
#
|
#
|
||||||
# id :integer not null, primary key
|
# id :integer not null, primary key
|
||||||
|
# author :string
|
||||||
|
# display_time :string
|
||||||
# text :string not null
|
# text :string not null
|
||||||
# time_seconds :integer not null
|
# time_seconds :integer not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
|
|||||||
@@ -1,17 +1,78 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.feature "Contests", type: :feature do
|
RSpec.feature "Contests", type: :feature do
|
||||||
context "visiting the home page" do
|
let!(:user) { create(:user) }
|
||||||
let!(:user) { create(:user) }
|
|
||||||
|
|
||||||
before do
|
before do
|
||||||
login(user)
|
login(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "index" do
|
||||||
|
let!(:first_contest) { create(:contest, user: user) }
|
||||||
|
let!(:second_contest) { create(:contest, user: user) }
|
||||||
|
|
||||||
|
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
|
end
|
||||||
|
|
||||||
it "should display the username" do
|
it "should offer to create a new contest" do
|
||||||
visit root_path
|
visit contests_path
|
||||||
|
|
||||||
expect(page).to have_content(user.username)
|
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
|
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
|
# Table name: messages
|
||||||
#
|
#
|
||||||
# id :integer not null, primary key
|
# id :integer not null, primary key
|
||||||
|
# author :string
|
||||||
|
# display_time :string
|
||||||
# text :string not null
|
# text :string not null
|
||||||
# time_seconds :integer not null
|
# time_seconds :integer not null
|
||||||
# created_at :datetime 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