Compare commits
72 Commits
c98caeea92
...
main
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
6afde8a971 | |||
70005468c6 | |||
2f23938e81 | |||
378c3011ef | |||
a421cd496d | |||
21f71f9d32 | |||
10fa821f19 | |||
8b0b1c6745 | |||
497768610d | |||
26b8064553 | |||
7023600cd1 | |||
12f9f33034 | |||
2144c22bd9 | |||
a5d165c4b3 |
@@ -69,16 +69,12 @@ jobs:
|
||||
ruby-version: .ruby-version
|
||||
bundler-cache: true
|
||||
|
||||
- name: Run tests
|
||||
- name: Setup test database
|
||||
env:
|
||||
RAILS_ENV: test
|
||||
# REDIS_URL: redis://localhost:6379/0
|
||||
run: bin/rails db:test:prepare test test:system
|
||||
run: bin/rails db:test:prepare
|
||||
|
||||
- name: Keep screenshots from failed system tests
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: screenshots
|
||||
path: ${{ github.workspace }}/tmp/screenshots
|
||||
if-no-files-found: ignore
|
||||
- name: Run rspec
|
||||
env:
|
||||
RAILS_ENV: test
|
||||
run: bundle exec rspec
|
||||
|
@@ -48,9 +48,6 @@ RUN bundle exec bootsnap precompile app/ lib/
|
||||
# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
|
||||
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
|
||||
|
||||
|
||||
|
||||
|
||||
# Final stage for app image
|
||||
FROM base
|
||||
|
||||
@@ -58,6 +55,9 @@ FROM base
|
||||
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
|
||||
COPY --from=build /rails /rails
|
||||
|
||||
# TODO: find how not to depend on this hack to include the compiled SCSS.
|
||||
RUN cp app/assets/builds/application.css `ls public/assets/application-*.css`
|
||||
|
||||
# Run and own only the runtime files as a non-root user for security
|
||||
RUN groupadd --system --gid 1000 rails && \
|
||||
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
|
||||
|
7
Gemfile
7
Gemfile
@@ -43,6 +43,9 @@ gem "thruster", require: false
|
||||
gem "slim"
|
||||
gem "dartsass-rails"
|
||||
gem "bootstrap", "~> 5.3.3"
|
||||
gem "friendly_id", "~> 5.5.0"
|
||||
gem "csv"
|
||||
gem "damerau-levenshtein"
|
||||
|
||||
group :development, :test do
|
||||
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
|
||||
@@ -53,6 +56,10 @@ group :development, :test do
|
||||
|
||||
# Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
|
||||
gem "rubocop-rails-omakase", require: false
|
||||
|
||||
gem "rspec-rails"
|
||||
gem "factory_bot_rails"
|
||||
gem "faker"
|
||||
end
|
||||
|
||||
group :development do
|
||||
|
211
Gemfile.lock
211
Gemfile.lock
@@ -74,22 +74,21 @@ GEM
|
||||
uri (>= 0.13.1)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
annotaterb (4.14.0)
|
||||
ast (2.4.2)
|
||||
autoprefixer-rails (10.4.19.0)
|
||||
execjs (~> 2)
|
||||
base64 (0.2.0)
|
||||
annotaterb (4.16.0)
|
||||
activerecord (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
ast (2.4.3)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.20)
|
||||
bcrypt_pbkdf (1.1.1)
|
||||
benchmark (0.4.0)
|
||||
bigdecimal (3.1.9)
|
||||
benchmark (0.4.1)
|
||||
bigdecimal (3.2.2)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.18.4)
|
||||
bootsnap (1.18.6)
|
||||
msgpack (~> 1.2)
|
||||
bootstrap (5.3.3)
|
||||
autoprefixer-rails (>= 9.1.0)
|
||||
bootstrap (5.3.5)
|
||||
popper_js (>= 2.11.8, < 3)
|
||||
brakeman (7.0.0)
|
||||
brakeman (7.0.2)
|
||||
racc
|
||||
builder (3.3.0)
|
||||
capybara (3.40.0)
|
||||
@@ -102,34 +101,52 @@ GEM
|
||||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.0)
|
||||
connection_pool (2.5.3)
|
||||
crass (1.0.6)
|
||||
csv (3.3.5)
|
||||
damerau-levenshtein (1.3.3)
|
||||
dartsass-rails (0.5.1)
|
||||
railties (>= 6.0.0)
|
||||
sass-embedded (~> 1.63)
|
||||
date (3.4.1)
|
||||
debug (1.10.0)
|
||||
debug (1.11.0)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
dotenv (3.1.7)
|
||||
drb (2.2.1)
|
||||
ed25519 (1.3.0)
|
||||
diff-lcs (1.6.2)
|
||||
dotenv (3.1.8)
|
||||
drb (2.2.3)
|
||||
ed25519 (1.4.0)
|
||||
erb (5.0.1)
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
execjs (2.10.0)
|
||||
factory_bot (6.5.4)
|
||||
activesupport (>= 6.1.0)
|
||||
factory_bot_rails (6.5.0)
|
||||
factory_bot (~> 6.5)
|
||||
railties (>= 6.1.0)
|
||||
faker (3.5.1)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
friendly_id (5.5.1)
|
||||
activerecord (>= 4.0.0)
|
||||
fugit (1.11.1)
|
||||
et-orbi (~> 1, >= 1.2.11)
|
||||
raabro (~> 1.4)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
google-protobuf (4.30.0)
|
||||
google-protobuf (4.31.1)
|
||||
bigdecimal
|
||||
rake (>= 13)
|
||||
google-protobuf (4.30.0-aarch64-linux)
|
||||
google-protobuf (4.31.1-aarch64-linux-gnu)
|
||||
bigdecimal
|
||||
rake (>= 13)
|
||||
google-protobuf (4.30.0-x86_64-linux)
|
||||
google-protobuf (4.31.1-aarch64-linux-musl)
|
||||
bigdecimal
|
||||
rake (>= 13)
|
||||
google-protobuf (4.31.1-x86_64-linux-gnu)
|
||||
bigdecimal
|
||||
rake (>= 13)
|
||||
google-protobuf (4.31.1-x86_64-linux-musl)
|
||||
bigdecimal
|
||||
rake (>= 13)
|
||||
i18n (1.14.7)
|
||||
@@ -139,29 +156,29 @@ GEM
|
||||
activesupport (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
io-console (0.8.0)
|
||||
irb (1.15.1)
|
||||
irb (1.15.2)
|
||||
pp (>= 0.6.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jbuilder (2.13.0)
|
||||
actionview (>= 5.0.0)
|
||||
activesupport (>= 5.0.0)
|
||||
json (2.10.2)
|
||||
kamal (2.5.3)
|
||||
json (2.12.2)
|
||||
kamal (2.7.0)
|
||||
activesupport (>= 7.0)
|
||||
base64 (~> 0.2)
|
||||
bcrypt_pbkdf (~> 1.0)
|
||||
concurrent-ruby (~> 1.2)
|
||||
dotenv (~> 3.1)
|
||||
ed25519 (~> 1.2)
|
||||
ed25519 (~> 1.4)
|
||||
net-ssh (~> 7.3)
|
||||
sshkit (>= 1.23.0, < 2.0)
|
||||
thor (~> 1.3)
|
||||
zeitwerk (>= 2.6.18, < 3.0)
|
||||
language_server-protocol (3.17.0.4)
|
||||
language_server-protocol (3.17.0.5)
|
||||
lint_roller (1.1.0)
|
||||
logger (1.6.6)
|
||||
loofah (2.24.0)
|
||||
logger (1.7.0)
|
||||
loofah (2.24.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
@@ -170,11 +187,11 @@ GEM
|
||||
net-pop
|
||||
net-smtp
|
||||
marcel (1.0.4)
|
||||
matrix (0.4.2)
|
||||
matrix (0.4.3)
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.25.4)
|
||||
minitest (5.25.5)
|
||||
msgpack (1.8.0)
|
||||
net-imap (0.5.6)
|
||||
net-imap (0.5.9)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -189,44 +206,45 @@ GEM
|
||||
net-protocol
|
||||
net-ssh (7.3.0)
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.18.3-aarch64-linux-gnu)
|
||||
nokogiri (1.18.8-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.3-aarch64-linux-musl)
|
||||
nokogiri (1.18.8-aarch64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.3-arm-linux-gnu)
|
||||
nokogiri (1.18.8-arm-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.3-arm-linux-musl)
|
||||
nokogiri (1.18.8-arm-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.3-x86_64-linux-gnu)
|
||||
nokogiri (1.18.8-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.3-x86_64-linux-musl)
|
||||
nokogiri (1.18.8-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
ostruct (0.6.1)
|
||||
parallel (1.26.3)
|
||||
parser (3.3.7.1)
|
||||
ostruct (0.6.2)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.8.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
popper_js (2.11.8)
|
||||
pp (0.6.2)
|
||||
prettyprint
|
||||
prettyprint (0.2.0)
|
||||
prism (1.4.0)
|
||||
propshaft (1.1.0)
|
||||
actionpack (>= 7.0.0)
|
||||
activesupport (>= 7.0.0)
|
||||
rack
|
||||
railties (>= 7.0.0)
|
||||
psych (5.2.3)
|
||||
psych (5.2.6)
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.1)
|
||||
public_suffix (6.0.2)
|
||||
puma (6.6.0)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.5.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (3.1.12)
|
||||
rack-session (2.1.0)
|
||||
rack (3.1.16)
|
||||
rack-session (2.1.1)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.2.0)
|
||||
@@ -247,7 +265,7 @@ GEM
|
||||
activesupport (= 8.0.2)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 8.0.2)
|
||||
rails-dom-testing (2.2.0)
|
||||
rails-dom-testing (2.3.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
@@ -263,14 +281,32 @@ GEM
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.2.1)
|
||||
rdoc (6.12.0)
|
||||
rake (13.3.0)
|
||||
rdoc (6.14.1)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
regexp_parser (2.10.0)
|
||||
reline (0.6.0)
|
||||
reline (0.6.1)
|
||||
io-console (~> 0.5)
|
||||
rexml (3.4.1)
|
||||
rubocop (1.73.2)
|
||||
rspec-core (3.13.4)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.5)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.5)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (8.0.1)
|
||||
actionpack (>= 7.2)
|
||||
activesupport (>= 7.2)
|
||||
railties (>= 7.2)
|
||||
rspec-core (~> 3.13)
|
||||
rspec-expectations (~> 3.13)
|
||||
rspec-mocks (~> 3.13)
|
||||
rspec-support (~> 3.13)
|
||||
rspec-support (3.13.4)
|
||||
rubocop (1.77.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
@@ -278,41 +314,42 @@ GEM
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
rubocop-ast (>= 1.45.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.38.1)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-performance (1.24.0)
|
||||
rubocop-ast (1.45.1)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.4)
|
||||
rubocop-performance (1.25.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (>= 1.72.1, < 2.0)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
rubocop-rails (2.30.3)
|
||||
rubocop-rails (2.32.0)
|
||||
activesupport (>= 4.2.0)
|
||||
lint_roller (~> 1.1)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.72.1, < 2.0)
|
||||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
rubocop-rails-omakase (1.1.0)
|
||||
rubocop (>= 1.72)
|
||||
rubocop-performance (>= 1.24)
|
||||
rubocop-rails (>= 2.30)
|
||||
ruby-progressbar (1.13.0)
|
||||
rubyzip (2.4.1)
|
||||
sass-embedded (1.85.1-aarch64-linux-gnu)
|
||||
google-protobuf (~> 4.29)
|
||||
sass-embedded (1.85.1-aarch64-linux-musl)
|
||||
google-protobuf (~> 4.29)
|
||||
sass-embedded (1.85.1-arm-linux-gnueabihf)
|
||||
google-protobuf (~> 4.29)
|
||||
sass-embedded (1.85.1-arm-linux-musleabihf)
|
||||
google-protobuf (~> 4.29)
|
||||
sass-embedded (1.85.1-x86_64-linux-gnu)
|
||||
google-protobuf (~> 4.29)
|
||||
sass-embedded (1.85.1-x86_64-linux-musl)
|
||||
google-protobuf (~> 4.29)
|
||||
sass-embedded (1.89.2-aarch64-linux-gnu)
|
||||
google-protobuf (~> 4.31)
|
||||
sass-embedded (1.89.2-aarch64-linux-musl)
|
||||
google-protobuf (~> 4.31)
|
||||
sass-embedded (1.89.2-arm-linux-gnueabihf)
|
||||
google-protobuf (~> 4.31)
|
||||
sass-embedded (1.89.2-arm-linux-musleabihf)
|
||||
google-protobuf (~> 4.31)
|
||||
sass-embedded (1.89.2-x86_64-linux-gnu)
|
||||
google-protobuf (~> 4.31)
|
||||
sass-embedded (1.89.2-x86_64-linux-musl)
|
||||
google-protobuf (~> 4.31)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.29.1)
|
||||
selenium-webdriver (4.33.0)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
@@ -321,7 +358,7 @@ GEM
|
||||
slim (5.2.1)
|
||||
temple (~> 0.10.0)
|
||||
tilt (>= 2.1.0)
|
||||
solid_cable (3.0.7)
|
||||
solid_cable (3.0.10)
|
||||
actioncable (>= 7.2)
|
||||
activejob (>= 7.2)
|
||||
activerecord (>= 7.2)
|
||||
@@ -330,19 +367,19 @@ GEM
|
||||
activejob (>= 7.2)
|
||||
activerecord (>= 7.2)
|
||||
railties (>= 7.2)
|
||||
solid_queue (1.1.3)
|
||||
solid_queue (1.1.5)
|
||||
activejob (>= 7.1)
|
||||
activerecord (>= 7.1)
|
||||
concurrent-ruby (>= 1.3.1)
|
||||
fugit (~> 1.11.0)
|
||||
railties (>= 7.1)
|
||||
thor (~> 1.3.1)
|
||||
sqlite3 (2.6.0-aarch64-linux-gnu)
|
||||
sqlite3 (2.6.0-aarch64-linux-musl)
|
||||
sqlite3 (2.6.0-arm-linux-gnu)
|
||||
sqlite3 (2.6.0-arm-linux-musl)
|
||||
sqlite3 (2.6.0-x86_64-linux-gnu)
|
||||
sqlite3 (2.6.0-x86_64-linux-musl)
|
||||
sqlite3 (2.7.0-aarch64-linux-gnu)
|
||||
sqlite3 (2.7.0-aarch64-linux-musl)
|
||||
sqlite3 (2.7.0-arm-linux-gnu)
|
||||
sqlite3 (2.7.0-arm-linux-musl)
|
||||
sqlite3 (2.7.0-x86_64-linux-gnu)
|
||||
sqlite3 (2.7.0-x86_64-linux-musl)
|
||||
sshkit (1.24.0)
|
||||
base64
|
||||
logger
|
||||
@@ -352,15 +389,15 @@ GEM
|
||||
ostruct
|
||||
stimulus-rails (1.3.4)
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.1.5)
|
||||
stringio (3.1.7)
|
||||
temple (0.10.3)
|
||||
thor (1.3.2)
|
||||
thruster (0.1.12)
|
||||
thruster (0.1.12-aarch64-linux)
|
||||
thruster (0.1.12-x86_64-linux)
|
||||
thruster (0.1.14)
|
||||
thruster (0.1.14-aarch64-linux)
|
||||
thruster (0.1.14-x86_64-linux)
|
||||
tilt (2.6.0)
|
||||
timeout (0.4.3)
|
||||
turbo-rails (2.0.13)
|
||||
turbo-rails (2.0.16)
|
||||
actionpack (>= 7.1.0)
|
||||
railties (>= 7.1.0)
|
||||
tzinfo (2.0.6)
|
||||
@@ -376,13 +413,13 @@ GEM
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
websocket (1.2.11)
|
||||
websocket-driver (0.7.7)
|
||||
websocket-driver (0.8.0)
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.7.2)
|
||||
zeitwerk (2.7.3)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
@@ -401,8 +438,13 @@ DEPENDENCIES
|
||||
bootstrap (~> 5.3.3)
|
||||
brakeman
|
||||
capybara
|
||||
csv
|
||||
damerau-levenshtein
|
||||
dartsass-rails
|
||||
debug
|
||||
factory_bot_rails
|
||||
faker
|
||||
friendly_id (~> 5.5.0)
|
||||
importmap-rails
|
||||
jbuilder
|
||||
kamal
|
||||
@@ -410,6 +452,7 @@ DEPENDENCIES
|
||||
puma (>= 5.0)
|
||||
pundit (~> 2.5)
|
||||
rails (~> 8.0.2)
|
||||
rspec-rails
|
||||
rubocop-rails-omakase
|
||||
selenium-webdriver
|
||||
slim
|
||||
|
41
README.md
41
README.md
@@ -3,3 +3,44 @@
|
||||
## Dependencies
|
||||
|
||||
Rails installation guide: https://guides.rubyonrails.org/install_ruby_on_rails.html.
|
||||
|
||||
## Put in production
|
||||
|
||||
### Create a master key
|
||||
|
||||
```
|
||||
bin/rails credentials:edit
|
||||
```
|
||||
|
||||
### Build docker image
|
||||
|
||||
```
|
||||
docker build -t puzzle_scoreboard .
|
||||
```
|
||||
|
||||
### Run docker container
|
||||
|
||||
```
|
||||
sudo docker run -d -p 3000:80 -e RAILS_MASTER_KEY=... -v puzzle-data:/rails/storage --name puzzle_scoreboard puzzle_scoreboard
|
||||
```
|
||||
|
||||
### Access command line
|
||||
|
||||
```
|
||||
sudo docker exec -it puzzle_scoreboard /bin/bash
|
||||
sudo docker exec -it puzzle_scoreboard /rails/bin/rails console
|
||||
```
|
||||
|
||||
### Logs
|
||||
|
||||
Attach to the docker container to see live logs:
|
||||
|
||||
```
|
||||
sudo docker container attach puzzle_scoreboard
|
||||
```
|
||||
|
||||
Or look at the logs saved on the host machine:
|
||||
|
||||
```
|
||||
sudo docker logs puzzle_scoreboard
|
||||
```
|
@@ -2,7 +2,7 @@ class ApplicationController < ActionController::Base
|
||||
include Authentication
|
||||
include Pundit::Authorization
|
||||
|
||||
before_action :set_title, :set_current_user
|
||||
before_action :set_title, :set_current_user, :set_lang
|
||||
after_action :verify_authorized
|
||||
|
||||
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
||||
@@ -13,13 +13,20 @@ class ApplicationController < ActionController::Base
|
||||
private
|
||||
|
||||
def set_title
|
||||
@title = "Public scoreboard"
|
||||
t_action_name = action_name
|
||||
t_action_name = "new" if action_name == "create"
|
||||
t_action_name = "edit" if action_name == "update"
|
||||
@title = I18n.t("#{controller_name}.#{t_action_name}.title")
|
||||
end
|
||||
|
||||
def set_current_user
|
||||
@current_user = current_user
|
||||
end
|
||||
|
||||
def set_lang
|
||||
I18n.locale = @current_user.lang if @current_user
|
||||
end
|
||||
|
||||
def user_not_authorized(exception)
|
||||
policy_name = exception.policy.class.to_s.underscore
|
||||
|
||||
|
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
|
@@ -1,22 +1,32 @@
|
||||
class CompletionsController < ApplicationController
|
||||
include CompletionsConcern
|
||||
|
||||
before_action :set_contest
|
||||
before_action :set_contestant
|
||||
before_action :set_data, only: %i[ create edit new update ]
|
||||
before_action :set_completion, only: %i[ destroy edit update ]
|
||||
|
||||
def edit
|
||||
authorize @contest
|
||||
|
||||
@title = "Edit completion"
|
||||
if @contestant
|
||||
@action_name = t("helpers.buttons.back_to_contestant")
|
||||
@action_path = edit_contest_contestant_path(@contest, @contestant)
|
||||
end
|
||||
end
|
||||
|
||||
def new
|
||||
authorize @contest
|
||||
|
||||
if @contestant
|
||||
@action_name = t("helpers.buttons.back_to_contestant")
|
||||
@action_path = edit_contest_contestant_path(@contest, @contestant)
|
||||
end
|
||||
|
||||
@completion = Completion.new
|
||||
if params[:contestant_id]
|
||||
@completion.contestant_id = params[:contestant_id]
|
||||
end
|
||||
@title = "New completion"
|
||||
end
|
||||
|
||||
def create
|
||||
@@ -25,11 +35,21 @@ class CompletionsController < ApplicationController
|
||||
@completion = Completion.new(completion_params)
|
||||
@completion.contest_id = @contest.id
|
||||
if @completion.save
|
||||
redirect_to contest_path(@contest)
|
||||
extend_completions!(@completion.contestant)
|
||||
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
|
||||
logger = Logger.new(STDOUT)
|
||||
logger.info(@completion.errors)
|
||||
@title = "New completion"
|
||||
if params[:completion].key?(:message_id)
|
||||
@message = Message.find(params[:completion][:message_id])
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
elsif @contestant
|
||||
@action_name = t("helpers.buttons.back_to_contestant")
|
||||
@action_path = edit_contest_contestant_path(@contest, @contestant)
|
||||
end
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
@@ -37,13 +57,19 @@ class CompletionsController < ApplicationController
|
||||
def update
|
||||
authorize @contest
|
||||
|
||||
if params[:contestant_id]
|
||||
@completion.contestant_id = params[:contestant_id]
|
||||
end
|
||||
@completion.contestant_id = params[:contestant_id] if params[:contestant_id]
|
||||
if @completion.update(completion_params)
|
||||
redirect_to @contest
|
||||
extend_completions!(@completion.contestant)
|
||||
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
|
||||
@title = "Edit completion"
|
||||
if @contestant
|
||||
@action_name = t("helpers.buttons.back_to_contestant")
|
||||
@action_path = edit_contest_contestant_path(@contest, @contestant)
|
||||
end
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
@@ -53,9 +79,9 @@ class CompletionsController < ApplicationController
|
||||
|
||||
@completion.destroy
|
||||
if params[:contestant_id]
|
||||
redirect_to contest_contestant_path(@contest, params[:contestant_id])
|
||||
redirect_to contest_contestant_path(@contest, params[:contestant_id]), notice: t("completions.destroy.notice")
|
||||
else
|
||||
redirect_to contest_path(@contest)
|
||||
redirect_to contest_path(@contest), notice: t("completions.destroy.notice")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -65,8 +91,16 @@ class CompletionsController < ApplicationController
|
||||
@contest = Contest.find(params[:contest_id])
|
||||
end
|
||||
|
||||
def set_contestant
|
||||
if params.key?(:contestant_id)
|
||||
@contestant = Contestant.find(params[:contestant_id])
|
||||
elsif params[:completion].key?(:contestant_id)
|
||||
@contestant = Contestant.find(params[:completion][:contestant_id])
|
||||
end
|
||||
end
|
||||
|
||||
def set_data
|
||||
@contestants = @contest.contestants
|
||||
@contestants = @contest.contestants.order(:name)
|
||||
@puzzles = @contest.puzzles
|
||||
end
|
||||
|
||||
@@ -75,6 +109,6 @@ class CompletionsController < ApplicationController
|
||||
end
|
||||
|
||||
def completion_params
|
||||
params.expect(completion: [ :time_seconds, :contestant_id, :puzzle_id ])
|
||||
params.expect(completion: [ :display_time_from_start, :contestant_id, :message_id, :puzzle_id ])
|
||||
end
|
||||
end
|
||||
|
@@ -8,23 +8,25 @@ module CompletionsConcern
|
||||
"0" + n.to_s
|
||||
end
|
||||
|
||||
def display_time(seconds)
|
||||
if seconds > 3600
|
||||
hours = seconds / 3600
|
||||
return hours.to_s + ":" + display_time(seconds % 3600)
|
||||
elsif seconds > 60
|
||||
minutes = seconds / 60
|
||||
return pad(minutes) + ":" + display_time(seconds % 60)
|
||||
def display_time(time)
|
||||
h = time / 3600
|
||||
m = (time % 3600) / 60
|
||||
s = (time % 3600) % 60
|
||||
if h > 0
|
||||
return h.to_s + ":" + pad(m) + ":" + pad(s)
|
||||
elsif m > 0
|
||||
return m.to_s + ":" + pad(s)
|
||||
end
|
||||
pad(seconds)
|
||||
s.to_s
|
||||
end
|
||||
|
||||
def extend_completions!(completions)
|
||||
def extend_completions!(contestant)
|
||||
current_time_from_start = 0
|
||||
@completions.each do |completion|
|
||||
completion.display_time_from_start = display_time(completion.time_seconds)
|
||||
completion.display_relative_time = display_time(completion.time_seconds - current_time_from_start)
|
||||
current_time_from_start += completion.time_seconds
|
||||
contestant.completions.order(:time_seconds).each do |completion|
|
||||
completion.update(display_time_from_start: display_time(completion.time_seconds),
|
||||
display_relative_time: display_time(completion.time_seconds - current_time_from_start))
|
||||
current_time_from_start = completion.time_seconds
|
||||
end
|
||||
contestant.update(display_time: display_time(current_time_from_start), time_seconds: current_time_from_start)
|
||||
end
|
||||
end
|
||||
|
@@ -1,6 +1,4 @@
|
||||
class ContestantsController < ApplicationController
|
||||
include CompletionsConcern
|
||||
|
||||
before_action :set_contest
|
||||
before_action :set_contestant, only: %i[ destroy edit update]
|
||||
before_action :set_completions, only: %i[edit update ]
|
||||
@@ -8,14 +6,16 @@ class ContestantsController < ApplicationController
|
||||
def edit
|
||||
authorize @contest
|
||||
|
||||
@title = "Contestant"
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
end
|
||||
|
||||
def new
|
||||
authorize @contest
|
||||
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
@contestant = Contestant.new
|
||||
@title = "New contestant"
|
||||
end
|
||||
|
||||
def create
|
||||
@@ -24,9 +24,11 @@ class ContestantsController < ApplicationController
|
||||
@contestant = Contestant.new(contestant_params)
|
||||
@contestant.contest_id = @contest.id
|
||||
if @contestant.save
|
||||
redirect_to contest_path(@contest)
|
||||
update_contestant_categories
|
||||
redirect_to contest_path(@contest), notice: t("contestants.new.notice")
|
||||
else
|
||||
@title = "New contestant"
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
@@ -35,9 +37,11 @@ class ContestantsController < ApplicationController
|
||||
authorize @contest
|
||||
|
||||
if @contestant.update(contestant_params)
|
||||
redirect_to @contest
|
||||
update_contestant_categories
|
||||
redirect_to @contest, notice: t("contestants.edit.notice")
|
||||
else
|
||||
@title = "Contestant"
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
@@ -46,7 +50,65 @@ class ContestantsController < ApplicationController
|
||||
authorize @contest
|
||||
|
||||
@contestant.destroy
|
||||
redirect_to contest_path(@contest)
|
||||
redirect_to contest_path(@contest), notice: t("contestants.destroy.notice")
|
||||
end
|
||||
|
||||
def import
|
||||
authorize @contest
|
||||
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
|
||||
@csv_import = CsvImport.new
|
||||
end
|
||||
|
||||
def upload_csv
|
||||
authorize @contest
|
||||
|
||||
@csv_import = CsvImport.new(params.require(:csv_import).permit(:file, :separator))
|
||||
if @csv_import.save
|
||||
redirect_to "/contests/#{@contest.id}/import/#{@csv_import.id}"
|
||||
else
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
render :import, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def convert_csv
|
||||
authorize @contest
|
||||
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
@csv_import = CsvImport.find(params[:id])
|
||||
@content = JSON.parse(@csv_import.content)
|
||||
@form = Forms::CsvConversionForm.new
|
||||
end
|
||||
|
||||
def finalize_import
|
||||
authorize @contest
|
||||
|
||||
@csv_import = CsvImport.find(params[:id])
|
||||
@content = JSON.parse(@csv_import.content)
|
||||
all_params = params.require(:forms_csv_conversion_form)
|
||||
@form = Forms::CsvConversionForm.new(params.require(:forms_csv_conversion_form).permit(:email_column, :name_column))
|
||||
if @form.valid?
|
||||
@content.each_with_index do |row, i|
|
||||
if all_params["row_#{i}".to_sym] == "1"
|
||||
if @form.email_column == -1
|
||||
Contestant.create(name: row[@form.name_column], contest: @contest)
|
||||
else
|
||||
logger.info("Email")
|
||||
Contestant.create(name: row[@form.name_column], email: row[@form.email_column], contest: @contest)
|
||||
end
|
||||
end
|
||||
end
|
||||
redirect_to contest_path(@contest), notice: t("contestants.import.notice")
|
||||
else
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
render :convert_csv, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
@@ -61,11 +123,20 @@ class ContestantsController < ApplicationController
|
||||
|
||||
def set_completions
|
||||
@completions = @contestant.completions.order(:time_seconds)
|
||||
extend_completions!(@completions)
|
||||
@completions
|
||||
end
|
||||
|
||||
def contestant_params
|
||||
params.expect(contestant: [ :email, :name ])
|
||||
end
|
||||
|
||||
def update_contestant_categories
|
||||
@contestant.categories.clear
|
||||
@contest.categories.each do |category|
|
||||
logger.info(params[:contestant]["category_#{category.id}".to_sym] == "1")
|
||||
if params[:contestant].key?("category_#{category.id}".to_sym) && params[:contestant]["category_#{category.id}".to_sym] == "1"
|
||||
@contestant.categories << category
|
||||
end
|
||||
end
|
||||
@contestant.save
|
||||
end
|
||||
end
|
||||
|
@@ -1,6 +1,4 @@
|
||||
class ContestsController < ApplicationController
|
||||
include CompletionsConcern
|
||||
|
||||
before_action :set_contest, only: %i[ destroy edit show update ]
|
||||
skip_before_action :require_authentication, only: %i[ scoreboard ]
|
||||
|
||||
@@ -8,29 +6,33 @@ class ContestsController < ApplicationController
|
||||
authorize :contest
|
||||
|
||||
@contests = current_user.contests
|
||||
@title = "Welcome #{current_user.username}!"
|
||||
@title = I18n.t("contests.index.title", username: current_user.username)
|
||||
end
|
||||
|
||||
def show
|
||||
authorize @contest
|
||||
|
||||
@title = @contest.name
|
||||
@contestants = @contest.contestants.order(:name)
|
||||
@title = I18n.t("contests.show.title", name: @contest.name)
|
||||
@action_name = t("helpers.buttons.edit")
|
||||
@action_path = edit_contest_path(@contest)
|
||||
@contestants = @contest.contestants.sort_by { |contestant| [ -contestant.completions.size, contestant.time_seconds ] }
|
||||
filter_contestants_per_category
|
||||
@puzzles = @contest.puzzles.order(:id)
|
||||
@messages = @contest.messages.order(:time_seconds)
|
||||
set_badges
|
||||
end
|
||||
|
||||
def edit
|
||||
authorize @contest
|
||||
|
||||
@title = "Edit contest settings"
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
end
|
||||
|
||||
def new
|
||||
authorize :contest
|
||||
|
||||
@contest = Contest.new
|
||||
@title = "New jigsaw puzzle competition"
|
||||
end
|
||||
|
||||
def create
|
||||
@@ -39,7 +41,7 @@ class ContestsController < ApplicationController
|
||||
@contest = Contest.new(contest_params)
|
||||
@contest.user_id = current_user.id
|
||||
if @contest.save
|
||||
redirect_to @contest
|
||||
redirect_to @contest, notice: t("contests.new.notice")
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
@@ -49,28 +51,41 @@ class ContestsController < ApplicationController
|
||||
authorize @contest
|
||||
|
||||
if @contest.update(contest_params)
|
||||
redirect_to @contest
|
||||
redirect_to @contest, notice: t("contests.edit.notice")
|
||||
else
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @contest
|
||||
|
||||
@contest.destroy
|
||||
redirect_to contests_path, notice: t("contests.destroy.notice")
|
||||
end
|
||||
|
||||
def scoreboard
|
||||
@contest = Contest.find_by(slug: params[:id])
|
||||
unless @contest
|
||||
unless @contest && @contest.public
|
||||
skip_authorization
|
||||
not_found and return
|
||||
end
|
||||
authorize @contest
|
||||
|
||||
@title = @contest.name
|
||||
@contestants = @contest.contestants.order(:name)
|
||||
I18n.locale = @contest.lang
|
||||
|
||||
@title = I18n.t("contests.scoreboard.title", name: @contest.name)
|
||||
@contestants = @contest.contestants.sort_by { |contestant| [ -contestant.completions.size, contestant.time_seconds ] }
|
||||
filter_contestants_per_category
|
||||
@puzzles = @contest.puzzles.order(:id)
|
||||
extend_completions!(@contest.completions)
|
||||
@action_name = t("helpers.buttons.refresh")
|
||||
if params.key?(:category)
|
||||
@action_path = "/public/#{@contest.friendly_id}?category=#{params[:category]}"
|
||||
else
|
||||
@action_path = "/public/#{@contest.friendly_id}"
|
||||
end
|
||||
render :scoreboard
|
||||
end
|
||||
|
||||
@@ -78,8 +93,8 @@ class ContestsController < ApplicationController
|
||||
|
||||
def set_badges
|
||||
@badges = []
|
||||
@badges.push("team") if @contest.team
|
||||
@badges.push("registration") if @contest.allow_registration
|
||||
@badges.push(t("helpers.badges.team")) if @contest.team
|
||||
@badges.push(t("helpers.badges.registration")) if @contest.allow_registration
|
||||
end
|
||||
|
||||
def set_contest
|
||||
@@ -87,6 +102,16 @@ class ContestsController < ApplicationController
|
||||
end
|
||||
|
||||
def contest_params
|
||||
params.expect(contest: [ :name, :team, :allow_registration, :slug ])
|
||||
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
|
||||
|
113
app/controllers/messages_controller.rb
Normal file
113
app/controllers/messages_controller.rb
Normal file
@@ -0,0 +1,113 @@
|
||||
class MessagesController < ApplicationController
|
||||
include CompletionsConcern
|
||||
|
||||
skip_before_action :verify_authenticity_token, only: %i[ create connect cors_preflight_check ]
|
||||
skip_before_action :require_authentication, only: %i[ create connect cors_preflight_check ]
|
||||
|
||||
before_action :cors_set_access_control_headers, only: %i[ create connect cors_preflight_check ]
|
||||
before_action :set_contest, only: %i[ convert destroy ]
|
||||
before_action :set_data, only: %i[ convert ]
|
||||
|
||||
def self.local_prefixes
|
||||
super + [ "completions" ]
|
||||
end
|
||||
|
||||
def cors_set_access_control_headers
|
||||
response.set_header("Access-Control-Allow-Origin", "*")
|
||||
response.set_header("Access-Control-Allow-Credentials", "true")
|
||||
response.set_header("Access-Control-Allow-Methods", "POST")
|
||||
response.set_header("Access-Control-Allow-Headers", "*")
|
||||
response.set_header("Access-Control-Max-Age", "86400")
|
||||
end
|
||||
|
||||
def cors_preflight_check
|
||||
skip_authorization
|
||||
end
|
||||
|
||||
def connect
|
||||
skip_authorization
|
||||
|
||||
if !params.key?(:token)
|
||||
respond_to do |format|
|
||||
format.json { render json: { error: "no token provided" }, status: 400 }
|
||||
end
|
||||
else
|
||||
@contest = Contest.find_by_token_for(:token, params[:token])
|
||||
if @contest
|
||||
respond_to do |format|
|
||||
format.json { render json: { name: @contest.name }, status: 200 }
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.json { render json: { error: "invalid token" }, status: 400 }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
skip_authorization
|
||||
|
||||
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
|
@@ -5,14 +5,16 @@ class PuzzlesController < ApplicationController
|
||||
def edit
|
||||
authorize @contest
|
||||
|
||||
@title = "Edit contest puzzle"
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
end
|
||||
|
||||
def new
|
||||
authorize @contest
|
||||
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
@puzzle = Puzzle.new
|
||||
@title = "New contest puzzle"
|
||||
end
|
||||
|
||||
def create
|
||||
@@ -21,9 +23,10 @@ class PuzzlesController < ApplicationController
|
||||
@puzzle = Puzzle.new(puzzle_params)
|
||||
@puzzle.contest_id = @contest.id
|
||||
if @puzzle.save
|
||||
redirect_to contest_path(@contest)
|
||||
redirect_to contest_path(@contest), notice: t("puzzles.new.notice")
|
||||
else
|
||||
@title = "New contest puzzle"
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
@@ -32,9 +35,10 @@ class PuzzlesController < ApplicationController
|
||||
authorize @contest
|
||||
|
||||
if @puzzle.update(puzzle_params)
|
||||
redirect_to @contest
|
||||
redirect_to @contest, notice: t("puzzles.edit.notice")
|
||||
else
|
||||
@title = "Edit contest puzzle"
|
||||
@action_name = t("helpers.buttons.back")
|
||||
@action_path = contest_path(@contest)
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
@@ -43,7 +47,7 @@ class PuzzlesController < ApplicationController
|
||||
authorize @contest
|
||||
|
||||
@puzzle.destroy
|
||||
redirect_to contest_path(@contest)
|
||||
redirect_to contest_path(@contest), notice: t("puzzles.destroy.notice")
|
||||
end
|
||||
|
||||
private
|
||||
@@ -57,6 +61,6 @@ class PuzzlesController < ApplicationController
|
||||
end
|
||||
|
||||
def puzzle_params
|
||||
params.expect(puzzle: [ :brand, :name, :image ])
|
||||
params.expect(puzzle: [ :brand, :name, :image, :pieces ])
|
||||
end
|
||||
end
|
||||
|
@@ -9,7 +9,7 @@ class SessionsController < ApplicationController
|
||||
def create
|
||||
if user = User.authenticate_by(params.permit(:email_address, :password))
|
||||
start_new_session_for user
|
||||
redirect_to after_authentication_url
|
||||
redirect_to after_authentication_url, notice: t("sessions.new.notice")
|
||||
else
|
||||
redirect_to new_session_path, alert: "Try another email address or password."
|
||||
end
|
||||
|
@@ -4,21 +4,18 @@ class UsersController < ApplicationController
|
||||
def index
|
||||
authorize :user
|
||||
|
||||
@title = "All users"
|
||||
@users = User.all
|
||||
end
|
||||
|
||||
def edit
|
||||
authorize @user
|
||||
|
||||
@title = "My settings"
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @user
|
||||
|
||||
if @user.update(user_params)
|
||||
redirect_to contests_path
|
||||
redirect_to contests_path, notice: t("users.edit.notice")
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
@@ -33,7 +30,6 @@ class UsersController < ApplicationController
|
||||
def new
|
||||
authorize :user
|
||||
|
||||
@title = "New user"
|
||||
@user = User.new()
|
||||
end
|
||||
|
||||
@@ -42,9 +38,8 @@ class UsersController < ApplicationController
|
||||
|
||||
@user = User.new(user_params)
|
||||
if @user.save
|
||||
redirect_to users_path
|
||||
redirect_to users_path, notice: t("users.new.notice")
|
||||
else
|
||||
@title = "New user"
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
@@ -60,6 +55,6 @@ class UsersController < ApplicationController
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.expect(user: [ :username, :email_address, :password ])
|
||||
params.expect(user: [ :username, :email_address, :lang, :password ])
|
||||
end
|
||||
end
|
||||
|
@@ -1,2 +1,20 @@
|
||||
module ContestsHelper
|
||||
def pad(n)
|
||||
if n > 9
|
||||
return n.to_s
|
||||
end
|
||||
"0" + n.to_s
|
||||
end
|
||||
|
||||
def display_time(time)
|
||||
h = time / 3600
|
||||
m = (time % 3600) / 60
|
||||
s = (time % 3600) % 60
|
||||
if h > 0
|
||||
return h.to_s + ":" + pad(m) + ":" + pad(s)
|
||||
elsif m > 0
|
||||
return m.to_s + ":" + pad(s)
|
||||
end
|
||||
"0:" + pad(s)
|
||||
end
|
||||
end
|
||||
|
14
app/lib/forms.rb
Normal file
14
app/lib/forms.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
module Forms
|
||||
class CsvConversionForm
|
||||
include ActiveModel::Model
|
||||
include ActiveModel::Attributes
|
||||
include ActiveModel::Validations::Callbacks
|
||||
include ActiveRecord::Transactions
|
||||
|
||||
attribute :name_column, :integer
|
||||
attribute :email_column, :integer
|
||||
|
||||
validates :name_column, presence: true
|
||||
validates_numericality_of :name_column, greater_than: -1
|
||||
end
|
||||
end
|
3
app/lib/languages.rb
Normal file
3
app/lib/languages.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
module Languages
|
||||
AVAILABLE_LANGUAGES = [ { id: "en", name: "English" }, { id: "fr", name: "Français" } ]
|
||||
end
|
@@ -1,9 +1,8 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: puzzles
|
||||
# Table name: categories
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# brand :string
|
||||
# name :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
@@ -11,16 +10,15 @@
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_puzzles_on_contest_id (contest_id)
|
||||
# index_categories_on_contest_id (contest_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# contest_id (contest_id => contests.id)
|
||||
#
|
||||
require "test_helper"
|
||||
class Category < ApplicationRecord
|
||||
belongs_to :contest
|
||||
has_and_belongs_to_many :contestants
|
||||
|
||||
class PuzzleTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
validates :name, presence: true
|
||||
end
|
@@ -2,34 +2,51 @@
|
||||
#
|
||||
# Table name: completions
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# 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
|
||||
# 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
|
||||
# message_id :integer
|
||||
# puzzle_id :integer not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_completions_on_contest_id (contest_id)
|
||||
# index_completions_on_contestant_id (contestant_id)
|
||||
# index_completions_on_message_id (message_id)
|
||||
# index_completions_on_puzzle_id (puzzle_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# contest_id (contest_id => contests.id)
|
||||
# contestant_id (contestant_id => contestants.id)
|
||||
# message_id (message_id => messages.id)
|
||||
# puzzle_id (puzzle_id => puzzles.id)
|
||||
#
|
||||
class Completion < ApplicationRecord
|
||||
belongs_to :contest
|
||||
belongs_to :contestant
|
||||
belongs_to :puzzle
|
||||
belongs_to :message, optional: true
|
||||
|
||||
attr_accessor :display_time_from_start, :display_relative_time
|
||||
before_save :add_time_seconds
|
||||
|
||||
validates :time_seconds, presence: true
|
||||
validates_numericality_of :time_seconds
|
||||
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/ }
|
||||
validates :contestant_id, uniqueness: { scope: :puzzle }, if: -> { contest.puzzles.size == 1 }
|
||||
validates :puzzle_id, uniqueness: { scope: :contestant }, if: -> { contest.puzzles.size > 1 }
|
||||
|
||||
def add_time_seconds
|
||||
arr = display_time_from_start.split(":")
|
||||
if arr.size == 3
|
||||
self.time_seconds = arr[0].to_i * 3600 + arr[1].to_i * 60 + arr[2].to_i
|
||||
elsif arr.size == 2
|
||||
self.time_seconds = arr[0].to_i * 60 + arr[1].to_i
|
||||
elsif arr.size == 1
|
||||
self.time_seconds = arr[0].to_i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@@ -4,7 +4,9 @@
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# allow_registration :boolean default(FALSE)
|
||||
# lang :string default("en")
|
||||
# name :string
|
||||
# public :boolean default(FALSE)
|
||||
# slug :string
|
||||
# team :boolean default(FALSE)
|
||||
# created_at :datetime not null
|
||||
@@ -13,6 +15,7 @@
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_contests_on_slug (slug) UNIQUE
|
||||
# index_contests_on_user_id (user_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
@@ -20,12 +23,19 @@
|
||||
# user_id (user_id => users.id)
|
||||
#
|
||||
class Contest < ApplicationRecord
|
||||
belongs_to :user
|
||||
extend FriendlyId
|
||||
|
||||
belongs_to :user
|
||||
has_many :categories
|
||||
has_many :completions, dependent: :destroy
|
||||
has_many :contestants, dependent: :destroy
|
||||
has_many :puzzles, dependent: :destroy
|
||||
has_many :messages, dependent: :destroy
|
||||
|
||||
friendly_id :name, use: :slugged
|
||||
|
||||
validates :name, presence: true
|
||||
validates :slug, presence: true, uniqueness: true, format: { with: /\A(\w|-)*\z/, message: 'Only alphanumeric characters, "-" and "_" allowed.' }
|
||||
validates :lang, inclusion: { in: Languages::AVAILABLE_LANGUAGES.map { |lang| lang[:id] } }
|
||||
|
||||
generates_token_for :token
|
||||
end
|
||||
|
@@ -2,12 +2,14 @@
|
||||
#
|
||||
# Table name: contestants
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# email :string
|
||||
# name :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# contest_id :integer not null
|
||||
# id :integer not null, primary key
|
||||
# display_time :string
|
||||
# email :string
|
||||
# name :string
|
||||
# time_seconds :integer
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# contest_id :integer not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
@@ -19,8 +21,27 @@
|
||||
#
|
||||
class Contestant < ApplicationRecord
|
||||
belongs_to :contest
|
||||
has_many :completions, dependent: :destroy
|
||||
has_and_belongs_to_many :categories
|
||||
|
||||
has_many :completions
|
||||
before_validation :initialize_time_seconds_if_empty
|
||||
|
||||
validates :name, presence: true
|
||||
validates :time_seconds, presence: true
|
||||
|
||||
def form_name
|
||||
if email.present?
|
||||
"#{name} - #{email}"
|
||||
else
|
||||
name
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def initialize_time_seconds_if_empty
|
||||
if !self.time_seconds
|
||||
self.time_seconds = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
51
app/models/csv_import.rb
Normal file
51
app/models/csv_import.rb
Normal file
@@ -0,0 +1,51 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: csv_imports
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# content :string not null
|
||||
# separator :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
class CsvImport < ApplicationRecord
|
||||
enum :separator, { comma: ",", semicolon: ";" }, suffix: true, default: :comma
|
||||
|
||||
has_one_attached :file
|
||||
|
||||
validates :file, presence: true
|
||||
validate :acceptable_csv, on: :create
|
||||
|
||||
before_save :read_csv
|
||||
|
||||
def acceptable_csv
|
||||
return unless file.attached?
|
||||
|
||||
if file.blob.byte_size > 5 * 1024 * 1024
|
||||
errors.add(:file, "this csv file is too large, it must be under 5MB")
|
||||
return
|
||||
end
|
||||
|
||||
if file.content_type != "text/csv"
|
||||
errors.add(:file, :not_a_csv_file)
|
||||
return
|
||||
end
|
||||
|
||||
begin
|
||||
csv = CSV.read(attachment_changes["file"].attachable.path, col_sep: separator_for_database)
|
||||
|
||||
errors.add(:file, :empty) if csv.count < 1 || (csv.count == 1 && csv[0].count == 1 && csv[0][0] == "")
|
||||
rescue CSV::MalformedCSVError => e
|
||||
errors.add(:file, e.message)
|
||||
end
|
||||
end
|
||||
|
||||
def read_csv
|
||||
self.content = JSON.dump(CSV.read(attachment_changes["file"].attachable.path, col_sep: separator_for_database))
|
||||
end
|
||||
|
||||
def options_for_separator
|
||||
keys = self.class.separators.keys
|
||||
keys.map(&:humanize).zip(keys).to_h
|
||||
end
|
||||
end
|
28
app/models/message.rb
Normal file
28
app/models/message.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: messages
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# author :string
|
||||
# display_time :string
|
||||
# text :string not null
|
||||
# time_seconds :integer not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# contest_id :integer not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_messages_on_contest_id (contest_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# contest_id (contest_id => contests.id)
|
||||
#
|
||||
class Message < ApplicationRecord
|
||||
belongs_to :contest
|
||||
has_many :completions, dependent: :nullify
|
||||
|
||||
validates :author, presence: true
|
||||
validates :text, presence: true
|
||||
end
|
@@ -5,6 +5,7 @@
|
||||
# id :integer not null, primary key
|
||||
# brand :string
|
||||
# name :string
|
||||
# pieces :integer not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# contest_id :integer not null
|
||||
@@ -20,9 +21,9 @@
|
||||
class Puzzle < ApplicationRecord
|
||||
belongs_to :contest
|
||||
|
||||
has_many :completions
|
||||
has_many :completions, dependent: :destroy
|
||||
has_one_attached :image
|
||||
|
||||
validates :name, presence: true
|
||||
validates :brand, presence: true
|
||||
validates :pieces, presence: true
|
||||
end
|
||||
|
@@ -5,6 +5,7 @@
|
||||
# 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
|
||||
@@ -22,4 +23,6 @@ class User < ApplicationRecord
|
||||
normalizes :email_address, with: ->(e) { e.strip.downcase }
|
||||
|
||||
validates :username, presence: true, uniqueness: true
|
||||
validates :email_address, presence: true, uniqueness: true
|
||||
validates :lang, inclusion: { in: Languages::AVAILABLE_LANGUAGES.map { |lang| lang[:id] } }
|
||||
end
|
||||
|
@@ -15,10 +15,22 @@ class ContestPolicy < ApplicationPolicy
|
||||
true
|
||||
end
|
||||
|
||||
def convert?
|
||||
record.user.id == user.id || user.admin?
|
||||
end
|
||||
|
||||
def convert_csv?
|
||||
record.user.id == user.id || user.admin?
|
||||
end
|
||||
|
||||
def edit?
|
||||
record.user.id == user.id || user.admin?
|
||||
end
|
||||
|
||||
def finalize_import?
|
||||
record.user.id == user.id || user.admin?
|
||||
end
|
||||
|
||||
def update?
|
||||
record.user.id == user.id || user.admin?
|
||||
end
|
||||
@@ -27,7 +39,15 @@ class ContestPolicy < ApplicationPolicy
|
||||
record.user.id == user.id || user.admin?
|
||||
end
|
||||
|
||||
def import?
|
||||
record.user.id == user.id || user.admin?
|
||||
end
|
||||
|
||||
def scoreboard?
|
||||
true
|
||||
end
|
||||
|
||||
def upload_csv?
|
||||
record.user.id == user.id || user.admin?
|
||||
end
|
||||
end
|
||||
|
@@ -1,5 +1,5 @@
|
||||
class UserPolicy < ApplicationPolicy
|
||||
def index
|
||||
def index?
|
||||
user.admin?
|
||||
end
|
||||
|
||||
|
@@ -1,19 +1,41 @@
|
||||
= form_with model: completion, url: url, method: method do |form|
|
||||
- if @message
|
||||
= form.hidden_field :message_id, value: @message.id
|
||||
.row.mb-3
|
||||
.col
|
||||
h4 = t("messages.singular").capitalize
|
||||
.alert.alert-secondary
|
||||
b
|
||||
= @message.author
|
||||
br
|
||||
= @message.text
|
||||
.row
|
||||
.col
|
||||
h4 = t("completions.singular").capitalize
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-floating
|
||||
= form.text_field :time_seconds, autocomplete: "off", class: "form-control"
|
||||
= form.label :time_seconds, class: "required"
|
||||
= form.text_field :display_time_from_start, autocomplete: "off", class: "form-control"
|
||||
= form.label :display_time_from_start, class: "required"
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-floating
|
||||
= form.select :contestant_id, @contestants.map { |contestant| [contestant.name, contestant.id] }, {}, class: "form-select"
|
||||
= form.select :contestant_id, @contestants.map { |contestant| [contestant.form_name, contestant.id] }, {}, class: "form-select"
|
||||
= form.label :contestant_id
|
||||
.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
|
||||
- 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
|
||||
.col
|
||||
= form.submit submit_text, class: "btn btn-primary"
|
@@ -1 +1 @@
|
||||
= render "form", contest: @contest, completion: @completion, submit_text: "Save", method: :patch, url: "/contests/#{@contest.id}/completions/#{@completion.id}"
|
||||
= render "form", contest: @contest, completion: @completion, submit_text: t("helpers.buttons.save"), method: :patch, url: "/contests/#{@contest.id}/completions/#{@completion.id}"
|
@@ -1 +1 @@
|
||||
= render "form", completion: @completion, submit_text: "Create", method: :post, url: "/contests/#{@contest.id}/completions"
|
||||
= render "form", completion: @completion, submit_text: t("helpers.buttons.create"), method: :post, url: "/contests/#{@contest.id}/completions"
|
@@ -13,11 +13,20 @@
|
||||
.form-floating
|
||||
= form.text_field :email, autocomplete: "off", class: "form-control"
|
||||
= form.label :email
|
||||
.form-text Optional. Fill this only if you intend to send emails through this app.
|
||||
.form-text
|
||||
= t("activerecord.attributes.contestant.email_description")
|
||||
- if @contest.categories
|
||||
.row.mt-4
|
||||
.col
|
||||
- @contest.categories.each do |category|
|
||||
.form-check.form-switch
|
||||
= form.check_box "category_#{category.id}".to_sym, class: "form-check-input", checked: @contestant.categories.where(id: category.id).any?
|
||||
= form.label category.name
|
||||
|
||||
.row.mt-4
|
||||
.col
|
||||
- if method == :patch
|
||||
= link_to "Delete", contest_contestant_path(contest, contestant), data: { turbo_method: :delete }, class: "btn btn-danger me-2"
|
||||
= link_to t("helpers.buttons.delete"), contest_contestant_path(contest, contestant), data: { turbo_method: :delete }, class: "btn btn-danger me-2"
|
||||
= form.submit submit_text, class: "btn btn-primary"
|
||||
|
||||
- if method == :patch
|
||||
@@ -27,27 +36,35 @@
|
||||
table.table.table-striped.table-hover
|
||||
thead
|
||||
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"
|
||||
| Time since start
|
||||
th scope="col"
|
||||
| Relative time
|
||||
th scope="col"
|
||||
| Puzzle
|
||||
= t("activerecord.attributes.completion.puzzle")
|
||||
tbody
|
||||
- @completions.each do |completion|
|
||||
tr scope="row"
|
||||
td
|
||||
= completion.display_time_from_start
|
||||
= completion.display_time_from_start
|
||||
- if @contest.puzzles.size > 1
|
||||
td
|
||||
= completion.display_relative_time
|
||||
td
|
||||
= completion.display_relative_time
|
||||
- if !completion.puzzle.brand.blank?
|
||||
| #{completion.puzzle.name} - #{completion.puzzle.brand}
|
||||
- else
|
||||
| #{completion.puzzle.name}
|
||||
td
|
||||
| #{completion.puzzle.name} - #{completion.puzzle.brand}
|
||||
td
|
||||
a.btn.btn-sm.btn-secondary.me-2 href=edit_contest_completion_path(@contest, completion, contestant.id)
|
||||
| Edit
|
||||
= link_to "Delete", contest_completion_path(contest, completion, contestant_id: contestant.id),
|
||||
a.btn.btn-sm.btn-secondary.me-2 href=edit_contest_completion_path(@contest, completion, contestant_id: contestant.id)
|
||||
= t("helpers.buttons.edit")
|
||||
= link_to t("helpers.buttons.delete"), contest_completion_path(contest, completion, contestant_id: contestant.id),
|
||||
data: { turbo_method: :delete }, class: "btn btn-sm btn-secondary"
|
||||
.row
|
||||
.col
|
||||
a.btn.btn-primary href=new_contest_completion_path(@contest, contestant_id: contestant.id)
|
||||
| Add completion
|
||||
= t("helpers.buttons.add")
|
||||
|
40
app/views/contestants/convert_csv.html.slim
Normal file
40
app/views/contestants/convert_csv.html.slim
Normal file
@@ -0,0 +1,40 @@
|
||||
= form_with model: @form, url: "/contests/#{@contest.id}/import/#{@csv_import.id}" do |form|
|
||||
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-floating
|
||||
= form.select :name_column, [[t("helpers.none"), -1]] + Array.new(@content[0].count) {|i| [t("helpers.field") + "_#{i}", i] }, {}, class: "form-select"
|
||||
= form.label :name_column
|
||||
= t("contestants.import.name_column")
|
||||
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-floating
|
||||
= form.select :email_column, [[t("helpers.none"), -1]] + Array.new(@content[0].count) {|i| [t("helpers.field") + "_#{i}", i] }, {}, class: "form-select"
|
||||
= form.label :email_column
|
||||
= t("contestants.import.email_column")
|
||||
|
||||
.row.mb-3
|
||||
.col
|
||||
= form.submit t("helpers.buttons.confirm"), class: "btn btn-primary"
|
||||
|
||||
.row.g-3
|
||||
.col
|
||||
table.table.table-striped.table-hover
|
||||
thead
|
||||
tr
|
||||
- @content[0].each_with_index do |_, i|
|
||||
th scope="col"
|
||||
= t("helpers.field") + "_#{i}"
|
||||
th scope="col" style="white-space: nowrap"
|
||||
= t("contestants.import.import_column")
|
||||
tbody
|
||||
- @content.each_with_index do |row, i|
|
||||
tr scope="row"
|
||||
- row.each do |value|
|
||||
td
|
||||
= value
|
||||
td
|
||||
.form-check.form-switch
|
||||
= form.check_box "row_#{i}".to_sym, class: "form-check-input", checked: true
|
||||
|
@@ -1 +1 @@
|
||||
= render "form", contest: @contest, contestant: @contestant, submit_text: "Save", method: :patch, url: "/contests/#{@contest.id}/contestants/#{@contestant.id}"
|
||||
= render "form", contest: @contest, contestant: @contestant, submit_text: t("helpers.buttons.save"), method: :patch, url: "/contests/#{@contest.id}/contestants/#{@contestant.id}"
|
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,3 +1,4 @@
|
||||
h4.mt-5 = t("contests.form.general")
|
||||
= form_with model: contest do |form|
|
||||
.row.mb-3
|
||||
.col
|
||||
@@ -7,22 +8,58 @@
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-floating
|
||||
= form.text_field :slug, autocomplete: "off", class: "form-control"
|
||||
= form.label :slug, class: "required"
|
||||
.form-text This will be used for building the public scoreboard URL: https://puzzle-scoreboard.org/public/<slug>.
|
||||
= form.select :lang, Languages::AVAILABLE_LANGUAGES.map { |lang| [ lang[:name], lang[:id] ] }, {}, class: "form-select"
|
||||
= form.label :lang
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-check.form-switch
|
||||
= form.check_box :public, class: "form-check-input"
|
||||
= form.label :public
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-check.form-switch
|
||||
= form.check_box :team, class: "form-check-input"
|
||||
= form.label :team
|
||||
| Team contest
|
||||
.form-text For UI display purposes mainly
|
||||
.row.mb-3
|
||||
.form-text = t("activerecord.attributes.contest.team_description")
|
||||
.row.mb-3 style="display: none"
|
||||
.col
|
||||
.form-check.form-switch
|
||||
= form.check_box :allow_registration, class: "form-check-input"
|
||||
= form.label :allow_registration
|
||||
.form-text Generates a shareable registration form for this contest
|
||||
.form-text = t("activerecord.attributes.contest.allow_registration_description")
|
||||
.row
|
||||
.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 +1 @@
|
||||
= render "form", contest: @contest, submit_text: "Save"
|
||||
= render "form", contest: @contest, submit_text: t("helpers.buttons.save")
|
@@ -1,10 +1,10 @@
|
||||
.row
|
||||
.col
|
||||
h4.mb-3
|
||||
| Manage your contests
|
||||
= t("contests.index.manage_contests")
|
||||
.float-end
|
||||
a.btn.btn-primary.mb-4 href=new_contest_path
|
||||
| Create a new contest
|
||||
= t("contests.index.new_contest")
|
||||
|
||||
.row.row-cols-1.row-cols-md-3.g-4
|
||||
- @contests.each do |contest|
|
||||
@@ -15,7 +15,11 @@
|
||||
.card-header
|
||||
= contest.name
|
||||
.card-body
|
||||
.card-text.mb-2 = "#{contest.puzzles.length} puzzles - #{contest.contestants.length} participants"
|
||||
.card-text.mb-2
|
||||
= "#{contest.puzzles.length} #{t('puzzles.singular')}" if contest.puzzles.length <= 1
|
||||
= "#{contest.puzzles.length} #{t('puzzles.plural')}" if contest.puzzles.length > 1
|
||||
= " - #{contest.contestants.length} #{t('contestants.singular')}" if contest.contestants.length <= 1
|
||||
= " - #{contest.contestants.length} #{t('contestants.plural')}" if contest.contestants.length > 1
|
||||
.row
|
||||
.col
|
||||
- contest.puzzles.each do |puzzle|
|
||||
|
@@ -1 +1 @@
|
||||
= render "form", contest: @contest, submit_text: "Create"
|
||||
= render "form", contest: @contest, submit_text: t("helpers.buttons.create")
|
@@ -1,18 +1,94 @@
|
||||
table.table.table-striped.table-hover
|
||||
thead
|
||||
tr
|
||||
th scope="col"
|
||||
| Rank
|
||||
th scope="col"
|
||||
| Name
|
||||
th scope="col"
|
||||
| Completed puzzles
|
||||
tbody
|
||||
- @contestants.each_with_index do |contestant, index|
|
||||
tr scope="row"
|
||||
td
|
||||
= index + 1
|
||||
td
|
||||
= contestant.name
|
||||
td
|
||||
= contestant.completions.length
|
||||
css:
|
||||
@media (max-width: 800px) {
|
||||
a.btn { display: none; }
|
||||
.col-5 { display: none; }
|
||||
.col-6 { width: 100% !important; display: block !important; }
|
||||
.small-screen-image { display: block !important; }
|
||||
.container { margin-top: 2rem !important; }
|
||||
}
|
||||
|
||||
- if @contest.puzzles.size <= 1
|
||||
.row.small-screen-image style="display: none"
|
||||
- @contest.puzzles.each do |puzzle|
|
||||
.d-flex.flex-column.justify-content-center.mb-5
|
||||
= image_tag(puzzle.image, style: "max-height: 200px; object-fit: contain") if puzzle.image.attached?
|
||||
.mt-2.fs-6 style="text-align: center"
|
||||
=> "#{puzzle.name} -"
|
||||
= "#{puzzle.brand} #{puzzle.pieces}p"
|
||||
|
||||
= render "category_selector"
|
||||
|
||||
.row
|
||||
.col-6.d-flex.flex-column style="height: calc(100vh - 180px)"
|
||||
.d-flex.flex-column style="overflow-y: auto"
|
||||
table.table.table-striped.table-hover
|
||||
thead
|
||||
tr
|
||||
th
|
||||
= 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.length
|
||||
td style="position: relative"
|
||||
- if index > 0 && contestant.time_seconds > 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.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.length
|
||||
td
|
||||
= contestant.display_time
|
@@ -1,69 +1,168 @@
|
||||
.row.mb-4
|
||||
.col
|
||||
css:
|
||||
.badges { margin-top: -18px; position: absolute; }
|
||||
.badges
|
||||
- @badges.each do |badge|
|
||||
span.badge.text-bg-info.me-2
|
||||
= badge
|
||||
- if @badges.size > 0 && false
|
||||
.row.mb-4
|
||||
.col
|
||||
.badges style="margin-top: -18px; position: absolute"
|
||||
- @badges.each do |badge|
|
||||
span.badge.text-bg-info.me-2
|
||||
= badge
|
||||
|
||||
.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
|
||||
.float-end
|
||||
a.btn.btn-primary href=edit_contest_path(@contest)
|
||||
| Edit contest
|
||||
p
|
||||
|> Public scoreboard:
|
||||
= link_to root_url + "public/#{@contest.slug}", root_url + "public/#{@contest.slug}"
|
||||
- if @contest.public
|
||||
a.btn.btn-success href="/public/#{@contest.slug}"
|
||||
= t("contests.show.open_public_scoreboard")
|
||||
- else
|
||||
a.btn.btn-success.disabled
|
||||
= t("contests.show.public_scoreboard_disabled")
|
||||
button.btn.btn-success.ms-3 onclick="copyExtensionUrlToClipboard()"
|
||||
css:
|
||||
button > svg {
|
||||
margin-right: 2px;
|
||||
margin-top: -3px;
|
||||
}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16">
|
||||
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1z"/>
|
||||
<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0z"/>
|
||||
</svg>
|
||||
=< t("contests.show.copy_extension_url")
|
||||
|
||||
.row.mb-4
|
||||
.col-6
|
||||
.row.mb-4 style="height: calc(100vh - 280px)"
|
||||
.col-6.d-flex.flex-column style="height: 100%"
|
||||
.row
|
||||
.col
|
||||
h4
|
||||
| Puzzles
|
||||
.row.row-cols-1.row-cols-md-3.g-4.mb-4
|
||||
- @puzzles.each do |puzzle|
|
||||
= 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-sm.btn.btn-primary href=contest_import_path(@contest) style="margin-top: -3px"
|
||||
| #{t("helpers.buttons.import")}
|
||||
- if @contest.categories.size > 0
|
||||
.row
|
||||
.col
|
||||
css:
|
||||
.card:hover { background-color: lightblue; }
|
||||
.card.h-100
|
||||
.card-header
|
||||
= puzzle.name
|
||||
= image_tag puzzle.image if puzzle.image.attached?
|
||||
.card-body
|
||||
p.card-text
|
||||
= puzzle.brand
|
||||
a.stretched-link href=edit_contest_puzzle_path(@contest, puzzle)
|
||||
.row
|
||||
.col
|
||||
a.btn.btn-primary href=new_contest_puzzle_path(@contest)
|
||||
| Add puzzle
|
||||
.col-6
|
||||
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.length
|
||||
td
|
||||
= 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
|
||||
| Contestants
|
||||
= t("puzzles.plural").capitalize
|
||||
a.ms-3.btn.btn-sm.btn-primary href=new_contest_puzzle_path(@contest) style="margin-top: -3px"
|
||||
| + #{t("helpers.buttons.add")}
|
||||
table.table.table-striped.table-hover
|
||||
thead
|
||||
tr
|
||||
th scope="col"
|
||||
| Name
|
||||
th scope="col"
|
||||
| Completed puzzles
|
||||
th
|
||||
= t("activerecord.attributes.puzzle.image")
|
||||
th
|
||||
= t("activerecord.attributes.puzzle.name")
|
||||
th
|
||||
= t("activerecord.attributes.puzzle.brand")
|
||||
th
|
||||
= t("activerecord.attributes.puzzle.pieces")
|
||||
tbody
|
||||
- @contestants.each do |contestant|
|
||||
tr scope="row"
|
||||
- @puzzles.each do |puzzle|
|
||||
tr.align-middle scope="row"
|
||||
td
|
||||
= contestant.name
|
||||
= image_tag(puzzle.image, class: "img-fluid", style: "max-height: 48px;") if puzzle.image.attached?
|
||||
td
|
||||
= contestant.completions.length
|
||||
= puzzle.name
|
||||
td
|
||||
a.btn.btn-sm.btn-secondary href=edit_contest_contestant_path(@contest, contestant)
|
||||
| Open
|
||||
a.btn.btn-sm.btn-secondary.ms-2 href=new_contest_completion_path(@contest, contestant_id: contestant.id)
|
||||
| Add completion
|
||||
.row.mt-4
|
||||
.col
|
||||
a.btn.btn-primary href=new_contest_contestant_path(@contest)
|
||||
| Add contestant
|
||||
= puzzle.brand
|
||||
td
|
||||
= puzzle.pieces
|
||||
td
|
||||
a.btn.btn-sm.btn-secondary href=edit_contest_puzzle_path(@contest, puzzle)
|
||||
= t("helpers.buttons.edit")
|
||||
- if @messages
|
||||
.row.mt-5
|
||||
.col
|
||||
h4 = t("messages.plural").capitalize
|
||||
- if @puzzles.size == 0
|
||||
.row
|
||||
.col.alert.alert-danger
|
||||
= t("messages.warning")
|
||||
.d-flex.flex-column style="overflow-y: auto"
|
||||
table.table.table-striped.table-hover
|
||||
thead
|
||||
tr
|
||||
th scope="col" style="white-space: nowrap"
|
||||
= t("activerecord.attributes.message.processed")
|
||||
th scope="col"
|
||||
= t("activerecord.attributes.message.time")
|
||||
th scope="col"
|
||||
= t("activerecord.attributes.message.author")
|
||||
th.w-25 scope="col"
|
||||
= t("activerecord.attributes.message.text")
|
||||
th.w-25 scope="col"
|
||||
tbody
|
||||
- @messages.each do |message|
|
||||
tr.align-middle scope="row"
|
||||
td style="text-align: center"
|
||||
- if message.completions.size > 0
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-square" viewBox="0 0 16 16">
|
||||
<path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"/>
|
||||
<path d="M10.97 4.97a.75.75 0 0 1 1.071 1.05l-3.992 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425z"/>
|
||||
</svg>
|
||||
td
|
||||
= 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;"
|
||||
nav.navbar.bg-body-primary
|
||||
- if @current_user.admin
|
||||
a.navbar-brand href=users_path
|
||||
| Users
|
||||
a.navbar-brand href=contests_path
|
||||
| Home
|
||||
a.navbar-brand href=user_path(@current_user)
|
||||
| Settings
|
||||
= button_to "Log out", session_path, method: :delete
|
||||
a.navbar-brand href=users_path class="btn btn-light" style="margin-right: 0"
|
||||
= t("nav.users")
|
||||
a.navbar-brand href=contests_path class="btn btn-light" style="margin-right: 0"
|
||||
= t("nav.home")
|
||||
a.navbar-brand href=user_path(@current_user) class="btn btn-light"
|
||||
= t("nav.settings")
|
||||
= button_to t("nav.log_out"), session_path, method: :delete, class: "btn btn-danger"
|
||||
|
||||
h1.mb-4 = @title
|
||||
css:
|
||||
.toast {
|
||||
opacity: 0;
|
||||
animation: fadeInAndOut 6s linear;
|
||||
}
|
||||
@keyframes fadeInAndOut {
|
||||
0%, 5%, 100% { opacity: 0 }
|
||||
7%, 85% { opacity: 1 }
|
||||
}
|
||||
javascript:
|
||||
function closeToast(event) {
|
||||
event.target.parentElement.parentElement.style.display = 'none';
|
||||
}
|
||||
|
||||
.toast-container.position-fixed.p-3 style="right: 30px; top: 85px"
|
||||
- flash.each do |type, msg|
|
||||
.toast role="alert" aria-live="assertive" aria-atomic="true" style="display: block"
|
||||
.toast-header
|
||||
strong.me-auto
|
||||
i.bi-bell-fill.fs-6.text-primary
|
||||
=< type.humanize
|
||||
small.text-body-secondary
|
||||
| Just now
|
||||
button.btn-close type="button" data-bs-dismiss="toast" aria-label="Close" onclick="closeToast(event)"
|
||||
.toast-body
|
||||
= msg
|
||||
|
||||
h1.mb-4
|
||||
= @title
|
||||
- if @action_path
|
||||
a.ms-4.btn.btn-primary href=@action_path style="margin-top: -6px"
|
||||
= @action_name
|
||||
|
||||
= yield
|
@@ -1,4 +1,10 @@
|
||||
= form_with model: puzzle, url: url, method: method do |form|
|
||||
.row.mb-3
|
||||
.col.alert.alert-warning
|
||||
= t("puzzles.form.fake_data_recommendation")
|
||||
.row.mb-3
|
||||
.col
|
||||
= image_tag(puzzle.image, class: "img-fluid", style: "max-height: 256px") if puzzle.image.attached?
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-floating
|
||||
@@ -11,10 +17,32 @@
|
||||
= form.label :brand, class: "required"
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-text Select an image
|
||||
.form-floating
|
||||
= form.number_field :pieces, autocomplete: "off", class: "form-control"
|
||||
= form.label :pieces, class: "required"
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-text.mb-1
|
||||
= t("puzzles.image_select")
|
||||
= form.file_field :image, accept: "image/*", class: "form-control"
|
||||
.form-text.error-message style="display: none;" id="image-error-message"
|
||||
= t("puzzles.form.file_too_big")
|
||||
javascript:
|
||||
function setMaxUploadSize() {
|
||||
const el = document.querySelector('input[type="file"]');
|
||||
el.onchange = function() {
|
||||
if(this.files[0].size > 2 * 1024 * 1024) {
|
||||
document.getElementById('image-error-message').style.display = 'block';
|
||||
this.value = "";
|
||||
} else {
|
||||
document.getElementById('image-error-message').style.display = 'none';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
setMaxUploadSize();
|
||||
.row.mt-4
|
||||
.col
|
||||
- if method == :patch
|
||||
= link_to "Delete", contest_puzzle_path(contest, puzzle), data: { turbo_method: :delete }, class: "btn btn-danger me-2"
|
||||
= link_to t("helpers.buttons.delete"), contest_puzzle_path(contest, puzzle), data: { turbo_method: :delete }, class: "btn btn-danger me-2"
|
||||
= form.submit submit_text, class: "btn btn-primary"
|
@@ -1 +1 @@
|
||||
= render "form", contest: @contest, puzzle: @puzzle, submit_text: "Save", method: :patch, url: "/contests/#{@contest.id}/puzzles/#{@puzzle.id}"
|
||||
= render "form", contest: @contest, puzzle: @puzzle, submit_text: t("helpers.buttons.save"), method: :patch, url: "/contests/#{@contest.id}/puzzles/#{@puzzle.id}"
|
@@ -1 +1 @@
|
||||
= render "form", contest: @contest, puzzle: @puzzle, submit_text: "Add", method: :post, url: "/contests/#{@contest.id}/puzzles"
|
||||
= render "form", contest: @contest, puzzle: @puzzle, submit_text: t("helpers.buttons.add"), method: :post, url: "/contests/#{@contest.id}/puzzles"
|
@@ -1,11 +0,0 @@
|
||||
<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
|
||||
<%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>
|
||||
|
||||
<%= form_with url: session_path do |form| %>
|
||||
<%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %><br>
|
||||
<%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72 %><br>
|
||||
<%= form.submit "Sign in" %>
|
||||
<% end %>
|
||||
<br>
|
||||
|
||||
<%= link_to "Forgot password?", new_password_path %>
|
15
app/views/sessions/new.html.slim
Normal file
15
app/views/sessions/new.html.slim
Normal file
@@ -0,0 +1,15 @@
|
||||
= form_with url: session_path do |form|
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-floating
|
||||
= form.email_field :email_address, autocomplete: "username", required: true, autofocus: true, class: "form-control"
|
||||
= form.label :email_address, class: "required"
|
||||
= t("activerecord.attributes.session.email_address")
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-floating
|
||||
= form.password_field :password, autocomplete: "current-password", required: true, autofocus: true, class: "form-control", maxlength: 72
|
||||
= form.label :password, class: "required"
|
||||
= t("activerecord.attributes.session.password")
|
||||
|
||||
= form.submit t("helpers.buttons.sign_in")
|
@@ -1,6 +1,7 @@
|
||||
= form_with model: user, method: method do |form|
|
||||
- if method == :patch
|
||||
h4 General settings
|
||||
h4 = t("users.edit.general_section")
|
||||
|
||||
.row.mb-3
|
||||
.col
|
||||
.input-group
|
||||
@@ -8,26 +9,34 @@
|
||||
.form-floating
|
||||
= form.text_field :username, autocomplete: "off", class: "form-control"
|
||||
= form.label :username, class: "required"
|
||||
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-floating
|
||||
= form.text_field :email_address, autocomplete: "off", class: "form-control"
|
||||
= form.label :email_address, 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
|
||||
|
||||
- if method == :post
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-floating
|
||||
= form.password_field :password, autocomplete: "off", class: "form-control"
|
||||
= form.label :password, class: "required"
|
||||
= form.submit "Save", class: "btn btn-primary"
|
||||
= form.submit t("helpers.buttons.save"), class: "btn btn-primary"
|
||||
|
||||
- if method == :patch
|
||||
h4.mt-5 Change password
|
||||
h4.mt-5 = t("users.edit.password_section")
|
||||
|
||||
= form_with model: user, method: method do |form|
|
||||
.row.mb-3
|
||||
.col
|
||||
.form-floating
|
||||
= form.password_field :password, autocomplete: "off", class: "form-control"
|
||||
= form.label :password, class: "required"
|
||||
| New password
|
||||
= form.submit "Save new password", class: "btn btn-primary"
|
||||
= form.submit t("helpers.buttons.save"), class: "btn btn-primary"
|
@@ -14,6 +14,7 @@ module PuzzleScoreboard
|
||||
# Please, add to the `ignore` list any other `lib` subdirectories that do
|
||||
# not contain `.rb` files, or that should not be reloaded or eager loaded.
|
||||
# Common ones are `templates`, `generators`, or `middleware`, for example.
|
||||
config.autoload_paths << Rails.root.join("lib")
|
||||
config.autoload_lib(ignore: %w[assets tasks])
|
||||
|
||||
# Configuration for the application, engines, and railties goes here.
|
||||
@@ -23,5 +24,8 @@ module PuzzleScoreboard
|
||||
#
|
||||
# config.time_zone = "Central Time (US & Canada)"
|
||||
# config.eager_load_paths << Rails.root.join("extras")
|
||||
|
||||
config.i18n.default_locale = :en
|
||||
config.i18n.available_locales = [ :en, :fr ]
|
||||
end
|
||||
end
|
||||
|
107
config/initializers/friendly_id.rb
Normal file
107
config/initializers/friendly_id.rb
Normal file
@@ -0,0 +1,107 @@
|
||||
# FriendlyId Global Configuration
|
||||
#
|
||||
# Use this to set up shared configuration options for your entire application.
|
||||
# Any of the configuration options shown here can also be applied to single
|
||||
# models by passing arguments to the `friendly_id` class method or defining
|
||||
# methods in your model.
|
||||
#
|
||||
# To learn more, check out the guide:
|
||||
#
|
||||
# http://norman.github.io/friendly_id/file.Guide.html
|
||||
|
||||
FriendlyId.defaults do |config|
|
||||
# ## Reserved Words
|
||||
#
|
||||
# Some words could conflict with Rails's routes when used as slugs, or are
|
||||
# undesirable to allow as slugs. Edit this list as needed for your app.
|
||||
config.use :reserved
|
||||
|
||||
config.reserved_words = %w[new edit index session login logout users admin
|
||||
stylesheets assets javascripts images]
|
||||
|
||||
# This adds an option to treat reserved words as conflicts rather than exceptions.
|
||||
# When there is no good candidate, a UUID will be appended, matching the existing
|
||||
# conflict behavior.
|
||||
|
||||
config.treat_reserved_as_conflict = true
|
||||
|
||||
# ## Friendly Finders
|
||||
#
|
||||
# Uncomment this to use friendly finders in all models. By default, if
|
||||
# you wish to find a record by its friendly id, you must do:
|
||||
#
|
||||
# MyModel.friendly.find('foo')
|
||||
#
|
||||
# If you uncomment this, you can do:
|
||||
#
|
||||
# MyModel.find('foo')
|
||||
#
|
||||
# This is significantly more convenient but may not be appropriate for
|
||||
# all applications, so you must explicitly opt-in to this behavior. You can
|
||||
# always also configure it on a per-model basis if you prefer.
|
||||
#
|
||||
# Something else to consider is that using the :finders addon boosts
|
||||
# performance because it will avoid Rails-internal code that makes runtime
|
||||
# calls to `Module.extend`.
|
||||
|
||||
config.use :finders
|
||||
|
||||
# ## Slugs
|
||||
#
|
||||
# Most applications will use the :slugged module everywhere. If you wish
|
||||
# to do so, uncomment the following line.
|
||||
|
||||
config.use :slugged
|
||||
|
||||
# By default, FriendlyId's :slugged addon expects the slug column to be named
|
||||
# 'slug', but you can change it if you wish.
|
||||
#
|
||||
# config.slug_column = 'slug'
|
||||
#
|
||||
# By default, slug has no size limit, but you can change it if you wish.
|
||||
#
|
||||
# config.slug_limit = 255
|
||||
#
|
||||
# When FriendlyId can not generate a unique ID from your base method, it appends
|
||||
# a UUID, separated by a single dash. You can configure the character used as the
|
||||
# separator. If you're upgrading from FriendlyId 4, you may wish to replace this
|
||||
# with two dashes.
|
||||
#
|
||||
# config.sequence_separator = '-'
|
||||
#
|
||||
# Note that you must use the :slugged addon **prior** to the line which
|
||||
# configures the sequence separator, or else FriendlyId will raise an undefined
|
||||
# method error.
|
||||
#
|
||||
# ## Tips and Tricks
|
||||
#
|
||||
# ### Controlling when slugs are generated
|
||||
#
|
||||
# As of FriendlyId 5.0, new slugs are generated only when the slug field is
|
||||
# nil, but if you're using a column as your base method can change this
|
||||
# behavior by overriding the `should_generate_new_friendly_id?` method that
|
||||
# FriendlyId adds to your model. The change below makes FriendlyId 5.0 behave
|
||||
# more like 4.0.
|
||||
# Note: Use(include) Slugged module in the config if using the anonymous module.
|
||||
# If you have `friendly_id :name, use: slugged` in the model, Slugged module
|
||||
# is included after the anonymous module defined in the initializer, so it
|
||||
# overrides the `should_generate_new_friendly_id?` method from the anonymous module.
|
||||
#
|
||||
# config.use :slugged
|
||||
# config.use Module.new {
|
||||
# def should_generate_new_friendly_id?
|
||||
# slug.blank? || <your_column_name_here>_changed?
|
||||
# end
|
||||
# }
|
||||
#
|
||||
# FriendlyId uses Rails's `parameterize` method to generate slugs, but for
|
||||
# languages that don't use the Roman alphabet, that's not usually sufficient.
|
||||
# Here we use the Babosa library to transliterate Russian Cyrillic slugs to
|
||||
# ASCII. If you use this, don't forget to add "babosa" to your Gemfile.
|
||||
#
|
||||
# config.use Module.new {
|
||||
# def normalize_friendly_id(text)
|
||||
# text.to_slug.normalize! :transliterations => [:russian, :latin]
|
||||
# end
|
||||
# }
|
||||
end
|
@@ -28,4 +28,230 @@
|
||||
# enabled: "ON"
|
||||
|
||||
en:
|
||||
hello: "Hello world"
|
||||
activemodel:
|
||||
errors:
|
||||
models:
|
||||
forms/csv_conversion_form:
|
||||
attributes:
|
||||
name_column:
|
||||
blank: "Participant names are required"
|
||||
greater_than: "Participant names are required"
|
||||
activerecord:
|
||||
attributes:
|
||||
category:
|
||||
contestant_count: Contestants count
|
||||
new: New category
|
||||
name: Category
|
||||
completion:
|
||||
contestant: Participant
|
||||
display_time: Time
|
||||
display_time_from_start: Time since start
|
||||
display_relative_time: Time for this puzzle
|
||||
puzzle: Puzzle
|
||||
contest:
|
||||
lang: Language for the public scoreboard
|
||||
name: Name
|
||||
public: Enable the public scoreboard
|
||||
team: Team contest
|
||||
team_description: For UI display purposes mainly
|
||||
allow_registration: Allow registration
|
||||
allow_registration_description: Generates a shareable registration form for this contest
|
||||
contestant:
|
||||
completions: completions
|
||||
display_time: Time
|
||||
email: Email
|
||||
name: Name
|
||||
email_description: Optional. Used for sending emails through this app, or for identifying participants whose gmeet handle doesn't match their registered name.
|
||||
csv_import:
|
||||
file: File
|
||||
separator: Separator
|
||||
message:
|
||||
author: Author
|
||||
processed: Processed?
|
||||
text: Content
|
||||
time: Time
|
||||
puzzle:
|
||||
brand: Brand
|
||||
image: Image
|
||||
name: Name
|
||||
pieces: Number of pieces
|
||||
session:
|
||||
email_address: Email address
|
||||
password: Password
|
||||
user:
|
||||
username: Username
|
||||
email_address: Email address
|
||||
lang: Language
|
||||
password: New password
|
||||
errors:
|
||||
models:
|
||||
completion:
|
||||
attributes:
|
||||
contestant_id:
|
||||
taken: "This contestant has already completed the puzzle"
|
||||
display_time_from_start:
|
||||
blank: Mandatory
|
||||
invalid: "Allowed formats: xx:xx:xx, x:xx:xx, xx:xx, x:xx, xx"
|
||||
puzzle_id:
|
||||
taken: "This contestant has already completed this puzzle"
|
||||
contest:
|
||||
attributes:
|
||||
name:
|
||||
blank: The contest name cannot be empty
|
||||
contestant:
|
||||
attributes:
|
||||
name:
|
||||
blank: The participant name cannot be empty
|
||||
csv_import:
|
||||
attributes:
|
||||
file:
|
||||
blank: "No file selected"
|
||||
empty: "This file is empty"
|
||||
not_a_csv_file: "it must be a CSV file"
|
||||
puzzle:
|
||||
attributes:
|
||||
name:
|
||||
blank: The puzzle name cannot be empty
|
||||
pieces:
|
||||
blank: It's mandatory to provide the number of pieces
|
||||
user:
|
||||
attributes:
|
||||
email_address:
|
||||
blank: Your email cannot be empty
|
||||
username:
|
||||
blank: Your username cannot be empty
|
||||
categories:
|
||||
destroy:
|
||||
notice: Category deleted
|
||||
new:
|
||||
error: The category name can't be empty
|
||||
notice: Category added
|
||||
completions:
|
||||
destroy:
|
||||
notice: Completion deleted
|
||||
edit:
|
||||
notice: Completion updated
|
||||
title: Edit completion
|
||||
new:
|
||||
notice: Completion added
|
||||
title: New completion
|
||||
singular: completion
|
||||
contests:
|
||||
destroy:
|
||||
notice: Contest deleted
|
||||
edit:
|
||||
notice: Contest updated
|
||||
title: Edit contest settings
|
||||
form:
|
||||
categories: Participant categories
|
||||
general: General parameters
|
||||
index:
|
||||
title: Welcome %{username}!
|
||||
manage_contests: Manage my contests
|
||||
new_contest: Create a new contest
|
||||
new:
|
||||
notice: Contest added
|
||||
title: New jigsaw puzzle contest
|
||||
scoreboard:
|
||||
refresh: Activate auto-refresh (every 5s)
|
||||
title: "%{name}"
|
||||
show:
|
||||
title: "%{name}"
|
||||
add_participant: Add participant
|
||||
add_puzzle: Add puzzle
|
||||
copy_extension_url: Copy the URL for connecting from the browser extension
|
||||
open_public_scoreboard: Open public scoreboard
|
||||
public_scoreboard_disabled: The public scoreboard is disabled
|
||||
url_copied: URL copied to the clipboard
|
||||
contestants:
|
||||
convert_csv:
|
||||
title: Import participants
|
||||
destroy:
|
||||
notice: Participant deleted
|
||||
edit:
|
||||
notice: Participant updated
|
||||
title: Participant
|
||||
team_title: Teams
|
||||
finalize_import:
|
||||
title: Import participants
|
||||
import:
|
||||
email_column: Participant email
|
||||
import_column: Import?
|
||||
name_column: Participant name
|
||||
notice: Participants imported
|
||||
title: Import participants
|
||||
new:
|
||||
notice: Participant added
|
||||
title: New participant
|
||||
team_title: New team
|
||||
singular: participant
|
||||
plural: participants
|
||||
teams:
|
||||
singular: team
|
||||
plural: teams
|
||||
upload_csv:
|
||||
title: Import participants
|
||||
helpers:
|
||||
badges:
|
||||
registration: "registration"
|
||||
team: "team"
|
||||
buttons:
|
||||
add: "Add"
|
||||
add_completion: "Add completion"
|
||||
back: "⬅ Back to the contest"
|
||||
back_to_contestant: "⬅ Back to the participant"
|
||||
confirm: "Confirm"
|
||||
create: "Create"
|
||||
delete: "Delete"
|
||||
edit: "Edit"
|
||||
import: CSV Import
|
||||
open: Open
|
||||
refresh: Refresh
|
||||
sign_in: Sign in
|
||||
save: Save
|
||||
field: Field
|
||||
none: No field selected
|
||||
rank: Rank
|
||||
messages:
|
||||
convert:
|
||||
title: New completion
|
||||
destroy:
|
||||
notice: Message deleted
|
||||
plural: "messages"
|
||||
singular: "message"
|
||||
warning: "You first need to add a puzzle before converting messages to completions."
|
||||
nav:
|
||||
users: Users
|
||||
home: My contests
|
||||
settings: Settings
|
||||
log_out: Log out
|
||||
puzzles:
|
||||
destroy:
|
||||
notice: Puzzle deleted
|
||||
edit:
|
||||
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:
|
||||
notice: Puzzle added
|
||||
title: New contest puzzle
|
||||
singular: puzzle
|
||||
plural: puzzles
|
||||
sessions:
|
||||
new:
|
||||
notice: Login successful
|
||||
title: "Login to the Public Scoreboard app"
|
||||
users:
|
||||
edit:
|
||||
notice: Settings updated
|
||||
title: "My settings"
|
||||
general_section: "General settings"
|
||||
password_section: "Change password"
|
||||
index:
|
||||
title: "All users"
|
||||
new:
|
||||
notice: User created
|
||||
title: "New user"
|
||||
|
228
config/locales/fr.yml
Normal file
228
config/locales/fr.yml
Normal file
@@ -0,0 +1,228 @@
|
||||
fr:
|
||||
activemodel:
|
||||
errors:
|
||||
models:
|
||||
forms/csv_conversion_form:
|
||||
attributes:
|
||||
name_column:
|
||||
blank: "Choisir une colonne pour les noms des participant.e.s est nécessaire"
|
||||
greater_than: "Choisir une colonne pour les noms des participant.e.s est nécessaire"
|
||||
activerecord:
|
||||
attributes:
|
||||
category:
|
||||
contestant_count: Nombre de participant.e.s
|
||||
new: Nouvelle catégorie
|
||||
name: Catégorie
|
||||
completion:
|
||||
contestant_id: Participant.e
|
||||
display_time: Temps
|
||||
display_time_from_start: Temps depuis le début
|
||||
display_relative_time: Temps pour ce puzzle
|
||||
puzzle: Puzzle
|
||||
contest:
|
||||
lang: Langue pour le classement public
|
||||
name: Nom
|
||||
public: Activer le classement public
|
||||
team: Concours par équipes
|
||||
team_description: Principalement pour des raisons d'affichage
|
||||
allow_registration: Autoriser l'inscription via l'interface
|
||||
allow_registration_description: Génère un formulaire d'inscription pour ce concours
|
||||
contestant:
|
||||
completions: Complétions
|
||||
display_time: Temps
|
||||
email: Email
|
||||
name: Nom
|
||||
email_description: Optionnel. Utile pour envoyer des emails aux participant.e.s depuis cette app, ou pour reconnaître les pseudos gmeet quand ils ne correspondent pas au nom préalablement entré.
|
||||
csv_import:
|
||||
file: Fichier
|
||||
separator: Délimiteur
|
||||
message:
|
||||
author: Auteur.ice
|
||||
processed: Traité ?
|
||||
text: Contenu
|
||||
time: Temps
|
||||
puzzle:
|
||||
brand: Marque
|
||||
image: Image
|
||||
name: Nom
|
||||
pieces: Nombre de pièces
|
||||
session:
|
||||
email_address: Adresse email
|
||||
password: Mot de passe
|
||||
user:
|
||||
username: Nom d'utilisateur.ice
|
||||
email_address: Adresse email
|
||||
lang: Langue de l'interface
|
||||
password: Nouveau mot de passe
|
||||
errors:
|
||||
models:
|
||||
completion:
|
||||
attributes:
|
||||
contestant_id:
|
||||
taken: "Ce.tte participant.e a déjà complété le puzzle"
|
||||
display_time_from_start:
|
||||
blank: Obligatoire
|
||||
invalid: "Formats autorisés: xx:xx:xx, x:xx:xx, xx:xx, x:xx, xx"
|
||||
puzzle_id:
|
||||
taken: "Ce.tte participant.e a déjà complété ce puzzle"
|
||||
contest:
|
||||
attributes:
|
||||
name:
|
||||
blank: Le nom du concours ne peut pas être vide
|
||||
contestant:
|
||||
attributes:
|
||||
name:
|
||||
blank: Le nom du ou de la participant.e ne peut pas être vide
|
||||
csv_import:
|
||||
attributes:
|
||||
file:
|
||||
blank: "Aucun fichier sélectionné"
|
||||
empty: "Ce fichier est vide"
|
||||
not_a_csv_file: "Le fichier doit être au format CSV"
|
||||
puzzle:
|
||||
attributes:
|
||||
name:
|
||||
blank: Le nom du puzzle est obligatoire
|
||||
pieces:
|
||||
blank: Il est obligatoire d'indiquer le nombre de pièces
|
||||
user:
|
||||
attributes:
|
||||
email_address:
|
||||
blank: L'email est obligatoire
|
||||
username:
|
||||
blank: Le nom d'utilisateur.ice est obligatoire
|
||||
categories:
|
||||
destroy:
|
||||
notice: Catégorie supprimée
|
||||
new:
|
||||
error: Le nom de la catégorie ne peut pas être vide
|
||||
notice: Catégorie ajoutée
|
||||
completions:
|
||||
destroy:
|
||||
notice: Complétion supprimée
|
||||
edit:
|
||||
notice: Complétion modifiée
|
||||
title: Modifier la complétion
|
||||
new:
|
||||
notice: Complétion ajoutée
|
||||
title: Ajout d'une complétion
|
||||
singular: complétion
|
||||
contests:
|
||||
destroy:
|
||||
notice: Concours supprimé
|
||||
edit:
|
||||
notice: Concours modifié
|
||||
title: Paramètres du concours
|
||||
form:
|
||||
categories: Catégories de participant.e.s
|
||||
general: Paramètres généraux
|
||||
index:
|
||||
title: Bienvenue %{username} !
|
||||
manage_contests: Mes concours de puzzle
|
||||
new_contest: Créer un nouveau concours
|
||||
new:
|
||||
notice: Concours ajouté
|
||||
title: Nouveau concours
|
||||
scoreboard:
|
||||
refresh: Activer le rafraichissement automatique de la page (toutes les 5s)
|
||||
title: "%{name}"
|
||||
show:
|
||||
title: "%{name}"
|
||||
add_participant: Ajouter un.e participant.e
|
||||
add_puzzle: Ajouter un puzzle
|
||||
copy_extension_url: Copier l'URL pour la connexion depuis l'extension web
|
||||
open_public_scoreboard: Ouvrir le classement public
|
||||
public_scoreboard_disabled: Le classement public n'est pas activé
|
||||
url_copied: L’URL a été copiée dans le presse-papier
|
||||
contestants:
|
||||
convert_csv:
|
||||
title: Importer des participant.e.s
|
||||
destroy:
|
||||
notice: Participant.e supprimé.e
|
||||
edit:
|
||||
notice: Participant.e modifié.e
|
||||
title: Participant.e
|
||||
team_title: Équipe
|
||||
finalize_import:
|
||||
title: Importer des participant.e.s
|
||||
import:
|
||||
email_column: Email des participant.e.s
|
||||
import_column: Importer ?
|
||||
name_column: Noms des participant.e.s
|
||||
notice: Participant.e.s importé.e.s
|
||||
title: Importer des participant.e.s
|
||||
new:
|
||||
notice: Participant.e ajouté.e
|
||||
title: Nouveau.elle participant.e
|
||||
team_title: Nouvelle équipe
|
||||
singular: participant.e
|
||||
plural: participant.e.s
|
||||
teams:
|
||||
singular: équipe
|
||||
plural: équipes
|
||||
upload_csv:
|
||||
title: Importer des participant.e.s
|
||||
helpers:
|
||||
badges:
|
||||
registration: "auto-inscription"
|
||||
team: "équipes"
|
||||
buttons:
|
||||
add: "Ajouter"
|
||||
add_completion: "Convertir"
|
||||
back: "⬅ Revenir au concours"
|
||||
back_to_contestant: "⬅ Revenir au/à la participant.e"
|
||||
confirm: "Confirmer"
|
||||
create: "Créer"
|
||||
delete: "Supprimer"
|
||||
edit: "Modifier"
|
||||
import: Importer un CSV
|
||||
open: Détails
|
||||
refresh: Rafraîchir
|
||||
sign_in: Se connecter
|
||||
save: Modifier
|
||||
field: Champ
|
||||
none: Aucun champ sélectionné
|
||||
rank: Rang
|
||||
messages:
|
||||
convert:
|
||||
title: Ajout d'une complétion
|
||||
destroy:
|
||||
notice: Message supprimé
|
||||
plural: "messages"
|
||||
singular: "message"
|
||||
warning: "Au moins un puzzle doit être ajouté avant de pouvoir convertir des messages en complétions."
|
||||
nav:
|
||||
users: Utilisateur.ices
|
||||
home: Mes concours
|
||||
settings: Paramètres
|
||||
log_out: Déconnexion
|
||||
puzzles:
|
||||
destroy:
|
||||
notice: Puzzle supprimé
|
||||
edit:
|
||||
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:
|
||||
notice: Puzzle ajouté
|
||||
title: Nouveau puzzle
|
||||
singular: puzzle
|
||||
plural: puzzles
|
||||
sessions:
|
||||
new:
|
||||
notice: Connection réussie
|
||||
title: "Se connecter à l'app Public Scoreboard"
|
||||
users:
|
||||
edit:
|
||||
notice: Paramètres modifiés
|
||||
title: "Mes paramètres"
|
||||
general_section: "Paramètres globaux"
|
||||
password_section: "Modifier mon mot de passe"
|
||||
index:
|
||||
title: "Tous.tes les utilisateur.ices"
|
||||
new:
|
||||
notice: Utilisateur.ice ajouté.e
|
||||
title: "Nouveau.elle utilisateur.ice"
|
@@ -9,13 +9,26 @@ Rails.application.routes.draw do
|
||||
root "contests#index"
|
||||
|
||||
resources :contests do
|
||||
resources :categories, only: [ :create, :destroy ]
|
||||
resources :completions
|
||||
resources :contestants
|
||||
resources :puzzles
|
||||
resources :messages, only: :destroy do
|
||||
get "convert", to: "messages#convert"
|
||||
end
|
||||
get "import", to: "contestants#import"
|
||||
post "import", to: "contestants#upload_csv"
|
||||
get "import/:id", to: "contestants#convert_csv"
|
||||
post "import/:id", to: "contestants#finalize_import"
|
||||
end
|
||||
resources :passwords, param: :token
|
||||
resource :session
|
||||
resources :users
|
||||
|
||||
options "connect", to: "messages#cors_preflight_check"
|
||||
options "message", to: "messages#cors_preflight_check"
|
||||
post "connect", to: "messages#connect"
|
||||
post "message", to: "messages#create"
|
||||
|
||||
get "public/:id", to: "contests#scoreboard"
|
||||
end
|
||||
|
@@ -0,0 +1,6 @@
|
||||
class AddDisplayTimesToCompletions < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :completions, :display_time_from_start, :string
|
||||
add_column :completions, :display_relative_time, :string
|
||||
end
|
||||
end
|
@@ -0,0 +1,5 @@
|
||||
class AddDisplayTimeToContestants < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :contestants, :display_time, :string
|
||||
end
|
||||
end
|
5
db/migrate/20250326162646_remove_slug_from_contests.rb
Normal file
5
db/migrate/20250326162646_remove_slug_from_contests.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class RemoveSlugFromContests < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
remove_column :contests, :slug, :string
|
||||
end
|
||||
end
|
6
db/migrate/20250326162828_add_slug_to_contests.rb
Normal file
6
db/migrate/20250326162828_add_slug_to_contests.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
class AddSlugToContests < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :contests, :slug, :string
|
||||
add_index :contests, :slug, unique: true
|
||||
end
|
||||
end
|
21
db/migrate/20250326162920_create_friendly_id_slugs.rb
Normal file
21
db/migrate/20250326162920_create_friendly_id_slugs.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
MIGRATION_CLASS =
|
||||
if ActiveRecord::VERSION::MAJOR >= 5
|
||||
ActiveRecord::Migration["#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"]
|
||||
else
|
||||
ActiveRecord::Migration
|
||||
end
|
||||
|
||||
class CreateFriendlyIdSlugs < MIGRATION_CLASS
|
||||
def change
|
||||
create_table :friendly_id_slugs do |t|
|
||||
t.string :slug, null: false
|
||||
t.integer :sluggable_id, null: false
|
||||
t.string :sluggable_type, limit: 50
|
||||
t.string :scope
|
||||
t.datetime :created_at
|
||||
end
|
||||
add_index :friendly_id_slugs, [ :sluggable_type, :sluggable_id ]
|
||||
add_index :friendly_id_slugs, [ :slug, :sluggable_type ], length: { slug: 140, sluggable_type: 50 }
|
||||
add_index :friendly_id_slugs, [ :slug, :sluggable_type, :scope ], length: { slug: 70, sluggable_type: 50, scope: 70 }, unique: true
|
||||
end
|
||||
end
|
5
db/migrate/20250327111835_add_lang_to_user.rb
Normal file
5
db/migrate/20250327111835_add_lang_to_user.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class AddLangToUser < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :users, :lang, :string, default: 'en'
|
||||
end
|
||||
end
|
11
db/migrate/20250511173749_create_messages.rb
Normal file
11
db/migrate/20250511173749_create_messages.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
class CreateMessages < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :messages do |t|
|
||||
t.integer :time_seconds, null: false
|
||||
t.belongs_to :contest, null: false, foreign_key: true
|
||||
t.string :text, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
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
|
60
db/schema.rb
generated
60
db/schema.rb
generated
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_03_22_164205) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_07_14_115208) do
|
||||
create_table "active_storage_attachments", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "record_type", null: false
|
||||
@@ -39,6 +39,21 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_22_164205) do
|
||||
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
|
||||
end
|
||||
|
||||
create_table "categories", force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.integer "contest_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["contest_id"], name: "index_categories_on_contest_id"
|
||||
end
|
||||
|
||||
create_table "categories_contestants", id: false, force: :cascade do |t|
|
||||
t.integer "category_id", null: false
|
||||
t.integer "contestant_id", null: false
|
||||
t.index ["category_id"], name: "index_categories_contestants_on_category_id"
|
||||
t.index ["contestant_id"], name: "index_categories_contestants_on_contestant_id"
|
||||
end
|
||||
|
||||
create_table "completions", force: :cascade do |t|
|
||||
t.integer "time_seconds"
|
||||
t.integer "contestant_id", null: false
|
||||
@@ -46,8 +61,12 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_22_164205) do
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "contest_id", null: false
|
||||
t.string "display_time_from_start"
|
||||
t.string "display_relative_time"
|
||||
t.integer "message_id"
|
||||
t.index ["contest_id"], name: "index_completions_on_contest_id"
|
||||
t.index ["contestant_id"], name: "index_completions_on_contestant_id"
|
||||
t.index ["message_id"], name: "index_completions_on_message_id"
|
||||
t.index ["puzzle_id"], name: "index_completions_on_puzzle_id"
|
||||
end
|
||||
|
||||
@@ -57,6 +76,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_22_164205) do
|
||||
t.integer "contest_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "display_time"
|
||||
t.integer "time_seconds"
|
||||
t.index ["contest_id"], name: "index_contestants_on_contest_id"
|
||||
end
|
||||
|
||||
@@ -68,15 +89,48 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_22_164205) do
|
||||
t.boolean "team", default: false
|
||||
t.boolean "allow_registration", default: false
|
||||
t.string "slug"
|
||||
t.string "lang", default: "en"
|
||||
t.boolean "public", default: false
|
||||
t.index ["slug"], name: "index_contests_on_slug", unique: true
|
||||
t.index ["user_id"], name: "index_contests_on_user_id"
|
||||
end
|
||||
|
||||
create_table "csv_imports", force: :cascade do |t|
|
||||
t.string "separator", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "content", null: false
|
||||
end
|
||||
|
||||
create_table "friendly_id_slugs", force: :cascade do |t|
|
||||
t.string "slug", null: false
|
||||
t.integer "sluggable_id", null: false
|
||||
t.string "sluggable_type", limit: 50
|
||||
t.string "scope"
|
||||
t.datetime "created_at"
|
||||
t.index ["slug", "sluggable_type", "scope"], name: "index_friendly_id_slugs_on_slug_and_sluggable_type_and_scope", unique: true
|
||||
t.index ["slug", "sluggable_type"], name: "index_friendly_id_slugs_on_slug_and_sluggable_type"
|
||||
t.index ["sluggable_type", "sluggable_id"], name: "index_friendly_id_slugs_on_sluggable_type_and_sluggable_id"
|
||||
end
|
||||
|
||||
create_table "messages", force: :cascade do |t|
|
||||
t.integer "time_seconds", null: false
|
||||
t.integer "contest_id", null: false
|
||||
t.string "text", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "author"
|
||||
t.string "display_time"
|
||||
t.index ["contest_id"], name: "index_messages_on_contest_id"
|
||||
end
|
||||
|
||||
create_table "puzzles", force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "contest_id", null: false
|
||||
t.string "brand"
|
||||
t.integer "pieces", null: false
|
||||
t.index ["contest_id"], name: "index_puzzles_on_contest_id"
|
||||
end
|
||||
|
||||
@@ -96,16 +150,20 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_22_164205) do
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "username"
|
||||
t.boolean "admin", default: false, null: false
|
||||
t.string "lang", default: "en"
|
||||
t.index ["email_address"], name: "index_users_on_email_address", unique: true
|
||||
end
|
||||
|
||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "categories", "contests"
|
||||
add_foreign_key "completions", "contestants"
|
||||
add_foreign_key "completions", "contests"
|
||||
add_foreign_key "completions", "messages"
|
||||
add_foreign_key "completions", "puzzles"
|
||||
add_foreign_key "contestants", "contests"
|
||||
add_foreign_key "contests", "users"
|
||||
add_foreign_key "messages", "contests"
|
||||
add_foreign_key "puzzles", "contests"
|
||||
add_foreign_key "sessions", "users"
|
||||
end
|
||||
|
23
spec/factories/categories.rb
Normal file
23
spec/factories/categories.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: categories
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# name :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# contest_id :integer not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_categories_on_contest_id (contest_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# contest_id (contest_id => contests.id)
|
||||
#
|
||||
FactoryBot.define do
|
||||
factory :category do
|
||||
name { "MyString" }
|
||||
end
|
||||
end
|
@@ -4,7 +4,9 @@
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# allow_registration :boolean default(FALSE)
|
||||
# lang :string default("en")
|
||||
# name :string
|
||||
# public :boolean default(FALSE)
|
||||
# slug :string
|
||||
# team :boolean default(FALSE)
|
||||
# created_at :datetime not null
|
||||
@@ -13,16 +15,15 @@
|
||||
#
|
||||
# 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
|
||||
FactoryBot.define do
|
||||
factory :contest do
|
||||
name { Faker::Company.unique.name }
|
||||
end
|
||||
end
|
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
|
28
spec/factories/messages.rb
Normal file
28
spec/factories/messages.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: messages
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# author :string
|
||||
# display_time :string
|
||||
# text :string not null
|
||||
# time_seconds :integer not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# contest_id :integer not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_messages_on_contest_id (contest_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# contest_id (contest_id => contests.id)
|
||||
#
|
||||
FactoryBot.define do
|
||||
factory :message do
|
||||
time_seconds { 1 }
|
||||
contest { nil }
|
||||
text { "MyString" }
|
||||
end
|
||||
end
|
@@ -1,6 +1,3 @@
|
||||
<% admin_password_digest = BCrypt::Password.create("admin") %>
|
||||
<% user_password_digest = BCrypt::Password.create("user") %>
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: users
|
||||
@@ -8,6 +5,7 @@
|
||||
# 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
|
||||
@@ -17,14 +15,14 @@
|
||||
#
|
||||
# index_users_on_email_address (email_address) UNIQUE
|
||||
#
|
||||
admin_user:
|
||||
email_address: admin@admin.org
|
||||
password_digest: <%= admin_password_digest %>
|
||||
username: admin
|
||||
admin: true
|
||||
FactoryBot.define do
|
||||
factory :user do
|
||||
username { Faker::Internet.unique.username }
|
||||
email_address { Faker::Internet.unique.email }
|
||||
password { Faker::Internet.unique.password(min_length: 12, max_length: 18) }
|
||||
end
|
||||
|
||||
user:
|
||||
email_address: user@user.org
|
||||
password_digest: <%= user_password_digest %>
|
||||
username: user
|
||||
admin: false
|
||||
trait :admin do
|
||||
admin { true }
|
||||
end
|
||||
end
|
78
spec/features/contest_spec.rb
Normal file
78
spec/features/contest_spec.rb
Normal file
@@ -0,0 +1,78 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.feature "Contests", type: :feature do
|
||||
let!(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
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
|
||||
|
||||
it "should offer to create a new contest" do
|
||||
visit contests_path
|
||||
|
||||
click_link I18n.t("contests.index.new_contest")
|
||||
|
||||
expect(page).to have_current_path("/contests/new")
|
||||
end
|
||||
end
|
||||
|
||||
context "new" do
|
||||
it "should prevent creating contests without a name" do
|
||||
visit new_contest_path
|
||||
|
||||
click_button I18n.t("helpers.buttons.create")
|
||||
|
||||
expect(page).to have_content(I18n.t("activerecord.errors.models.contest.attributes.name.blank"))
|
||||
end
|
||||
|
||||
it "should allow creating new contests with valid parameters" do
|
||||
visit new_contest_path
|
||||
|
||||
fill_in I18n.t("activerecord.attributes.contest.name"), with: "Contest name"
|
||||
|
||||
click_button I18n.t("helpers.buttons.create")
|
||||
|
||||
expect(page).to have_current_path(contest_path(Contest.find_by(name: "Contest name")))
|
||||
end
|
||||
end
|
||||
|
||||
context "edit" do
|
||||
let!(:contest) { create(:contest, user: user) }
|
||||
|
||||
it "should prevent editing contests without a name" do
|
||||
visit edit_contest_path(contest)
|
||||
|
||||
fill_in I18n.t("activerecord.attributes.contest.name"), with: ""
|
||||
|
||||
click_button I18n.t("helpers.buttons.save")
|
||||
|
||||
expect(page).to have_content(I18n.t("activerecord.errors.models.contest.attributes.name.blank"))
|
||||
end
|
||||
|
||||
it "should allow editing contests with valid parameters" do
|
||||
visit edit_contest_path(contest)
|
||||
|
||||
fill_in I18n.t("activerecord.attributes.contest.name"), with: "Contest name"
|
||||
|
||||
click_button I18n.t("helpers.buttons.save")
|
||||
|
||||
expect(page).to have_current_path(contest_path(contest))
|
||||
end
|
||||
end
|
||||
end
|
34
spec/features/login_spec.rb
Normal file
34
spec/features/login_spec.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.feature "Login", type: :feature do
|
||||
context "visiting the login page" do
|
||||
let!(:user) { create(:user) }
|
||||
|
||||
it "should log in the user with the correct credentials" do
|
||||
visit '/'
|
||||
fill_in "Email address", with: user.email_address
|
||||
fill_in "Password", with: user.password
|
||||
click_button "Sign in"
|
||||
|
||||
expect(page).not_to have_content(I18n.t("sessions.new.title"))
|
||||
end
|
||||
|
||||
it "should fail to log in the user with an incorrect email address" do
|
||||
visit '/'
|
||||
fill_in "Email address", with: Faker::Internet.unique.email
|
||||
fill_in "Password", with: user.password
|
||||
click_button "Sign in"
|
||||
|
||||
expect(page).to have_content(I18n.t("sessions.new.title"))
|
||||
end
|
||||
|
||||
it "should fail to log in the user with an incorrect password" do
|
||||
visit '/'
|
||||
fill_in "Email address", with: user.email_address
|
||||
fill_in "Password", with: Faker::Internet.unique.password
|
||||
click_button "Sign in"
|
||||
|
||||
expect(page).to have_content(I18n.t("sessions.new.title"))
|
||||
end
|
||||
end
|
||||
end
|
63
spec/features/user_spec.rb
Normal file
63
spec/features/user_spec.rb
Normal file
@@ -0,0 +1,63 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.feature "Users", type: :feature do
|
||||
context "when the user is a regular user" do
|
||||
let!(:user) { create(:user) }
|
||||
let!(:contest) { create(:contest, user: user) }
|
||||
|
||||
before do
|
||||
login(user)
|
||||
end
|
||||
|
||||
it "should not see a link to all users" do
|
||||
visit root_path
|
||||
|
||||
expect(page).not_to have_content(I18n.t("nav.users"))
|
||||
end
|
||||
|
||||
it "should not be able to see the user list" do
|
||||
visit users_path
|
||||
|
||||
expect(page).not_to have_content(I18n.t("users.index.title"))
|
||||
end
|
||||
|
||||
it "should be able to create a new contest" do
|
||||
visit root_path
|
||||
|
||||
click_link "Create a new contest"
|
||||
|
||||
expect(page).to have_content(I18n.t("contests.new.title"))
|
||||
end
|
||||
|
||||
it "should be able to open an existing contest" do
|
||||
visit root_path
|
||||
|
||||
expect(page).to have_content(contest.name)
|
||||
find("div.card", text: contest.name).find("a").click
|
||||
|
||||
expect(page).to have_content(I18n.t("contests.show.title", name: contest.name))
|
||||
end
|
||||
end
|
||||
|
||||
context "when the user is an admin" do
|
||||
let!(:admin) { create(:user, :admin) }
|
||||
let!(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
login(admin)
|
||||
end
|
||||
|
||||
it "should see a link to all users" do
|
||||
visit root_path
|
||||
|
||||
expect(page).to have_content(I18n.t("nav.users"))
|
||||
end
|
||||
|
||||
it "should be able to see the user list" do
|
||||
visit users_path
|
||||
|
||||
expect(page).to have_content(I18n.t("users.index.title"))
|
||||
expect(page).to have_content(user.username)
|
||||
end
|
||||
end
|
||||
end
|
@@ -1,9 +1,8 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: contestants
|
||||
# Table name: categories
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# email :string
|
||||
# name :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
@@ -11,16 +10,14 @@
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_contestants_on_contest_id (contest_id)
|
||||
# index_categories_on_contest_id (contest_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# contest_id (contest_id => contests.id)
|
||||
#
|
||||
require "test_helper"
|
||||
require 'rails_helper'
|
||||
|
||||
class ContestantTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
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
|
26
spec/models/message_spec.rb
Normal file
26
spec/models/message_spec.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: messages
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# author :string
|
||||
# display_time :string
|
||||
# text :string not null
|
||||
# time_seconds :integer not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# contest_id :integer not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_messages_on_contest_id (contest_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# contest_id (contest_id => contests.id)
|
||||
#
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Message, type: :model do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
@@ -5,6 +5,7 @@
|
||||
# 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
|
||||
@@ -14,10 +15,12 @@
|
||||
#
|
||||
# index_users_on_email_address (email_address) UNIQUE
|
||||
#
|
||||
require "test_helper"
|
||||
require 'rails_helper'
|
||||
|
||||
class UserTest < ActiveSupport::TestCase
|
||||
test "the truth" do
|
||||
assert true
|
||||
RSpec.describe User, type: :model do
|
||||
context "1" do
|
||||
it "equals 1" do
|
||||
expect(1).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
70
spec/rails_helper.rb
Normal file
70
spec/rails_helper.rb
Normal file
@@ -0,0 +1,70 @@
|
||||
# This file is copied to spec/ when you run 'rails generate rspec:install'
|
||||
require 'spec_helper'
|
||||
ENV['RAILS_ENV'] ||= 'test'
|
||||
require_relative '../config/environment'
|
||||
# Prevent database truncation if the environment is production
|
||||
abort("The Rails environment is running in production mode!") if Rails.env.production?
|
||||
# Uncomment the line below in case you have `--require rails_helper` in the `.rspec` file
|
||||
# that will avoid rails generators crashing because migrations haven't been run yet
|
||||
# return unless Rails.env.test?
|
||||
require 'rspec/rails'
|
||||
# Add additional requires below this line. Rails is not loaded until this point!
|
||||
|
||||
# Requires supporting ruby files with custom matchers and macros, etc, in
|
||||
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
|
||||
# run as spec files by default. This means that files in spec/support that end
|
||||
# in _spec.rb will both be required and run as specs, causing the specs to be
|
||||
# run twice. It is recommended that you do not name files matching this glob to
|
||||
# end with _spec.rb. You can configure this pattern with the --pattern
|
||||
# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
|
||||
#
|
||||
# The following line is provided for convenience purposes. It has the downside
|
||||
# of increasing the boot-up time by auto-requiring all files in the support
|
||||
# directory. Alternatively, in the individual `*_spec.rb` files, manually
|
||||
# require only the support files necessary.
|
||||
#
|
||||
Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f }
|
||||
|
||||
# Checks for pending migrations and applies them before tests are run.
|
||||
# If you are not using ActiveRecord, you can remove these lines.
|
||||
begin
|
||||
ActiveRecord::Migration.maintain_test_schema!
|
||||
rescue ActiveRecord::PendingMigrationError => e
|
||||
abort e.to_s.strip
|
||||
end
|
||||
RSpec.configure do |config|
|
||||
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
|
||||
config.fixture_paths = [
|
||||
Rails.root.join('spec/fixtures')
|
||||
]
|
||||
|
||||
# If you're not using ActiveRecord, or you'd prefer not to run each of your
|
||||
# examples within a transaction, remove the following line or assign false
|
||||
# instead of true.
|
||||
config.use_transactional_fixtures = true
|
||||
|
||||
# You can uncomment this line to turn off ActiveRecord support entirely.
|
||||
# config.use_active_record = false
|
||||
|
||||
# RSpec Rails uses metadata to mix in different behaviours to your tests,
|
||||
# for example enabling you to call `get` and `post` in request specs. e.g.:
|
||||
#
|
||||
# RSpec.describe UsersController, type: :request do
|
||||
# # ...
|
||||
# end
|
||||
#
|
||||
# The different available types are documented in the features, such as in
|
||||
# https://rspec.info/features/7-1/rspec-rails
|
||||
#
|
||||
# You can also this infer these behaviours automatically by location, e.g.
|
||||
# /spec/models would pull in the same behaviour as `type: :model` but this
|
||||
# behaviour is considered legacy and will be removed in a future version.
|
||||
#
|
||||
# To enable this behaviour uncomment the line below.
|
||||
# config.infer_spec_type_from_file_location!
|
||||
|
||||
# Filter lines from Rails gems in backtraces.
|
||||
config.filter_rails_from_backtrace!
|
||||
# arbitrary gems may also be filtered via:
|
||||
# config.filter_gems_from_backtrace("gem name")
|
||||
end
|
94
spec/spec_helper.rb
Normal file
94
spec/spec_helper.rb
Normal file
@@ -0,0 +1,94 @@
|
||||
# This file was generated by the `rails generate rspec:install` command. Conventionally, all
|
||||
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
||||
# The generated `.rspec` file contains `--require spec_helper` which will cause
|
||||
# this file to always be loaded, without a need to explicitly require it in any
|
||||
# files.
|
||||
#
|
||||
# Given that it is always loaded, you are encouraged to keep this file as
|
||||
# light-weight as possible. Requiring heavyweight dependencies from this file
|
||||
# will add to the boot time of your test suite on EVERY test run, even for an
|
||||
# individual file that may not need all of that loaded. Instead, consider making
|
||||
# a separate helper file that requires the additional dependencies and performs
|
||||
# the additional setup, and require it from the spec files that actually need
|
||||
# it.
|
||||
#
|
||||
# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
||||
RSpec.configure do |config|
|
||||
# rspec-expectations config goes here. You can use an alternate
|
||||
# assertion/expectation library such as wrong or the stdlib/minitest
|
||||
# assertions if you prefer.
|
||||
config.expect_with :rspec do |expectations|
|
||||
# This option will default to `true` in RSpec 4. It makes the `description`
|
||||
# and `failure_message` of custom matchers include text for helper methods
|
||||
# defined using `chain`, e.g.:
|
||||
# be_bigger_than(2).and_smaller_than(4).description
|
||||
# # => "be bigger than 2 and smaller than 4"
|
||||
# ...rather than:
|
||||
# # => "be bigger than 2"
|
||||
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
||||
end
|
||||
|
||||
# rspec-mocks config goes here. You can use an alternate test double
|
||||
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
||||
config.mock_with :rspec do |mocks|
|
||||
# Prevents you from mocking or stubbing a method that does not exist on
|
||||
# a real object. This is generally recommended, and will default to
|
||||
# `true` in RSpec 4.
|
||||
mocks.verify_partial_doubles = true
|
||||
end
|
||||
|
||||
# This option will default to `:apply_to_host_groups` in RSpec 4 (and will
|
||||
# have no way to turn it off -- the option exists only for backwards
|
||||
# compatibility in RSpec 3). It causes shared context metadata to be
|
||||
# inherited by the metadata hash of host groups and examples, rather than
|
||||
# triggering implicit auto-inclusion in groups with matching metadata.
|
||||
config.shared_context_metadata_behavior = :apply_to_host_groups
|
||||
|
||||
# The settings below are suggested to provide a good initial experience
|
||||
# with RSpec, but feel free to customize to your heart's content.
|
||||
=begin
|
||||
# This allows you to limit a spec run to individual examples or groups
|
||||
# you care about by tagging them with `:focus` metadata. When nothing
|
||||
# is tagged with `:focus`, all examples get run. RSpec also provides
|
||||
# aliases for `it`, `describe`, and `context` that include `:focus`
|
||||
# metadata: `fit`, `fdescribe` and `fcontext`, respectively.
|
||||
config.filter_run_when_matching :focus
|
||||
|
||||
# Allows RSpec to persist some state between runs in order to support
|
||||
# the `--only-failures` and `--next-failure` CLI options. We recommend
|
||||
# you configure your source control system to ignore this file.
|
||||
config.example_status_persistence_file_path = "spec/examples.txt"
|
||||
|
||||
# Limits the available syntax to the non-monkey patched syntax that is
|
||||
# recommended. For more details, see:
|
||||
# https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/
|
||||
config.disable_monkey_patching!
|
||||
|
||||
# Many RSpec users commonly either run the entire suite or an individual
|
||||
# file, and it's useful to allow more verbose output when running an
|
||||
# individual spec file.
|
||||
if config.files_to_run.one?
|
||||
# Use the documentation formatter for detailed output,
|
||||
# unless a formatter has already been configured
|
||||
# (e.g. via a command-line flag).
|
||||
config.default_formatter = "doc"
|
||||
end
|
||||
|
||||
# Print the 10 slowest examples and example groups at the
|
||||
# end of the spec run, to help surface which specs are running
|
||||
# particularly slow.
|
||||
config.profile_examples = 10
|
||||
|
||||
# Run specs in random order to surface order dependencies. If you find an
|
||||
# order dependency and want to debug it, you can fix the order by providing
|
||||
# the seed, which is printed after each run.
|
||||
# --seed 1234
|
||||
config.order = :random
|
||||
|
||||
# Seed global randomization in this process using the `--seed` CLI option.
|
||||
# Setting this allows you to use `--seed` to deterministically reproduce
|
||||
# test failures related to randomization by passing the same `--seed` value
|
||||
# as the one that triggered the failure.
|
||||
Kernel.srand config.seed
|
||||
=end
|
||||
end
|
5
spec/support/factory_bot.rb
Normal file
5
spec/support/factory_bot.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
require "factory_bot_rails"
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.include FactoryBot::Syntax::Methods
|
||||
end
|
12
spec/support/helpers.rb
Normal file
12
spec/support/helpers.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
module Helpers
|
||||
def login(user)
|
||||
visit '/'
|
||||
fill_in "Email address", with: user.email_address
|
||||
fill_in "Password", with: user.password
|
||||
click_button "Sign in"
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.configure do |c|
|
||||
c.include Helpers
|
||||
end
|
@@ -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
|
37
test/fixtures/completions.yml
vendored
37
test/fixtures/completions.yml
vendored
@@ -1,37 +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
|
||||
# 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
|
32
test/fixtures/contestants.yml
vendored
32
test/fixtures/contestants.yml
vendored
@@ -1,32 +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
|
||||
# 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
|
35
test/fixtures/contests.yml
vendored
35
test/fixtures/contests.yml
vendored
@@ -1,35 +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_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
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user