Compare commits
	
		
			109 Commits
		
	
	
		
			4b3bc58474
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d62b46b7df | |||
| 8d50c14a7b | |||
| aeb6989223 | |||
| ff5f387a87 | |||
| 37a65526e4 | |||
| aea001cdf6 | |||
| 35ad7da355 | |||
| a8f1ffd920 | |||
| 7db96cfab4 | |||
| bbd2cef168 | |||
| 1fa7bf10ec | |||
| 916c7af738 | |||
| 537f32ab8b | |||
| 5b9862c19c | |||
| 4ca711f5aa | |||
| b13ef30807 | |||
| 657c5ac47b | |||
| 502649620b | |||
| ee476ab81b | |||
| 0599def237 | |||
| b6da55723d | |||
| 9862f0c74b | |||
| 1b34d10dee | |||
| d28f888ee2 | |||
| 2b1a2c9296 | |||
| 1a8ea0afee | |||
| 2cadc8eca5 | |||
| c34b9654c8 | |||
| 341e626f6f | |||
| c22b529858 | |||
| 50050064c2 | |||
| 5aa69a108c | |||
| ef3c63ea67 | |||
| 6fb5ba5f3e | |||
| 6c16e5e232 | |||
| 2969a24cb0 | |||
| 4b5c09f63b | |||
| ca7399f490 | |||
| f27b43ef45 | |||
| 5b908fe37c | |||
| 2616cbaa71 | |||
| 70c0fed0c4 | |||
| 6c0f5167a4 | |||
| ac3b354480 | |||
| 71f2bb6b70 | |||
| ac83a599f3 | |||
| 67492cdd15 | |||
| 79fb1edfaf | |||
| 4645b45f5d | |||
| f78a082ad3 | |||
| b8674a126f | |||
| 67d2ef41b3 | |||
| 96b8553b1f | |||
| 194c126c90 | |||
| a33f3ff4de | |||
| 17a1af4e9f | |||
| baea71b312 | |||
| bc32387c21 | |||
| 55399d80fe | |||
| d7d90f0c91 | |||
| 7444a09046 | |||
| ec2201f9a8 | |||
| 939e2157ab | |||
| 5ec0e264ba | |||
| c4902d85d5 | |||
| e65d639ca6 | |||
| 1397ddce2f | |||
| 138fe67baa | |||
| 3a8517e637 | |||
| 6afde8a971 | |||
| 70005468c6 | |||
| 2f23938e81 | |||
| 378c3011ef | |||
| a421cd496d | |||
| 21f71f9d32 | |||
| 10fa821f19 | |||
| 8b0b1c6745 | |||
| 497768610d | |||
| 26b8064553 | |||
| 7023600cd1 | |||
| 12f9f33034 | |||
| 2144c22bd9 | |||
| a5d165c4b3 | |||
| c98caeea92 | |||
| f8bfb020bc | |||
| 14be4a32e6 | |||
| 7ce684ced9 | |||
| 5525cc814a | |||
| 2982f44acc | |||
| 9a2a3a6f33 | |||
| d47ebf22ab | |||
| 6b02eecb9b | |||
| 5472a400d1 | |||
| 0b47cc4d8a | |||
| ce5b729fef | |||
| 884dbf40d9 | |||
| 570e517c28 | |||
| 15e2493f87 | |||
| ea7cdcf608 | |||
| a03907f756 | |||
| 44507bb85c | |||
| 658c50fd04 | |||
| 5339a864c0 | |||
| 4d32f9e7f0 | |||
| 6f07ec802f | |||
| 785e523ebe | |||
| 7ec51b6d85 | |||
| 0cbd2e4fdc | |||
| eca2e46d23 | 
							
								
								
									
										58
									
								
								.annotaterb.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								.annotaterb.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
				
			|||||||
 | 
					---
 | 
				
			||||||
 | 
					:position: before
 | 
				
			||||||
 | 
					:position_in_additional_file_patterns: before
 | 
				
			||||||
 | 
					:position_in_class: before
 | 
				
			||||||
 | 
					:position_in_factory: before
 | 
				
			||||||
 | 
					:position_in_fixture: before
 | 
				
			||||||
 | 
					:position_in_routes: before
 | 
				
			||||||
 | 
					:position_in_serializer: before
 | 
				
			||||||
 | 
					:position_in_test: before
 | 
				
			||||||
 | 
					:classified_sort: true
 | 
				
			||||||
 | 
					:exclude_controllers: true
 | 
				
			||||||
 | 
					:exclude_factories: false
 | 
				
			||||||
 | 
					:exclude_fixtures: false
 | 
				
			||||||
 | 
					:exclude_helpers: true
 | 
				
			||||||
 | 
					:exclude_scaffolds: true
 | 
				
			||||||
 | 
					:exclude_serializers: false
 | 
				
			||||||
 | 
					:exclude_sti_subclasses: false
 | 
				
			||||||
 | 
					:exclude_tests: false
 | 
				
			||||||
 | 
					:force: false
 | 
				
			||||||
 | 
					:format_markdown: false
 | 
				
			||||||
 | 
					:format_rdoc: false
 | 
				
			||||||
 | 
					:format_yard: false
 | 
				
			||||||
 | 
					:frozen: false
 | 
				
			||||||
 | 
					:ignore_model_sub_dir: false
 | 
				
			||||||
 | 
					:ignore_unknown_models: false
 | 
				
			||||||
 | 
					:include_version: false
 | 
				
			||||||
 | 
					:show_check_constraints: false
 | 
				
			||||||
 | 
					:show_complete_foreign_keys: false
 | 
				
			||||||
 | 
					:show_foreign_keys: true
 | 
				
			||||||
 | 
					:show_indexes: true
 | 
				
			||||||
 | 
					:simple_indexes: false
 | 
				
			||||||
 | 
					:sort: false
 | 
				
			||||||
 | 
					:timestamp: false
 | 
				
			||||||
 | 
					:trace: false
 | 
				
			||||||
 | 
					:with_comment: true
 | 
				
			||||||
 | 
					:with_column_comments: true
 | 
				
			||||||
 | 
					:with_table_comments: true
 | 
				
			||||||
 | 
					:active_admin: false
 | 
				
			||||||
 | 
					:command:
 | 
				
			||||||
 | 
					:debug: false
 | 
				
			||||||
 | 
					:hide_default_column_types: ''
 | 
				
			||||||
 | 
					:hide_limit_column_types: ''
 | 
				
			||||||
 | 
					:ignore_columns:
 | 
				
			||||||
 | 
					:ignore_routes:
 | 
				
			||||||
 | 
					:models: true
 | 
				
			||||||
 | 
					:routes: false
 | 
				
			||||||
 | 
					:skip_on_db_migrate: false
 | 
				
			||||||
 | 
					:target_action: :do_annotations
 | 
				
			||||||
 | 
					:wrapper:
 | 
				
			||||||
 | 
					:wrapper_close:
 | 
				
			||||||
 | 
					:wrapper_open:
 | 
				
			||||||
 | 
					:classes_default_to_s: []
 | 
				
			||||||
 | 
					:additional_file_patterns: []
 | 
				
			||||||
 | 
					:model_dir:
 | 
				
			||||||
 | 
					- app/models
 | 
				
			||||||
 | 
					:require: []
 | 
				
			||||||
 | 
					:root_dir:
 | 
				
			||||||
 | 
					- ''
 | 
				
			||||||
@@ -56,8 +56,9 @@ jobs:
 | 
				
			|||||||
  test:
 | 
					  test:
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Install packages
 | 
					      - name: Install packages
 | 
				
			||||||
        run: sudo apt-get update && sudo apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config google-chrome-stable
 | 
					        run: sudo apt-get update && sudo apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Checkout code
 | 
					      - name: Checkout code
 | 
				
			||||||
        uses: actions/checkout@v4
 | 
					        uses: actions/checkout@v4
 | 
				
			||||||
@@ -68,16 +69,12 @@ jobs:
 | 
				
			|||||||
          ruby-version: .ruby-version
 | 
					          ruby-version: .ruby-version
 | 
				
			||||||
          bundler-cache: true
 | 
					          bundler-cache: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Run tests
 | 
					      - name: Setup test database
 | 
				
			||||||
        env:
 | 
					        env:
 | 
				
			||||||
          RAILS_ENV: test
 | 
					          RAILS_ENV: test
 | 
				
			||||||
          # REDIS_URL: redis://localhost:6379/0
 | 
					        run: bin/rails db:test:prepare
 | 
				
			||||||
        run: bin/rails db:test:prepare test test:system
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Keep screenshots from failed system tests
 | 
					      - name: Run rspec
 | 
				
			||||||
        uses: actions/upload-artifact@v4
 | 
					        env:
 | 
				
			||||||
        if: failure()
 | 
					          RAILS_ENV: test
 | 
				
			||||||
        with:
 | 
					        run: bundle exec rspec
 | 
				
			||||||
          name: screenshots
 | 
					 | 
				
			||||||
          path: ${{ github.workspace }}/tmp/screenshots
 | 
					 | 
				
			||||||
          if-no-files-found: ignore
 | 
					 | 
				
			||||||
							
								
								
									
										12
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,12 +0,0 @@
 | 
				
			|||||||
version: 2
 | 
					 | 
				
			||||||
updates:
 | 
					 | 
				
			||||||
- package-ecosystem: bundler
 | 
					 | 
				
			||||||
  directory: "/"
 | 
					 | 
				
			||||||
  schedule:
 | 
					 | 
				
			||||||
    interval: daily
 | 
					 | 
				
			||||||
  open-pull-requests-limit: 10
 | 
					 | 
				
			||||||
- package-ecosystem: github-actions
 | 
					 | 
				
			||||||
  directory: "/"
 | 
					 | 
				
			||||||
  schedule:
 | 
					 | 
				
			||||||
    interval: daily
 | 
					 | 
				
			||||||
  open-pull-requests-limit: 10
 | 
					 | 
				
			||||||
@@ -48,9 +48,6 @@ RUN bundle exec bootsnap precompile app/ lib/
 | 
				
			|||||||
# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
 | 
					# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
 | 
				
			||||||
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
 | 
					RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Final stage for app image
 | 
					# Final stage for app image
 | 
				
			||||||
FROM base
 | 
					FROM base
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -58,6 +55,9 @@ FROM base
 | 
				
			|||||||
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
 | 
					COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
 | 
				
			||||||
COPY --from=build /rails /rails
 | 
					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 and own only the runtime files as a non-root user for security
 | 
				
			||||||
RUN groupadd --system --gid 1000 rails && \
 | 
					RUN groupadd --system --gid 1000 rails && \
 | 
				
			||||||
    useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
 | 
					    useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										11
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								Gemfile
									
									
									
									
									
								
							@@ -43,6 +43,9 @@ gem "thruster", require: false
 | 
				
			|||||||
gem "slim"
 | 
					gem "slim"
 | 
				
			||||||
gem "dartsass-rails"
 | 
					gem "dartsass-rails"
 | 
				
			||||||
gem "bootstrap", "~> 5.3.3"
 | 
					gem "bootstrap", "~> 5.3.3"
 | 
				
			||||||
 | 
					gem "friendly_id", "~> 5.5.0"
 | 
				
			||||||
 | 
					gem "csv"
 | 
				
			||||||
 | 
					gem "damerau-levenshtein"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
group :development, :test do
 | 
					group :development, :test do
 | 
				
			||||||
  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
 | 
					  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
 | 
				
			||||||
@@ -53,11 +56,17 @@ group :development, :test do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
 | 
					  # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
 | 
				
			||||||
  gem "rubocop-rails-omakase", require: false
 | 
					  gem "rubocop-rails-omakase", require: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  gem "rspec-rails"
 | 
				
			||||||
 | 
					  gem "factory_bot_rails"
 | 
				
			||||||
 | 
					  gem "faker"
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
group :development do
 | 
					group :development do
 | 
				
			||||||
  # Use console on exceptions pages [https://github.com/rails/web-console]
 | 
					  # Use console on exceptions pages [https://github.com/rails/web-console]
 | 
				
			||||||
  gem "web-console"
 | 
					  gem "web-console"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  gem "annotaterb"
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
group :test do
 | 
					group :test do
 | 
				
			||||||
@@ -65,3 +74,5 @@ group :test do
 | 
				
			|||||||
  gem "capybara"
 | 
					  gem "capybara"
 | 
				
			||||||
  gem "selenium-webdriver"
 | 
					  gem "selenium-webdriver"
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					gem "pundit", "~> 2.5"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										387
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										387
									
								
								Gemfile.lock
									
									
									
									
									
								
							@@ -1,29 +1,29 @@
 | 
				
			|||||||
GEM
 | 
					GEM
 | 
				
			||||||
  remote: https://rubygems.org/
 | 
					  remote: https://rubygems.org/
 | 
				
			||||||
  specs:
 | 
					  specs:
 | 
				
			||||||
    actioncable (8.0.2)
 | 
					    actioncable (8.0.4)
 | 
				
			||||||
      actionpack (= 8.0.2)
 | 
					      actionpack (= 8.0.4)
 | 
				
			||||||
      activesupport (= 8.0.2)
 | 
					      activesupport (= 8.0.4)
 | 
				
			||||||
      nio4r (~> 2.0)
 | 
					      nio4r (~> 2.0)
 | 
				
			||||||
      websocket-driver (>= 0.6.1)
 | 
					      websocket-driver (>= 0.6.1)
 | 
				
			||||||
      zeitwerk (~> 2.6)
 | 
					      zeitwerk (~> 2.6)
 | 
				
			||||||
    actionmailbox (8.0.2)
 | 
					    actionmailbox (8.0.4)
 | 
				
			||||||
      actionpack (= 8.0.2)
 | 
					      actionpack (= 8.0.4)
 | 
				
			||||||
      activejob (= 8.0.2)
 | 
					      activejob (= 8.0.4)
 | 
				
			||||||
      activerecord (= 8.0.2)
 | 
					      activerecord (= 8.0.4)
 | 
				
			||||||
      activestorage (= 8.0.2)
 | 
					      activestorage (= 8.0.4)
 | 
				
			||||||
      activesupport (= 8.0.2)
 | 
					      activesupport (= 8.0.4)
 | 
				
			||||||
      mail (>= 2.8.0)
 | 
					      mail (>= 2.8.0)
 | 
				
			||||||
    actionmailer (8.0.2)
 | 
					    actionmailer (8.0.4)
 | 
				
			||||||
      actionpack (= 8.0.2)
 | 
					      actionpack (= 8.0.4)
 | 
				
			||||||
      actionview (= 8.0.2)
 | 
					      actionview (= 8.0.4)
 | 
				
			||||||
      activejob (= 8.0.2)
 | 
					      activejob (= 8.0.4)
 | 
				
			||||||
      activesupport (= 8.0.2)
 | 
					      activesupport (= 8.0.4)
 | 
				
			||||||
      mail (>= 2.8.0)
 | 
					      mail (>= 2.8.0)
 | 
				
			||||||
      rails-dom-testing (~> 2.2)
 | 
					      rails-dom-testing (~> 2.2)
 | 
				
			||||||
    actionpack (8.0.2)
 | 
					    actionpack (8.0.4)
 | 
				
			||||||
      actionview (= 8.0.2)
 | 
					      actionview (= 8.0.4)
 | 
				
			||||||
      activesupport (= 8.0.2)
 | 
					      activesupport (= 8.0.4)
 | 
				
			||||||
      nokogiri (>= 1.8.5)
 | 
					      nokogiri (>= 1.8.5)
 | 
				
			||||||
      rack (>= 2.2.4)
 | 
					      rack (>= 2.2.4)
 | 
				
			||||||
      rack-session (>= 1.0.1)
 | 
					      rack-session (>= 1.0.1)
 | 
				
			||||||
@@ -31,35 +31,35 @@ GEM
 | 
				
			|||||||
      rails-dom-testing (~> 2.2)
 | 
					      rails-dom-testing (~> 2.2)
 | 
				
			||||||
      rails-html-sanitizer (~> 1.6)
 | 
					      rails-html-sanitizer (~> 1.6)
 | 
				
			||||||
      useragent (~> 0.16)
 | 
					      useragent (~> 0.16)
 | 
				
			||||||
    actiontext (8.0.2)
 | 
					    actiontext (8.0.4)
 | 
				
			||||||
      actionpack (= 8.0.2)
 | 
					      actionpack (= 8.0.4)
 | 
				
			||||||
      activerecord (= 8.0.2)
 | 
					      activerecord (= 8.0.4)
 | 
				
			||||||
      activestorage (= 8.0.2)
 | 
					      activestorage (= 8.0.4)
 | 
				
			||||||
      activesupport (= 8.0.2)
 | 
					      activesupport (= 8.0.4)
 | 
				
			||||||
      globalid (>= 0.6.0)
 | 
					      globalid (>= 0.6.0)
 | 
				
			||||||
      nokogiri (>= 1.8.5)
 | 
					      nokogiri (>= 1.8.5)
 | 
				
			||||||
    actionview (8.0.2)
 | 
					    actionview (8.0.4)
 | 
				
			||||||
      activesupport (= 8.0.2)
 | 
					      activesupport (= 8.0.4)
 | 
				
			||||||
      builder (~> 3.1)
 | 
					      builder (~> 3.1)
 | 
				
			||||||
      erubi (~> 1.11)
 | 
					      erubi (~> 1.11)
 | 
				
			||||||
      rails-dom-testing (~> 2.2)
 | 
					      rails-dom-testing (~> 2.2)
 | 
				
			||||||
      rails-html-sanitizer (~> 1.6)
 | 
					      rails-html-sanitizer (~> 1.6)
 | 
				
			||||||
    activejob (8.0.2)
 | 
					    activejob (8.0.4)
 | 
				
			||||||
      activesupport (= 8.0.2)
 | 
					      activesupport (= 8.0.4)
 | 
				
			||||||
      globalid (>= 0.3.6)
 | 
					      globalid (>= 0.3.6)
 | 
				
			||||||
    activemodel (8.0.2)
 | 
					    activemodel (8.0.4)
 | 
				
			||||||
      activesupport (= 8.0.2)
 | 
					      activesupport (= 8.0.4)
 | 
				
			||||||
    activerecord (8.0.2)
 | 
					    activerecord (8.0.4)
 | 
				
			||||||
      activemodel (= 8.0.2)
 | 
					      activemodel (= 8.0.4)
 | 
				
			||||||
      activesupport (= 8.0.2)
 | 
					      activesupport (= 8.0.4)
 | 
				
			||||||
      timeout (>= 0.4.0)
 | 
					      timeout (>= 0.4.0)
 | 
				
			||||||
    activestorage (8.0.2)
 | 
					    activestorage (8.0.4)
 | 
				
			||||||
      actionpack (= 8.0.2)
 | 
					      actionpack (= 8.0.4)
 | 
				
			||||||
      activejob (= 8.0.2)
 | 
					      activejob (= 8.0.4)
 | 
				
			||||||
      activerecord (= 8.0.2)
 | 
					      activerecord (= 8.0.4)
 | 
				
			||||||
      activesupport (= 8.0.2)
 | 
					      activesupport (= 8.0.4)
 | 
				
			||||||
      marcel (~> 1.0)
 | 
					      marcel (~> 1.0)
 | 
				
			||||||
    activesupport (8.0.2)
 | 
					    activesupport (8.0.4)
 | 
				
			||||||
      base64
 | 
					      base64
 | 
				
			||||||
      benchmark (>= 0.3)
 | 
					      benchmark (>= 0.3)
 | 
				
			||||||
      bigdecimal
 | 
					      bigdecimal
 | 
				
			||||||
@@ -74,21 +74,21 @@ GEM
 | 
				
			|||||||
      uri (>= 0.13.1)
 | 
					      uri (>= 0.13.1)
 | 
				
			||||||
    addressable (2.8.7)
 | 
					    addressable (2.8.7)
 | 
				
			||||||
      public_suffix (>= 2.0.2, < 7.0)
 | 
					      public_suffix (>= 2.0.2, < 7.0)
 | 
				
			||||||
    ast (2.4.2)
 | 
					    annotaterb (4.20.0)
 | 
				
			||||||
    autoprefixer-rails (10.4.19.0)
 | 
					      activerecord (>= 6.0.0)
 | 
				
			||||||
      execjs (~> 2)
 | 
					      activesupport (>= 6.0.0)
 | 
				
			||||||
    base64 (0.2.0)
 | 
					    ast (2.4.3)
 | 
				
			||||||
 | 
					    base64 (0.3.0)
 | 
				
			||||||
    bcrypt (3.1.20)
 | 
					    bcrypt (3.1.20)
 | 
				
			||||||
    bcrypt_pbkdf (1.1.1)
 | 
					    bcrypt_pbkdf (1.1.1)
 | 
				
			||||||
    benchmark (0.4.0)
 | 
					    benchmark (0.5.0)
 | 
				
			||||||
    bigdecimal (3.1.9)
 | 
					    bigdecimal (3.3.1)
 | 
				
			||||||
    bindex (0.8.1)
 | 
					    bindex (0.8.1)
 | 
				
			||||||
    bootsnap (1.18.4)
 | 
					    bootsnap (1.18.6)
 | 
				
			||||||
      msgpack (~> 1.2)
 | 
					      msgpack (~> 1.2)
 | 
				
			||||||
    bootstrap (5.3.3)
 | 
					    bootstrap (5.3.5)
 | 
				
			||||||
      autoprefixer-rails (>= 9.1.0)
 | 
					 | 
				
			||||||
      popper_js (>= 2.11.8, < 3)
 | 
					      popper_js (>= 2.11.8, < 3)
 | 
				
			||||||
    brakeman (7.0.0)
 | 
					    brakeman (7.1.0)
 | 
				
			||||||
      racc
 | 
					      racc
 | 
				
			||||||
    builder (3.3.0)
 | 
					    builder (3.3.0)
 | 
				
			||||||
    capybara (3.40.0)
 | 
					    capybara (3.40.0)
 | 
				
			||||||
@@ -101,79 +101,98 @@ GEM
 | 
				
			|||||||
      regexp_parser (>= 1.5, < 3.0)
 | 
					      regexp_parser (>= 1.5, < 3.0)
 | 
				
			||||||
      xpath (~> 3.2)
 | 
					      xpath (~> 3.2)
 | 
				
			||||||
    concurrent-ruby (1.3.5)
 | 
					    concurrent-ruby (1.3.5)
 | 
				
			||||||
    connection_pool (2.5.0)
 | 
					    connection_pool (2.5.4)
 | 
				
			||||||
    crass (1.0.6)
 | 
					    crass (1.0.6)
 | 
				
			||||||
 | 
					    csv (3.3.5)
 | 
				
			||||||
 | 
					    damerau-levenshtein (1.3.3)
 | 
				
			||||||
    dartsass-rails (0.5.1)
 | 
					    dartsass-rails (0.5.1)
 | 
				
			||||||
      railties (>= 6.0.0)
 | 
					      railties (>= 6.0.0)
 | 
				
			||||||
      sass-embedded (~> 1.63)
 | 
					      sass-embedded (~> 1.63)
 | 
				
			||||||
    date (3.4.1)
 | 
					    date (3.5.0)
 | 
				
			||||||
    debug (1.10.0)
 | 
					    debug (1.11.0)
 | 
				
			||||||
      irb (~> 1.10)
 | 
					      irb (~> 1.10)
 | 
				
			||||||
      reline (>= 0.3.8)
 | 
					      reline (>= 0.3.8)
 | 
				
			||||||
    dotenv (3.1.7)
 | 
					    diff-lcs (1.6.2)
 | 
				
			||||||
    drb (2.2.1)
 | 
					    dotenv (3.1.8)
 | 
				
			||||||
    ed25519 (1.3.0)
 | 
					    drb (2.2.3)
 | 
				
			||||||
 | 
					    ed25519 (1.4.0)
 | 
				
			||||||
 | 
					    erb (5.1.3)
 | 
				
			||||||
    erubi (1.13.1)
 | 
					    erubi (1.13.1)
 | 
				
			||||||
    et-orbi (1.2.11)
 | 
					    et-orbi (1.4.0)
 | 
				
			||||||
      tzinfo
 | 
					      tzinfo
 | 
				
			||||||
    execjs (2.10.0)
 | 
					    factory_bot (6.5.6)
 | 
				
			||||||
    fugit (1.11.1)
 | 
					      activesupport (>= 6.1.0)
 | 
				
			||||||
      et-orbi (~> 1, >= 1.2.11)
 | 
					    factory_bot_rails (6.5.1)
 | 
				
			||||||
 | 
					      factory_bot (~> 6.5)
 | 
				
			||||||
 | 
					      railties (>= 6.1.0)
 | 
				
			||||||
 | 
					    faker (3.5.2)
 | 
				
			||||||
 | 
					      i18n (>= 1.8.11, < 2)
 | 
				
			||||||
 | 
					    friendly_id (5.5.1)
 | 
				
			||||||
 | 
					      activerecord (>= 4.0.0)
 | 
				
			||||||
 | 
					    fugit (1.12.1)
 | 
				
			||||||
 | 
					      et-orbi (~> 1.4)
 | 
				
			||||||
      raabro (~> 1.4)
 | 
					      raabro (~> 1.4)
 | 
				
			||||||
    globalid (1.2.1)
 | 
					    globalid (1.3.0)
 | 
				
			||||||
      activesupport (>= 6.1)
 | 
					      activesupport (>= 6.1)
 | 
				
			||||||
    google-protobuf (4.30.0)
 | 
					    google-protobuf (4.33.0)
 | 
				
			||||||
      bigdecimal
 | 
					      bigdecimal
 | 
				
			||||||
      rake (>= 13)
 | 
					      rake (>= 13)
 | 
				
			||||||
    google-protobuf (4.30.0-aarch64-linux)
 | 
					    google-protobuf (4.33.0-aarch64-linux-gnu)
 | 
				
			||||||
      bigdecimal
 | 
					      bigdecimal
 | 
				
			||||||
      rake (>= 13)
 | 
					      rake (>= 13)
 | 
				
			||||||
    google-protobuf (4.30.0-x86_64-linux)
 | 
					    google-protobuf (4.33.0-aarch64-linux-musl)
 | 
				
			||||||
 | 
					      bigdecimal
 | 
				
			||||||
 | 
					      rake (>= 13)
 | 
				
			||||||
 | 
					    google-protobuf (4.33.0-x86_64-linux-gnu)
 | 
				
			||||||
 | 
					      bigdecimal
 | 
				
			||||||
 | 
					      rake (>= 13)
 | 
				
			||||||
 | 
					    google-protobuf (4.33.0-x86_64-linux-musl)
 | 
				
			||||||
      bigdecimal
 | 
					      bigdecimal
 | 
				
			||||||
      rake (>= 13)
 | 
					      rake (>= 13)
 | 
				
			||||||
    i18n (1.14.7)
 | 
					    i18n (1.14.7)
 | 
				
			||||||
      concurrent-ruby (~> 1.0)
 | 
					      concurrent-ruby (~> 1.0)
 | 
				
			||||||
    importmap-rails (2.1.0)
 | 
					    importmap-rails (2.2.2)
 | 
				
			||||||
      actionpack (>= 6.0.0)
 | 
					      actionpack (>= 6.0.0)
 | 
				
			||||||
      activesupport (>= 6.0.0)
 | 
					      activesupport (>= 6.0.0)
 | 
				
			||||||
      railties (>= 6.0.0)
 | 
					      railties (>= 6.0.0)
 | 
				
			||||||
    io-console (0.8.0)
 | 
					    io-console (0.8.1)
 | 
				
			||||||
    irb (1.15.1)
 | 
					    irb (1.15.2)
 | 
				
			||||||
      pp (>= 0.6.0)
 | 
					      pp (>= 0.6.0)
 | 
				
			||||||
      rdoc (>= 4.0.0)
 | 
					      rdoc (>= 4.0.0)
 | 
				
			||||||
      reline (>= 0.4.2)
 | 
					      reline (>= 0.4.2)
 | 
				
			||||||
    jbuilder (2.13.0)
 | 
					    jbuilder (2.14.1)
 | 
				
			||||||
      actionview (>= 5.0.0)
 | 
					      actionview (>= 7.0.0)
 | 
				
			||||||
      activesupport (>= 5.0.0)
 | 
					      activesupport (>= 7.0.0)
 | 
				
			||||||
    json (2.10.2)
 | 
					    json (2.15.2)
 | 
				
			||||||
    kamal (2.5.3)
 | 
					    kamal (2.8.2)
 | 
				
			||||||
      activesupport (>= 7.0)
 | 
					      activesupport (>= 7.0)
 | 
				
			||||||
      base64 (~> 0.2)
 | 
					      base64 (~> 0.2)
 | 
				
			||||||
      bcrypt_pbkdf (~> 1.0)
 | 
					      bcrypt_pbkdf (~> 1.0)
 | 
				
			||||||
      concurrent-ruby (~> 1.2)
 | 
					      concurrent-ruby (~> 1.2)
 | 
				
			||||||
      dotenv (~> 3.1)
 | 
					      dotenv (~> 3.1)
 | 
				
			||||||
      ed25519 (~> 1.2)
 | 
					      ed25519 (~> 1.4)
 | 
				
			||||||
      net-ssh (~> 7.3)
 | 
					      net-ssh (~> 7.3)
 | 
				
			||||||
      sshkit (>= 1.23.0, < 2.0)
 | 
					      sshkit (>= 1.23.0, < 2.0)
 | 
				
			||||||
      thor (~> 1.3)
 | 
					      thor (~> 1.3)
 | 
				
			||||||
      zeitwerk (>= 2.6.18, < 3.0)
 | 
					      zeitwerk (>= 2.6.18, < 3.0)
 | 
				
			||||||
    language_server-protocol (3.17.0.4)
 | 
					    language_server-protocol (3.17.0.5)
 | 
				
			||||||
    lint_roller (1.1.0)
 | 
					    lint_roller (1.1.0)
 | 
				
			||||||
    logger (1.6.6)
 | 
					    logger (1.7.0)
 | 
				
			||||||
    loofah (2.24.0)
 | 
					    loofah (2.24.1)
 | 
				
			||||||
      crass (~> 1.0.2)
 | 
					      crass (~> 1.0.2)
 | 
				
			||||||
      nokogiri (>= 1.12.0)
 | 
					      nokogiri (>= 1.12.0)
 | 
				
			||||||
    mail (2.8.1)
 | 
					    mail (2.9.0)
 | 
				
			||||||
 | 
					      logger
 | 
				
			||||||
      mini_mime (>= 0.1.1)
 | 
					      mini_mime (>= 0.1.1)
 | 
				
			||||||
      net-imap
 | 
					      net-imap
 | 
				
			||||||
      net-pop
 | 
					      net-pop
 | 
				
			||||||
      net-smtp
 | 
					      net-smtp
 | 
				
			||||||
    marcel (1.0.4)
 | 
					    marcel (1.1.0)
 | 
				
			||||||
    matrix (0.4.2)
 | 
					    matrix (0.4.3)
 | 
				
			||||||
    mini_mime (1.1.5)
 | 
					    mini_mime (1.1.5)
 | 
				
			||||||
    minitest (5.25.4)
 | 
					    minitest (5.26.0)
 | 
				
			||||||
    msgpack (1.8.0)
 | 
					    msgpack (1.8.0)
 | 
				
			||||||
    net-imap (0.5.6)
 | 
					    net-imap (0.5.12)
 | 
				
			||||||
      date
 | 
					      date
 | 
				
			||||||
      net-protocol
 | 
					      net-protocol
 | 
				
			||||||
    net-pop (0.1.2)
 | 
					    net-pop (0.1.2)
 | 
				
			||||||
@@ -188,86 +207,108 @@ GEM
 | 
				
			|||||||
      net-protocol
 | 
					      net-protocol
 | 
				
			||||||
    net-ssh (7.3.0)
 | 
					    net-ssh (7.3.0)
 | 
				
			||||||
    nio4r (2.7.4)
 | 
					    nio4r (2.7.4)
 | 
				
			||||||
    nokogiri (1.18.3-aarch64-linux-gnu)
 | 
					    nokogiri (1.18.10-aarch64-linux-gnu)
 | 
				
			||||||
      racc (~> 1.4)
 | 
					      racc (~> 1.4)
 | 
				
			||||||
    nokogiri (1.18.3-aarch64-linux-musl)
 | 
					    nokogiri (1.18.10-aarch64-linux-musl)
 | 
				
			||||||
      racc (~> 1.4)
 | 
					      racc (~> 1.4)
 | 
				
			||||||
    nokogiri (1.18.3-arm-linux-gnu)
 | 
					    nokogiri (1.18.10-arm-linux-gnu)
 | 
				
			||||||
      racc (~> 1.4)
 | 
					      racc (~> 1.4)
 | 
				
			||||||
    nokogiri (1.18.3-arm-linux-musl)
 | 
					    nokogiri (1.18.10-arm-linux-musl)
 | 
				
			||||||
      racc (~> 1.4)
 | 
					      racc (~> 1.4)
 | 
				
			||||||
    nokogiri (1.18.3-x86_64-linux-gnu)
 | 
					    nokogiri (1.18.10-x86_64-linux-gnu)
 | 
				
			||||||
      racc (~> 1.4)
 | 
					      racc (~> 1.4)
 | 
				
			||||||
    nokogiri (1.18.3-x86_64-linux-musl)
 | 
					    nokogiri (1.18.10-x86_64-linux-musl)
 | 
				
			||||||
      racc (~> 1.4)
 | 
					      racc (~> 1.4)
 | 
				
			||||||
    ostruct (0.6.1)
 | 
					    ostruct (0.6.3)
 | 
				
			||||||
    parallel (1.26.3)
 | 
					    parallel (1.27.0)
 | 
				
			||||||
    parser (3.3.7.1)
 | 
					    parser (3.3.10.0)
 | 
				
			||||||
      ast (~> 2.4.1)
 | 
					      ast (~> 2.4.1)
 | 
				
			||||||
      racc
 | 
					      racc
 | 
				
			||||||
    popper_js (2.11.8)
 | 
					    popper_js (2.11.8)
 | 
				
			||||||
    pp (0.6.2)
 | 
					    pp (0.6.3)
 | 
				
			||||||
      prettyprint
 | 
					      prettyprint
 | 
				
			||||||
    prettyprint (0.2.0)
 | 
					    prettyprint (0.2.0)
 | 
				
			||||||
    propshaft (1.1.0)
 | 
					    prism (1.6.0)
 | 
				
			||||||
 | 
					    propshaft (1.3.1)
 | 
				
			||||||
      actionpack (>= 7.0.0)
 | 
					      actionpack (>= 7.0.0)
 | 
				
			||||||
      activesupport (>= 7.0.0)
 | 
					      activesupport (>= 7.0.0)
 | 
				
			||||||
      rack
 | 
					      rack
 | 
				
			||||||
      railties (>= 7.0.0)
 | 
					    psych (5.2.6)
 | 
				
			||||||
    psych (5.2.3)
 | 
					 | 
				
			||||||
      date
 | 
					      date
 | 
				
			||||||
      stringio
 | 
					      stringio
 | 
				
			||||||
    public_suffix (6.0.1)
 | 
					    public_suffix (6.0.2)
 | 
				
			||||||
    puma (6.6.0)
 | 
					    puma (7.1.0)
 | 
				
			||||||
      nio4r (~> 2.0)
 | 
					      nio4r (~> 2.0)
 | 
				
			||||||
 | 
					    pundit (2.5.2)
 | 
				
			||||||
 | 
					      activesupport (>= 3.0.0)
 | 
				
			||||||
    raabro (1.4.0)
 | 
					    raabro (1.4.0)
 | 
				
			||||||
    racc (1.8.1)
 | 
					    racc (1.8.1)
 | 
				
			||||||
    rack (3.1.12)
 | 
					    rack (3.2.3)
 | 
				
			||||||
    rack-session (2.1.0)
 | 
					    rack-session (2.1.1)
 | 
				
			||||||
      base64 (>= 0.1.0)
 | 
					      base64 (>= 0.1.0)
 | 
				
			||||||
      rack (>= 3.0.0)
 | 
					      rack (>= 3.0.0)
 | 
				
			||||||
    rack-test (2.2.0)
 | 
					    rack-test (2.2.0)
 | 
				
			||||||
      rack (>= 1.3)
 | 
					      rack (>= 1.3)
 | 
				
			||||||
    rackup (2.2.1)
 | 
					    rackup (2.2.1)
 | 
				
			||||||
      rack (>= 3)
 | 
					      rack (>= 3)
 | 
				
			||||||
    rails (8.0.2)
 | 
					    rails (8.0.4)
 | 
				
			||||||
      actioncable (= 8.0.2)
 | 
					      actioncable (= 8.0.4)
 | 
				
			||||||
      actionmailbox (= 8.0.2)
 | 
					      actionmailbox (= 8.0.4)
 | 
				
			||||||
      actionmailer (= 8.0.2)
 | 
					      actionmailer (= 8.0.4)
 | 
				
			||||||
      actionpack (= 8.0.2)
 | 
					      actionpack (= 8.0.4)
 | 
				
			||||||
      actiontext (= 8.0.2)
 | 
					      actiontext (= 8.0.4)
 | 
				
			||||||
      actionview (= 8.0.2)
 | 
					      actionview (= 8.0.4)
 | 
				
			||||||
      activejob (= 8.0.2)
 | 
					      activejob (= 8.0.4)
 | 
				
			||||||
      activemodel (= 8.0.2)
 | 
					      activemodel (= 8.0.4)
 | 
				
			||||||
      activerecord (= 8.0.2)
 | 
					      activerecord (= 8.0.4)
 | 
				
			||||||
      activestorage (= 8.0.2)
 | 
					      activestorage (= 8.0.4)
 | 
				
			||||||
      activesupport (= 8.0.2)
 | 
					      activesupport (= 8.0.4)
 | 
				
			||||||
      bundler (>= 1.15.0)
 | 
					      bundler (>= 1.15.0)
 | 
				
			||||||
      railties (= 8.0.2)
 | 
					      railties (= 8.0.4)
 | 
				
			||||||
    rails-dom-testing (2.2.0)
 | 
					    rails-dom-testing (2.3.0)
 | 
				
			||||||
      activesupport (>= 5.0.0)
 | 
					      activesupport (>= 5.0.0)
 | 
				
			||||||
      minitest
 | 
					      minitest
 | 
				
			||||||
      nokogiri (>= 1.6)
 | 
					      nokogiri (>= 1.6)
 | 
				
			||||||
    rails-html-sanitizer (1.6.2)
 | 
					    rails-html-sanitizer (1.6.2)
 | 
				
			||||||
      loofah (~> 2.21)
 | 
					      loofah (~> 2.21)
 | 
				
			||||||
      nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
 | 
					      nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
 | 
				
			||||||
    railties (8.0.2)
 | 
					    railties (8.0.4)
 | 
				
			||||||
      actionpack (= 8.0.2)
 | 
					      actionpack (= 8.0.4)
 | 
				
			||||||
      activesupport (= 8.0.2)
 | 
					      activesupport (= 8.0.4)
 | 
				
			||||||
      irb (~> 1.13)
 | 
					      irb (~> 1.13)
 | 
				
			||||||
      rackup (>= 1.0.0)
 | 
					      rackup (>= 1.0.0)
 | 
				
			||||||
      rake (>= 12.2)
 | 
					      rake (>= 12.2)
 | 
				
			||||||
      thor (~> 1.0, >= 1.2.2)
 | 
					      thor (~> 1.0, >= 1.2.2)
 | 
				
			||||||
 | 
					      tsort (>= 0.2)
 | 
				
			||||||
      zeitwerk (~> 2.6)
 | 
					      zeitwerk (~> 2.6)
 | 
				
			||||||
    rainbow (3.1.1)
 | 
					    rainbow (3.1.1)
 | 
				
			||||||
    rake (13.2.1)
 | 
					    rake (13.3.1)
 | 
				
			||||||
    rdoc (6.12.0)
 | 
					    rdoc (6.15.0)
 | 
				
			||||||
 | 
					      erb
 | 
				
			||||||
      psych (>= 4.0.0)
 | 
					      psych (>= 4.0.0)
 | 
				
			||||||
    regexp_parser (2.10.0)
 | 
					      tsort
 | 
				
			||||||
    reline (0.6.0)
 | 
					    regexp_parser (2.11.3)
 | 
				
			||||||
 | 
					    reline (0.6.2)
 | 
				
			||||||
      io-console (~> 0.5)
 | 
					      io-console (~> 0.5)
 | 
				
			||||||
    rexml (3.4.1)
 | 
					    rexml (3.4.4)
 | 
				
			||||||
    rubocop (1.73.2)
 | 
					    rspec-core (3.13.6)
 | 
				
			||||||
 | 
					      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.6)
 | 
				
			||||||
 | 
					      diff-lcs (>= 1.2.0, < 2.0)
 | 
				
			||||||
 | 
					      rspec-support (~> 3.13.0)
 | 
				
			||||||
 | 
					    rspec-rails (8.0.2)
 | 
				
			||||||
 | 
					      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.6)
 | 
				
			||||||
 | 
					    rubocop (1.81.6)
 | 
				
			||||||
      json (~> 2.3)
 | 
					      json (~> 2.3)
 | 
				
			||||||
      language_server-protocol (~> 3.17.0.2)
 | 
					      language_server-protocol (~> 3.17.0.2)
 | 
				
			||||||
      lint_roller (~> 1.1.0)
 | 
					      lint_roller (~> 1.1.0)
 | 
				
			||||||
@@ -275,71 +316,72 @@ GEM
 | 
				
			|||||||
      parser (>= 3.3.0.2)
 | 
					      parser (>= 3.3.0.2)
 | 
				
			||||||
      rainbow (>= 2.2.2, < 4.0)
 | 
					      rainbow (>= 2.2.2, < 4.0)
 | 
				
			||||||
      regexp_parser (>= 2.9.3, < 3.0)
 | 
					      regexp_parser (>= 2.9.3, < 3.0)
 | 
				
			||||||
      rubocop-ast (>= 1.38.0, < 2.0)
 | 
					      rubocop-ast (>= 1.47.1, < 2.0)
 | 
				
			||||||
      ruby-progressbar (~> 1.7)
 | 
					      ruby-progressbar (~> 1.7)
 | 
				
			||||||
      unicode-display_width (>= 2.4.0, < 4.0)
 | 
					      unicode-display_width (>= 2.4.0, < 4.0)
 | 
				
			||||||
    rubocop-ast (1.38.1)
 | 
					    rubocop-ast (1.47.1)
 | 
				
			||||||
      parser (>= 3.3.1.0)
 | 
					      parser (>= 3.3.7.2)
 | 
				
			||||||
    rubocop-performance (1.24.0)
 | 
					      prism (~> 1.4)
 | 
				
			||||||
 | 
					    rubocop-performance (1.26.1)
 | 
				
			||||||
      lint_roller (~> 1.1)
 | 
					      lint_roller (~> 1.1)
 | 
				
			||||||
      rubocop (>= 1.72.1, < 2.0)
 | 
					      rubocop (>= 1.75.0, < 2.0)
 | 
				
			||||||
      rubocop-ast (>= 1.38.0, < 2.0)
 | 
					      rubocop-ast (>= 1.47.1, < 2.0)
 | 
				
			||||||
    rubocop-rails (2.30.3)
 | 
					    rubocop-rails (2.33.4)
 | 
				
			||||||
      activesupport (>= 4.2.0)
 | 
					      activesupport (>= 4.2.0)
 | 
				
			||||||
      lint_roller (~> 1.1)
 | 
					      lint_roller (~> 1.1)
 | 
				
			||||||
      rack (>= 1.1)
 | 
					      rack (>= 1.1)
 | 
				
			||||||
      rubocop (>= 1.72.1, < 2.0)
 | 
					      rubocop (>= 1.75.0, < 2.0)
 | 
				
			||||||
      rubocop-ast (>= 1.38.0, < 2.0)
 | 
					      rubocop-ast (>= 1.44.0, < 2.0)
 | 
				
			||||||
    rubocop-rails-omakase (1.1.0)
 | 
					    rubocop-rails-omakase (1.1.0)
 | 
				
			||||||
      rubocop (>= 1.72)
 | 
					      rubocop (>= 1.72)
 | 
				
			||||||
      rubocop-performance (>= 1.24)
 | 
					      rubocop-performance (>= 1.24)
 | 
				
			||||||
      rubocop-rails (>= 2.30)
 | 
					      rubocop-rails (>= 2.30)
 | 
				
			||||||
    ruby-progressbar (1.13.0)
 | 
					    ruby-progressbar (1.13.0)
 | 
				
			||||||
    rubyzip (2.4.1)
 | 
					    rubyzip (3.2.1)
 | 
				
			||||||
    sass-embedded (1.85.1-aarch64-linux-gnu)
 | 
					    sass-embedded (1.93.2-aarch64-linux-gnu)
 | 
				
			||||||
      google-protobuf (~> 4.29)
 | 
					      google-protobuf (~> 4.31)
 | 
				
			||||||
    sass-embedded (1.85.1-aarch64-linux-musl)
 | 
					    sass-embedded (1.93.2-aarch64-linux-musl)
 | 
				
			||||||
      google-protobuf (~> 4.29)
 | 
					      google-protobuf (~> 4.31)
 | 
				
			||||||
    sass-embedded (1.85.1-arm-linux-gnueabihf)
 | 
					    sass-embedded (1.93.2-arm-linux-gnueabihf)
 | 
				
			||||||
      google-protobuf (~> 4.29)
 | 
					      google-protobuf (~> 4.31)
 | 
				
			||||||
    sass-embedded (1.85.1-arm-linux-musleabihf)
 | 
					    sass-embedded (1.93.2-arm-linux-musleabihf)
 | 
				
			||||||
      google-protobuf (~> 4.29)
 | 
					      google-protobuf (~> 4.31)
 | 
				
			||||||
    sass-embedded (1.85.1-x86_64-linux-gnu)
 | 
					    sass-embedded (1.93.2-x86_64-linux-gnu)
 | 
				
			||||||
      google-protobuf (~> 4.29)
 | 
					      google-protobuf (~> 4.31)
 | 
				
			||||||
    sass-embedded (1.85.1-x86_64-linux-musl)
 | 
					    sass-embedded (1.93.2-x86_64-linux-musl)
 | 
				
			||||||
      google-protobuf (~> 4.29)
 | 
					      google-protobuf (~> 4.31)
 | 
				
			||||||
    securerandom (0.4.1)
 | 
					    securerandom (0.4.1)
 | 
				
			||||||
    selenium-webdriver (4.29.1)
 | 
					    selenium-webdriver (4.38.0)
 | 
				
			||||||
      base64 (~> 0.2)
 | 
					      base64 (~> 0.2)
 | 
				
			||||||
      logger (~> 1.4)
 | 
					      logger (~> 1.4)
 | 
				
			||||||
      rexml (~> 3.2, >= 3.2.5)
 | 
					      rexml (~> 3.2, >= 3.2.5)
 | 
				
			||||||
      rubyzip (>= 1.2.2, < 3.0)
 | 
					      rubyzip (>= 1.2.2, < 4.0)
 | 
				
			||||||
      websocket (~> 1.0)
 | 
					      websocket (~> 1.0)
 | 
				
			||||||
    slim (5.2.1)
 | 
					    slim (5.2.1)
 | 
				
			||||||
      temple (~> 0.10.0)
 | 
					      temple (~> 0.10.0)
 | 
				
			||||||
      tilt (>= 2.1.0)
 | 
					      tilt (>= 2.1.0)
 | 
				
			||||||
    solid_cable (3.0.7)
 | 
					    solid_cable (3.0.12)
 | 
				
			||||||
      actioncable (>= 7.2)
 | 
					      actioncable (>= 7.2)
 | 
				
			||||||
      activejob (>= 7.2)
 | 
					      activejob (>= 7.2)
 | 
				
			||||||
      activerecord (>= 7.2)
 | 
					      activerecord (>= 7.2)
 | 
				
			||||||
      railties (>= 7.2)
 | 
					      railties (>= 7.2)
 | 
				
			||||||
    solid_cache (1.0.7)
 | 
					    solid_cache (1.0.8)
 | 
				
			||||||
      activejob (>= 7.2)
 | 
					      activejob (>= 7.2)
 | 
				
			||||||
      activerecord (>= 7.2)
 | 
					      activerecord (>= 7.2)
 | 
				
			||||||
      railties (>= 7.2)
 | 
					      railties (>= 7.2)
 | 
				
			||||||
    solid_queue (1.1.3)
 | 
					    solid_queue (1.2.3)
 | 
				
			||||||
      activejob (>= 7.1)
 | 
					      activejob (>= 7.1)
 | 
				
			||||||
      activerecord (>= 7.1)
 | 
					      activerecord (>= 7.1)
 | 
				
			||||||
      concurrent-ruby (>= 1.3.1)
 | 
					      concurrent-ruby (>= 1.3.1)
 | 
				
			||||||
      fugit (~> 1.11.0)
 | 
					      fugit (~> 1.11)
 | 
				
			||||||
      railties (>= 7.1)
 | 
					      railties (>= 7.1)
 | 
				
			||||||
      thor (~> 1.3.1)
 | 
					      thor (>= 1.3.1)
 | 
				
			||||||
    sqlite3 (2.6.0-aarch64-linux-gnu)
 | 
					    sqlite3 (2.7.4-aarch64-linux-gnu)
 | 
				
			||||||
    sqlite3 (2.6.0-aarch64-linux-musl)
 | 
					    sqlite3 (2.7.4-aarch64-linux-musl)
 | 
				
			||||||
    sqlite3 (2.6.0-arm-linux-gnu)
 | 
					    sqlite3 (2.7.4-arm-linux-gnu)
 | 
				
			||||||
    sqlite3 (2.6.0-arm-linux-musl)
 | 
					    sqlite3 (2.7.4-arm-linux-musl)
 | 
				
			||||||
    sqlite3 (2.6.0-x86_64-linux-gnu)
 | 
					    sqlite3 (2.7.4-x86_64-linux-gnu)
 | 
				
			||||||
    sqlite3 (2.6.0-x86_64-linux-musl)
 | 
					    sqlite3 (2.7.4-x86_64-linux-musl)
 | 
				
			||||||
    sshkit (1.24.0)
 | 
					    sshkit (1.24.0)
 | 
				
			||||||
      base64
 | 
					      base64
 | 
				
			||||||
      logger
 | 
					      logger
 | 
				
			||||||
@@ -349,23 +391,24 @@ GEM
 | 
				
			|||||||
      ostruct
 | 
					      ostruct
 | 
				
			||||||
    stimulus-rails (1.3.4)
 | 
					    stimulus-rails (1.3.4)
 | 
				
			||||||
      railties (>= 6.0.0)
 | 
					      railties (>= 6.0.0)
 | 
				
			||||||
    stringio (3.1.5)
 | 
					    stringio (3.1.7)
 | 
				
			||||||
    temple (0.10.3)
 | 
					    temple (0.10.4)
 | 
				
			||||||
    thor (1.3.2)
 | 
					    thor (1.4.0)
 | 
				
			||||||
    thruster (0.1.12)
 | 
					    thruster (0.1.16)
 | 
				
			||||||
    thruster (0.1.12-aarch64-linux)
 | 
					    thruster (0.1.16-aarch64-linux)
 | 
				
			||||||
    thruster (0.1.12-x86_64-linux)
 | 
					    thruster (0.1.16-x86_64-linux)
 | 
				
			||||||
    tilt (2.6.0)
 | 
					    tilt (2.6.1)
 | 
				
			||||||
    timeout (0.4.3)
 | 
					    timeout (0.4.4)
 | 
				
			||||||
    turbo-rails (2.0.13)
 | 
					    tsort (0.2.0)
 | 
				
			||||||
 | 
					    turbo-rails (2.0.20)
 | 
				
			||||||
      actionpack (>= 7.1.0)
 | 
					      actionpack (>= 7.1.0)
 | 
				
			||||||
      railties (>= 7.1.0)
 | 
					      railties (>= 7.1.0)
 | 
				
			||||||
    tzinfo (2.0.6)
 | 
					    tzinfo (2.0.6)
 | 
				
			||||||
      concurrent-ruby (~> 1.0)
 | 
					      concurrent-ruby (~> 1.0)
 | 
				
			||||||
    unicode-display_width (3.1.4)
 | 
					    unicode-display_width (3.2.0)
 | 
				
			||||||
      unicode-emoji (~> 4.0, >= 4.0.4)
 | 
					      unicode-emoji (~> 4.1)
 | 
				
			||||||
    unicode-emoji (4.0.4)
 | 
					    unicode-emoji (4.1.0)
 | 
				
			||||||
    uri (1.0.3)
 | 
					    uri (1.0.4)
 | 
				
			||||||
    useragent (0.16.11)
 | 
					    useragent (0.16.11)
 | 
				
			||||||
    web-console (4.2.1)
 | 
					    web-console (4.2.1)
 | 
				
			||||||
      actionview (>= 6.0.0)
 | 
					      actionview (>= 6.0.0)
 | 
				
			||||||
@@ -373,13 +416,13 @@ GEM
 | 
				
			|||||||
      bindex (>= 0.4.0)
 | 
					      bindex (>= 0.4.0)
 | 
				
			||||||
      railties (>= 6.0.0)
 | 
					      railties (>= 6.0.0)
 | 
				
			||||||
    websocket (1.2.11)
 | 
					    websocket (1.2.11)
 | 
				
			||||||
    websocket-driver (0.7.7)
 | 
					    websocket-driver (0.8.0)
 | 
				
			||||||
      base64
 | 
					      base64
 | 
				
			||||||
      websocket-extensions (>= 0.1.0)
 | 
					      websocket-extensions (>= 0.1.0)
 | 
				
			||||||
    websocket-extensions (0.1.5)
 | 
					    websocket-extensions (0.1.5)
 | 
				
			||||||
    xpath (3.2.0)
 | 
					    xpath (3.2.0)
 | 
				
			||||||
      nokogiri (~> 1.8)
 | 
					      nokogiri (~> 1.8)
 | 
				
			||||||
    zeitwerk (2.7.2)
 | 
					    zeitwerk (2.7.3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
PLATFORMS
 | 
					PLATFORMS
 | 
				
			||||||
  aarch64-linux
 | 
					  aarch64-linux
 | 
				
			||||||
@@ -392,19 +435,27 @@ PLATFORMS
 | 
				
			|||||||
  x86_64-linux-musl
 | 
					  x86_64-linux-musl
 | 
				
			||||||
 | 
					
 | 
				
			||||||
DEPENDENCIES
 | 
					DEPENDENCIES
 | 
				
			||||||
 | 
					  annotaterb
 | 
				
			||||||
  bcrypt (~> 3.1.7)
 | 
					  bcrypt (~> 3.1.7)
 | 
				
			||||||
  bootsnap
 | 
					  bootsnap
 | 
				
			||||||
  bootstrap (~> 5.3.3)
 | 
					  bootstrap (~> 5.3.3)
 | 
				
			||||||
  brakeman
 | 
					  brakeman
 | 
				
			||||||
  capybara
 | 
					  capybara
 | 
				
			||||||
 | 
					  csv
 | 
				
			||||||
 | 
					  damerau-levenshtein
 | 
				
			||||||
  dartsass-rails
 | 
					  dartsass-rails
 | 
				
			||||||
  debug
 | 
					  debug
 | 
				
			||||||
 | 
					  factory_bot_rails
 | 
				
			||||||
 | 
					  faker
 | 
				
			||||||
 | 
					  friendly_id (~> 5.5.0)
 | 
				
			||||||
  importmap-rails
 | 
					  importmap-rails
 | 
				
			||||||
  jbuilder
 | 
					  jbuilder
 | 
				
			||||||
  kamal
 | 
					  kamal
 | 
				
			||||||
  propshaft
 | 
					  propshaft
 | 
				
			||||||
  puma (>= 5.0)
 | 
					  puma (>= 5.0)
 | 
				
			||||||
 | 
					  pundit (~> 2.5)
 | 
				
			||||||
  rails (~> 8.0.2)
 | 
					  rails (~> 8.0.2)
 | 
				
			||||||
 | 
					  rspec-rails
 | 
				
			||||||
  rubocop-rails-omakase
 | 
					  rubocop-rails-omakase
 | 
				
			||||||
  selenium-webdriver
 | 
					  selenium-webdriver
 | 
				
			||||||
  slim
 | 
					  slim
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										41
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								README.md
									
									
									
									
									
								
							@@ -3,3 +3,44 @@
 | 
				
			|||||||
## Dependencies
 | 
					## Dependencies
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Rails installation guide: https://guides.rubyonrails.org/install_ruby_on_rails.html.
 | 
					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
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
@@ -1,3 +1,7 @@
 | 
				
			|||||||
// Sassy
 | 
					// Sassy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@import "bootstrap";
 | 
					@import "bootstrap";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.error-message {
 | 
				
			||||||
 | 
					  color: var(--bs-danger)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,17 +1,44 @@
 | 
				
			|||||||
class ApplicationController < ActionController::Base
 | 
					class ApplicationController < ActionController::Base
 | 
				
			||||||
  include Authentication
 | 
					  include Authentication
 | 
				
			||||||
 | 
					  include Pundit::Authorization
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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.
 | 
					  # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
 | 
				
			||||||
  allow_browser versions: :modern
 | 
					  allow_browser versions: :modern
 | 
				
			||||||
  before_action :set_title, :set_current_user
 | 
					  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
 | 
				
			||||||
  layout "authenticated"
 | 
					  layout "authenticated"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def set_title
 | 
					  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
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def set_current_user
 | 
					  def set_current_user
 | 
				
			||||||
    @current_user = current_user
 | 
					    @current_user = current_user
 | 
				
			||||||
  end
 | 
					  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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if current_user
 | 
				
			||||||
 | 
					      flash[:error] = t "#{policy_name}.#{exception.query}", scope: "pundit", default: :default
 | 
				
			||||||
 | 
					      redirect_back_or_to(root_path)
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      not_found
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def not_found
 | 
				
			||||||
 | 
					    render file: "#{Rails.root}/public/404.html", layout: false, status: :not_found
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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
 | 
				
			||||||
							
								
								
									
										114
									
								
								app/controllers/completions_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								app/controllers/completions_controller.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,114 @@
 | 
				
			|||||||
 | 
					class CompletionsController < ApplicationController
 | 
				
			||||||
 | 
					  include CompletionsConcern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  before_action :set_contest
 | 
				
			||||||
 | 
					  before_action :set_contestant
 | 
				
			||||||
 | 
					  before_action :set_data, only: %i[ create edit new update ]
 | 
				
			||||||
 | 
					  before_action :set_completion, only: %i[ destroy edit update ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def edit
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if @contestant
 | 
				
			||||||
 | 
					      @action_name = t("helpers.buttons.back_to_contestant")
 | 
				
			||||||
 | 
					      @action_path = edit_contest_contestant_path(@contest, @contestant)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def new
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if @contestant
 | 
				
			||||||
 | 
					      @action_name = t("helpers.buttons.back_to_contestant")
 | 
				
			||||||
 | 
					      @action_path = edit_contest_contestant_path(@contest, @contestant)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @completion = Completion.new
 | 
				
			||||||
 | 
					    if params[:contestant_id]
 | 
				
			||||||
 | 
					      @completion.contestant_id = params[:contestant_id]
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def create
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @completion = Completion.new(completion_params)
 | 
				
			||||||
 | 
					    @completion.contest_id = @contest.id
 | 
				
			||||||
 | 
					    if @completion.save
 | 
				
			||||||
 | 
					      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
 | 
				
			||||||
 | 
					      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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def update
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @completion.contestant_id = params[:contestant_id] if params[:contestant_id]
 | 
				
			||||||
 | 
					    if @completion.update(completion_params)
 | 
				
			||||||
 | 
					      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
 | 
				
			||||||
 | 
					      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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def destroy
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @completion.destroy
 | 
				
			||||||
 | 
					    if params[:contestant_id]
 | 
				
			||||||
 | 
					      redirect_to contest_contestant_path(@contest, params[:contestant_id]), notice: t("completions.destroy.notice")
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      redirect_to contest_path(@contest), notice: t("completions.destroy.notice")
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def set_contest
 | 
				
			||||||
 | 
					    @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.order(:name)
 | 
				
			||||||
 | 
					    @puzzles = @contest.puzzles
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def set_completion
 | 
				
			||||||
 | 
					    @completion = Completion.find(params[:id])
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def completion_params
 | 
				
			||||||
 | 
					    params.expect(completion: [ :display_time_from_start, :remaining_pieces, :contestant_id, :message_id, :puzzle_id ])
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -51,6 +51,7 @@ module Authentication
 | 
				
			|||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def current_user
 | 
					    def current_user
 | 
				
			||||||
 | 
					      return unless Current.session
 | 
				
			||||||
      return unless Current.session[:user_id]
 | 
					      return unless Current.session[:user_id]
 | 
				
			||||||
      User.find(Current.session[:user_id])
 | 
					      User.find(Current.session[:user_id])
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										32
									
								
								app/controllers/concerns/completions_concern.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								app/controllers/concerns/completions_concern.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					module CompletionsConcern
 | 
				
			||||||
 | 
					  extend ActiveSupport::Concern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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
 | 
				
			||||||
 | 
					    s.to_s
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def extend_completions!(contestant)
 | 
				
			||||||
 | 
					    current_time_from_start = 0
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
							
								
								
									
										160
									
								
								app/controllers/contestants_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								app/controllers/contestants_controller.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,160 @@
 | 
				
			|||||||
 | 
					class ContestantsController < ApplicationController
 | 
				
			||||||
 | 
					  before_action :set_contest
 | 
				
			||||||
 | 
					  before_action :set_contestant, only: %i[ destroy edit update]
 | 
				
			||||||
 | 
					  before_action :set_completions, only: %i[edit update ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def edit
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @action_name = t("helpers.buttons.back")
 | 
				
			||||||
 | 
					    @action_path = contest_path(@contest)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def new
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @action_name = t("helpers.buttons.back")
 | 
				
			||||||
 | 
					    @action_path = contest_path(@contest)
 | 
				
			||||||
 | 
					    @contestant = Contestant.new
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def create
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @contestant = Contestant.new(contestant_params)
 | 
				
			||||||
 | 
					    @contestant.contest_id = @contest.id
 | 
				
			||||||
 | 
					    if @contestant.save
 | 
				
			||||||
 | 
					      update_contestant_categories
 | 
				
			||||||
 | 
					      redirect_to contest_path(@contest), notice: t("contestants.new.notice")
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      @action_name = t("helpers.buttons.back")
 | 
				
			||||||
 | 
					      @action_path = contest_path(@contest)
 | 
				
			||||||
 | 
					      render :new, status: :unprocessable_entity
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def update
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if @contestant.update(contestant_params)
 | 
				
			||||||
 | 
					      update_contestant_categories
 | 
				
			||||||
 | 
					      redirect_to @contest, notice: t("contestants.edit.notice")
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      @action_name = t("helpers.buttons.back")
 | 
				
			||||||
 | 
					      @action_path = contest_path(@contest)
 | 
				
			||||||
 | 
					      render :edit, status: :unprocessable_entity
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def destroy
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @contestant.destroy
 | 
				
			||||||
 | 
					    redirect_to contest_path(@contest), notice: t("contestants.destroy.notice")
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def import
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @action_name = t("helpers.buttons.back")
 | 
				
			||||||
 | 
					    @action_path = contest_path(@contest)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @csv_import = CsvImport.new
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def upload_csv
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @csv_import = CsvImport.new(params.require(:csv_import).permit(:file, :separator))
 | 
				
			||||||
 | 
					    if @csv_import.save
 | 
				
			||||||
 | 
					      redirect_to "/contests/#{@contest.id}/import/#{@csv_import.id}"
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      @action_name = t("helpers.buttons.back")
 | 
				
			||||||
 | 
					      @action_path = contest_path(@contest)
 | 
				
			||||||
 | 
					      render :import, status: :unprocessable_entity
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def convert_csv
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @action_name = t("helpers.buttons.back")
 | 
				
			||||||
 | 
					    @action_path = contest_path(@contest)
 | 
				
			||||||
 | 
					    @csv_import = CsvImport.find(params[:id])
 | 
				
			||||||
 | 
					    @content = JSON.parse(@csv_import.content)
 | 
				
			||||||
 | 
					    @form = Forms::CsvConversionForm.new
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def finalize_import
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @csv_import = CsvImport.find(params[:id])
 | 
				
			||||||
 | 
					    @content = JSON.parse(@csv_import.content)
 | 
				
			||||||
 | 
					    all_params = params.require(:forms_csv_conversion_form)
 | 
				
			||||||
 | 
					    @form = Forms::CsvConversionForm.new(params.require(:forms_csv_conversion_form).permit(:email_column, :name_column))
 | 
				
			||||||
 | 
					    if @form.valid?
 | 
				
			||||||
 | 
					      @content.each_with_index do |row, i|
 | 
				
			||||||
 | 
					        if all_params["row_#{i}".to_sym] == "1"
 | 
				
			||||||
 | 
					          if @form.email_column == -1
 | 
				
			||||||
 | 
					            Contestant.create(name: row[@form.name_column], contest: @contest)
 | 
				
			||||||
 | 
					          else
 | 
				
			||||||
 | 
					            logger.info("Email")
 | 
				
			||||||
 | 
					            Contestant.create(name: row[@form.name_column], email: row[@form.email_column], contest: @contest)
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					      redirect_to contest_path(@contest), notice: t("contestants.import.notice")
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      @action_name = t("helpers.buttons.back")
 | 
				
			||||||
 | 
					      @action_path = contest_path(@contest)
 | 
				
			||||||
 | 
					      render :convert_csv, status: :unprocessable_entity
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def export
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @contestants = @contest.contestants.sort_by { |contestant| [
 | 
				
			||||||
 | 
					      -contestant.completions.where(remaining_pieces: nil).size,
 | 
				
			||||||
 | 
					      (contestant.completions.where(remaining_pieces: nil).size == @contest.puzzles.length ? 1 : 0) * contestant.time_seconds,
 | 
				
			||||||
 | 
					      contestant.completions.size > 0 && contestant.completions[-1].remaining_pieces ? contestant.completions[-1].remaining_pieces : 1000000,
 | 
				
			||||||
 | 
					      contestant.time_seconds
 | 
				
			||||||
 | 
					    ] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    respond_to do |format|
 | 
				
			||||||
 | 
					      format.csv do
 | 
				
			||||||
 | 
					        response.headers["Content-Type"] = "text/csv"
 | 
				
			||||||
 | 
					        response.headers["Content-Disposition"] = "attachment; filename=export.csv"
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def set_contest
 | 
				
			||||||
 | 
					    @contest = Contest.find(params[:contest_id])
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def set_contestant
 | 
				
			||||||
 | 
					    @contestant = Contestant.find(params[:id])
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def set_completions
 | 
				
			||||||
 | 
					    @completions = @contestant.completions.order(:time_seconds)
 | 
				
			||||||
 | 
					  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,52 +1,198 @@
 | 
				
			|||||||
class ContestsController < ApplicationController
 | 
					class ContestsController < ApplicationController
 | 
				
			||||||
  before_action :set_contest, only: %i[ destroy edit show update ]
 | 
					  before_action :set_contest, only: %i[ destroy edit show update ]
 | 
				
			||||||
 | 
					  before_action :offline_setup, only: %i[ offline_new offline_create offline_edit offline_update offline_completed ]
 | 
				
			||||||
 | 
					  skip_before_action :require_authentication, only: %i[ scoreboard offline_new offline_create offline_edit offline_update offline_completed ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def index
 | 
					  def index
 | 
				
			||||||
 | 
					    authorize :contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @contests = current_user.contests
 | 
					    @contests = current_user.contests
 | 
				
			||||||
    @title = "Welcome #{current_user.username}!"
 | 
					    @title = I18n.t("contests.index.title", username: current_user.username)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def show
 | 
					  def show
 | 
				
			||||||
    @title = @contest.name
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @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.where(remaining_pieces: nil).size,
 | 
				
			||||||
 | 
					      (contestant.completions.where(remaining_pieces: nil).size == @contest.puzzles.length ? 1 : 0) * contestant.time_seconds,
 | 
				
			||||||
 | 
					      contestant.completions.size > 0 && contestant.completions[-1].remaining_pieces ? contestant.completions[-1].remaining_pieces : 1000000,
 | 
				
			||||||
 | 
					      contestant.time_seconds
 | 
				
			||||||
 | 
					    ] }
 | 
				
			||||||
 | 
					    filter_contestants_per_category
 | 
				
			||||||
 | 
					    @puzzles = @contest.puzzles.order(:id)
 | 
				
			||||||
 | 
					    @messages = @contest.messages.order(:time_seconds)
 | 
				
			||||||
 | 
					    set_badges
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def edit
 | 
					  def edit
 | 
				
			||||||
    @title = "Edit contest settings"
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @action_name = t("helpers.buttons.back")
 | 
				
			||||||
 | 
					    @action_path = contest_path(@contest)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def new
 | 
					  def new
 | 
				
			||||||
 | 
					    authorize :contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @contest = Contest.new
 | 
					    @contest = Contest.new
 | 
				
			||||||
    @title = "New jigsaw puzzle competition"
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def create
 | 
					  def create
 | 
				
			||||||
 | 
					    authorize :contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @contest = Contest.new(contest_params)
 | 
					    @contest = Contest.new(contest_params)
 | 
				
			||||||
    @contest.user_id = current_user.id
 | 
					    @contest.user_id = current_user.id
 | 
				
			||||||
    if @contest.save
 | 
					    if @contest.save
 | 
				
			||||||
      redirect_to @contest
 | 
					      redirect_to @contest, notice: t("contests.new.notice")
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      render :new, status: :unprocessable_entity
 | 
					      render :new, status: :unprocessable_entity
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def update
 | 
					  def update
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if @contest.update(contest_params)
 | 
					    if @contest.update(contest_params)
 | 
				
			||||||
      redirect_to @contest
 | 
					      redirect_to @contest, notice: t("contests.edit.notice")
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
 | 
					      @action_name = t("helpers.buttons.back")
 | 
				
			||||||
 | 
					      @action_path = contest_path(@contest)
 | 
				
			||||||
      render :edit, status: :unprocessable_entity
 | 
					      render :edit, status: :unprocessable_entity
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def destroy
 | 
					  def destroy
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @contest.destroy
 | 
				
			||||||
 | 
					    redirect_to contests_path, notice: t("contests.destroy.notice")
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def scoreboard
 | 
				
			||||||
 | 
					    @contest = Contest.find_by(slug: params[:id])
 | 
				
			||||||
 | 
					    unless @contest && @contest.public
 | 
				
			||||||
 | 
					      skip_authorization
 | 
				
			||||||
 | 
					      not_found and return
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    I18n.locale = @contest.lang
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @title = I18n.t("contests.scoreboard.title", name: @contest.name)
 | 
				
			||||||
 | 
					    @contestants = @contest.contestants.sort_by { |contestant| [
 | 
				
			||||||
 | 
					      -contestant.completions.where(remaining_pieces: nil).size,
 | 
				
			||||||
 | 
					      (contestant.completions.where(remaining_pieces: nil).size == @contest.puzzles.length ? 1 : 0) * contestant.time_seconds,
 | 
				
			||||||
 | 
					      contestant.completions.size > 0 && contestant.completions[-1].remaining_pieces ? contestant.completions[-1].remaining_pieces : 1000000,
 | 
				
			||||||
 | 
					      contestant.time_seconds
 | 
				
			||||||
 | 
					    ] }
 | 
				
			||||||
 | 
					    filter_contestants_per_category
 | 
				
			||||||
 | 
					    @puzzles = @contest.puzzles.order(:id)
 | 
				
			||||||
 | 
					    @action_name = t("helpers.buttons.refresh")
 | 
				
			||||||
 | 
					    if params.key?(:category)
 | 
				
			||||||
 | 
					      @action_path = "/public/#{@contest.friendly_id}?category=#{params[:category]}"
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      @action_path = "/public/#{@contest.friendly_id}"
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					    render :scoreboard
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def offline_new
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					    @offline = Offline.new
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def offline_create
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					    @offline = Offline.new(offline_start_params)
 | 
				
			||||||
 | 
					    @offline.contest = @contest
 | 
				
			||||||
 | 
					    @offline.start_time = Time.now()
 | 
				
			||||||
 | 
					    if @offline.save
 | 
				
			||||||
 | 
					      redirect_to "/public/#{@contest.friendly_id}/offline/#{@offline.generate_token_for(:token)}"
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      render :offline_new, status: :unprocessable_entity
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def offline_edit
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @offline = Offline.find_by_token_for(:token, params[:token])
 | 
				
			||||||
 | 
					    if !@offline
 | 
				
			||||||
 | 
					      not_found and return
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if @offline.images.length > 1
 | 
				
			||||||
 | 
					      render :offline_already_submitted and return
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def offline_update
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @offline = Offline.find_by_token_for(:token, params[:token])
 | 
				
			||||||
 | 
					    if !@offline
 | 
				
			||||||
 | 
					      not_found and return
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @offline.completed = true
 | 
				
			||||||
 | 
					    @offline.images.attach(params[:offline][:end_image])
 | 
				
			||||||
 | 
					    if @offline.save
 | 
				
			||||||
 | 
					      redirect_to "/public/#{@contest.friendly_id}/offline/#{@offline.generate_token_for(:token)}/completed"
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      render :offline_edit, status: :unprocessable_entity
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def offline_completed
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @offline = Offline.find_by_token_for(:token, params[:token])
 | 
				
			||||||
 | 
					    if !@offline
 | 
				
			||||||
 | 
					      not_found and return
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def offline_setup
 | 
				
			||||||
 | 
					    @contest = Contest.find_by(slug: params[:id])
 | 
				
			||||||
 | 
					    I18n.locale = @contest.lang
 | 
				
			||||||
 | 
					    @title = I18n.t("contests.scoreboard.title", name: @contest.name)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def set_badges
 | 
				
			||||||
 | 
					    @badges = []
 | 
				
			||||||
 | 
					    @badges.push(t("helpers.badges.team")) if @contest.team
 | 
				
			||||||
 | 
					    @badges.push(t("helpers.badges.registration")) if @contest.allow_registration
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def set_contest
 | 
					  def set_contest
 | 
				
			||||||
    @contest = Contest.find(params[:id])
 | 
					    @contest = Contest.find(params[:id])
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def contest_params
 | 
					  def contest_params
 | 
				
			||||||
    params.expect(contest: [ :name, :team, :allow_registration ])
 | 
					    params.expect(contest: [ :lang, :name, :offline_form, :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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def offline_start_params
 | 
				
			||||||
 | 
					    params.expect(offline: [ :name, :images ])
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def offline_end_params
 | 
				
			||||||
 | 
					    params.expect(offline: [ :completed, :end_image ])
 | 
				
			||||||
  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
 | 
				
			||||||
@@ -1,38 +1,66 @@
 | 
				
			|||||||
class PuzzlesController < ApplicationController
 | 
					class PuzzlesController < ApplicationController
 | 
				
			||||||
  before_action :set_puzzle, only: %i[ show destroy ]
 | 
					  before_action :set_contest
 | 
				
			||||||
 | 
					  before_action :set_puzzle, only: %i[ destroy edit update]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def index
 | 
					  def edit
 | 
				
			||||||
    @puzzles = Puzzle.all
 | 
					    authorize @contest
 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def show
 | 
					    @action_name = t("helpers.buttons.back")
 | 
				
			||||||
 | 
					    @action_path = contest_path(@contest)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def new
 | 
					  def new
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @action_name = t("helpers.buttons.back")
 | 
				
			||||||
 | 
					    @action_path = contest_path(@contest)
 | 
				
			||||||
    @puzzle = Puzzle.new
 | 
					    @puzzle = Puzzle.new
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def create
 | 
					  def create
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @puzzle = Puzzle.new(puzzle_params)
 | 
					    @puzzle = Puzzle.new(puzzle_params)
 | 
				
			||||||
 | 
					    @puzzle.contest_id = @contest.id
 | 
				
			||||||
    if @puzzle.save
 | 
					    if @puzzle.save
 | 
				
			||||||
      redirect_to @puzzle
 | 
					      redirect_to contest_path(@contest), notice: t("puzzles.new.notice")
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
 | 
					      @action_name = t("helpers.buttons.back")
 | 
				
			||||||
 | 
					      @action_path = contest_path(@contest)
 | 
				
			||||||
      render :new, status: :unprocessable_entity
 | 
					      render :new, status: :unprocessable_entity
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def update
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if @puzzle.update(puzzle_params)
 | 
				
			||||||
 | 
					      redirect_to @contest, notice: t("puzzles.edit.notice")
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      @action_name = t("helpers.buttons.back")
 | 
				
			||||||
 | 
					      @action_path = contest_path(@contest)
 | 
				
			||||||
 | 
					      render :edit, status: :unprocessable_entity
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def destroy
 | 
					  def destroy
 | 
				
			||||||
 | 
					    authorize @contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @puzzle.destroy
 | 
					    @puzzle.destroy
 | 
				
			||||||
    redirect_to puzzles_path
 | 
					    redirect_to contest_path(@contest), notice: t("puzzles.destroy.notice")
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def set_contest
 | 
				
			||||||
 | 
					    @contest = Contest.find(params[:contest_id])
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def set_puzzle
 | 
					  def set_puzzle
 | 
				
			||||||
    @puzzle = Puzzle.find(params[:id])
 | 
					    @puzzle = Puzzle.find(params[:id])
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def puzzle_params
 | 
					  def puzzle_params
 | 
				
			||||||
    params.expect(puzzle: [ :name, :image ])
 | 
					    params.expect(puzzle: [ :brand, :name, :image, :pieces ])
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
class SessionsController < ApplicationController
 | 
					class SessionsController < ApplicationController
 | 
				
			||||||
  allow_unauthenticated_access only: %i[ new create ]
 | 
					  allow_unauthenticated_access only: %i[ new create ]
 | 
				
			||||||
  rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Try again later." }
 | 
					  rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Try again later." }
 | 
				
			||||||
 | 
					  before_action :skip_authorization
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def new
 | 
					  def new
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
@@ -8,7 +9,7 @@ class SessionsController < ApplicationController
 | 
				
			|||||||
  def create
 | 
					  def create
 | 
				
			||||||
    if user = User.authenticate_by(params.permit(:email_address, :password))
 | 
					    if user = User.authenticate_by(params.permit(:email_address, :password))
 | 
				
			||||||
      start_new_session_for user
 | 
					      start_new_session_for user
 | 
				
			||||||
      redirect_to after_authentication_url
 | 
					      redirect_to after_authentication_url, notice: t("sessions.new.notice")
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      redirect_to new_session_path, alert: "Try another email address or password."
 | 
					      redirect_to new_session_path, alert: "Try another email address or password."
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,29 +2,50 @@ class UsersController < ApplicationController
 | 
				
			|||||||
  before_action :set_user, only: %i[ destroy edit show update ]
 | 
					  before_action :set_user, only: %i[ destroy edit show update ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def index
 | 
					  def index
 | 
				
			||||||
    @title = "All users"
 | 
					    authorize :user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @users = User.all
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def edit
 | 
					  def edit
 | 
				
			||||||
    @title = "My settings"
 | 
					    authorize @user
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def update
 | 
					  def update
 | 
				
			||||||
 | 
					    authorize @user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if @user.update(user_params)
 | 
					    if @user.update(user_params)
 | 
				
			||||||
      redirect_to @user
 | 
					      redirect_to contests_path, notice: t("users.edit.notice")
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      render :edit, status: :unprocessable_entity
 | 
					      render :edit, status: :unprocessable_entity
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def show
 | 
					  def show
 | 
				
			||||||
 | 
					    authorize @user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    redirect_to edit_user_path(@user)
 | 
					    redirect_to edit_user_path(@user)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def new
 | 
					  def new
 | 
				
			||||||
 | 
					    authorize :user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @user = User.new()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def create
 | 
				
			||||||
 | 
					    authorize :user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @user = User.new(user_params)
 | 
				
			||||||
 | 
					    if @user.save
 | 
				
			||||||
 | 
					      redirect_to users_path, notice: t("users.new.notice")
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      render :new, status: :unprocessable_entity
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def destroy
 | 
					  def destroy
 | 
				
			||||||
 | 
					    authorize @user
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
@@ -34,6 +55,6 @@ class UsersController < ApplicationController
 | 
				
			|||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def user_params
 | 
					  def user_params
 | 
				
			||||||
    params.expect(user: [ :username, :email_address ])
 | 
					    params.expect(user: [ :username, :email_address, :lang, :password ])
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								app/helpers/completions_helper.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								app/helpers/completions_helper.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					module CompletionsHelper
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										2
									
								
								app/helpers/contestants_helper.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								app/helpers/contestants_helper.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					module ContestantsHelper
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -1,2 +1,20 @@
 | 
				
			|||||||
module ContestsHelper
 | 
					module ContestsHelper
 | 
				
			||||||
 | 
					  def pad(n)
 | 
				
			||||||
 | 
					    if n > 9
 | 
				
			||||||
 | 
					      return n.to_s
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					    "0" + n.to_s
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def display_time(time)
 | 
				
			||||||
 | 
					    h = time / 3600
 | 
				
			||||||
 | 
					    m = (time % 3600) / 60
 | 
				
			||||||
 | 
					    s = (time % 3600) % 60
 | 
				
			||||||
 | 
					    if h > 0
 | 
				
			||||||
 | 
					      return h.to_s + ":" + pad(m) + ":" + pad(s)
 | 
				
			||||||
 | 
					    elsif m > 0
 | 
				
			||||||
 | 
					      return m.to_s + ":" + pad(s)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					    "0:" + pad(s)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										14
									
								
								app/lib/forms.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								app/lib/forms.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					module Forms
 | 
				
			||||||
 | 
					  class CsvConversionForm
 | 
				
			||||||
 | 
					    include ActiveModel::Model
 | 
				
			||||||
 | 
					    include ActiveModel::Attributes
 | 
				
			||||||
 | 
					    include ActiveModel::Validations::Callbacks
 | 
				
			||||||
 | 
					    include ActiveRecord::Transactions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    attribute :name_column, :integer
 | 
				
			||||||
 | 
					    attribute :email_column, :integer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    validates :name_column, presence: true
 | 
				
			||||||
 | 
					    validates_numericality_of :name_column, greater_than: -1
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										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
 | 
				
			||||||
							
								
								
									
										24
									
								
								app/models/category.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								app/models/category.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					# == 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)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					class Category < ApplicationRecord
 | 
				
			||||||
 | 
					  belongs_to :contest
 | 
				
			||||||
 | 
					  has_and_belongs_to_many :contestants
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  validates :name, presence: true
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										68
									
								
								app/models/completion.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								app/models/completion.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
				
			|||||||
 | 
					# == Schema Information
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Table name: completions
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  id                      :integer          not null, primary key
 | 
				
			||||||
 | 
					#  display_relative_time   :string
 | 
				
			||||||
 | 
					#  display_time_from_start :string
 | 
				
			||||||
 | 
					#  remaining_pieces        :integer
 | 
				
			||||||
 | 
					#  time_seconds            :integer
 | 
				
			||||||
 | 
					#  created_at              :datetime         not null
 | 
				
			||||||
 | 
					#  updated_at              :datetime         not null
 | 
				
			||||||
 | 
					#  contest_id              :integer          not null
 | 
				
			||||||
 | 
					#  contestant_id           :integer          not null
 | 
				
			||||||
 | 
					#  message_id              :integer
 | 
				
			||||||
 | 
					#  puzzle_id               :integer          not null
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Indexes
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  index_completions_on_contest_id     (contest_id)
 | 
				
			||||||
 | 
					#  index_completions_on_contestant_id  (contestant_id)
 | 
				
			||||||
 | 
					#  index_completions_on_message_id     (message_id)
 | 
				
			||||||
 | 
					#  index_completions_on_puzzle_id      (puzzle_id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Foreign Keys
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  contest_id     (contest_id => contests.id)
 | 
				
			||||||
 | 
					#  contestant_id  (contestant_id => contestants.id)
 | 
				
			||||||
 | 
					#  message_id     (message_id => messages.id)
 | 
				
			||||||
 | 
					#  puzzle_id      (puzzle_id => puzzles.id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					class Completion < ApplicationRecord
 | 
				
			||||||
 | 
					  belongs_to :contest
 | 
				
			||||||
 | 
					  belongs_to :contestant
 | 
				
			||||||
 | 
					  belongs_to :puzzle
 | 
				
			||||||
 | 
					  belongs_to :message, optional: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  before_save :add_time_seconds
 | 
				
			||||||
 | 
					  before_save :nullify_display_time
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  validates :display_time_from_start, presence: true, format: { with: /\A(((\d\d|\d):\d\d|\d\d|\d):\d\d|\d\d|\d)\z/ }, if: -> { remaining_pieces == nil }
 | 
				
			||||||
 | 
					  validates :contestant_id, uniqueness: { scope: :puzzle }, if: -> { contest.puzzles.size == 1 }
 | 
				
			||||||
 | 
					  validates :puzzle_id, uniqueness: { scope: :contestant }, if: -> { contest.puzzles.size > 1 }
 | 
				
			||||||
 | 
					  validate :remaining_pieces_is_correct
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def remaining_pieces_is_correct
 | 
				
			||||||
 | 
					    if self.remaining_pieces && self.remaining_pieces > self.puzzle.pieces
 | 
				
			||||||
 | 
					      errors.add(:remaining_pieces, "Cannot be greater than the number of pieces for this puzzle")
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def nullify_display_time
 | 
				
			||||||
 | 
					    if self.remaining_pieces
 | 
				
			||||||
 | 
					      self.display_time_from_start = nil
 | 
				
			||||||
 | 
					      self.display_relative_time = nil
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def add_time_seconds
 | 
				
			||||||
 | 
					    arr = display_time_from_start.split(":")
 | 
				
			||||||
 | 
					    if arr.size == 3
 | 
				
			||||||
 | 
					      self.time_seconds = arr[0].to_i * 3600 + arr[1].to_i * 60 + arr[2].to_i
 | 
				
			||||||
 | 
					    elsif arr.size == 2
 | 
				
			||||||
 | 
					      self.time_seconds = arr[0].to_i * 60 + arr[1].to_i
 | 
				
			||||||
 | 
					    elsif arr.size == 1
 | 
				
			||||||
 | 
					      self.time_seconds = arr[0].to_i
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -1,3 +1,43 @@
 | 
				
			|||||||
 | 
					# == Schema Information
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Table name: contests
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  id                 :integer          not null, primary key
 | 
				
			||||||
 | 
					#  allow_registration :boolean          default(FALSE)
 | 
				
			||||||
 | 
					#  lang               :string           default("en")
 | 
				
			||||||
 | 
					#  name               :string
 | 
				
			||||||
 | 
					#  offline_form       :boolean          default(FALSE)
 | 
				
			||||||
 | 
					#  public             :boolean          default(FALSE)
 | 
				
			||||||
 | 
					#  slug               :string
 | 
				
			||||||
 | 
					#  team               :boolean          default(FALSE)
 | 
				
			||||||
 | 
					#  created_at         :datetime         not null
 | 
				
			||||||
 | 
					#  updated_at         :datetime         not null
 | 
				
			||||||
 | 
					#  user_id            :integer          not null
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Indexes
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  index_contests_on_slug     (slug) UNIQUE
 | 
				
			||||||
 | 
					#  index_contests_on_user_id  (user_id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Foreign Keys
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  user_id  (user_id => users.id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
class Contest < ApplicationRecord
 | 
					class Contest < ApplicationRecord
 | 
				
			||||||
 | 
					  extend FriendlyId
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  belongs_to :user
 | 
					  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
 | 
				
			||||||
 | 
					  has_many :offlines, dependent: :destroy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  friendly_id :name, use: :slugged
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  validates :name, presence: true
 | 
				
			||||||
 | 
					  validates :lang, inclusion: { in: Languages::AVAILABLE_LANGUAGES.map { |lang| lang[:id] } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  generates_token_for :token
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										47
									
								
								app/models/contestant.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								app/models/contestant.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					# == Schema Information
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Table name: contestants
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  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
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  index_contestants_on_contest_id  (contest_id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Foreign Keys
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  contest_id  (contest_id => contests.id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					class Contestant < ApplicationRecord
 | 
				
			||||||
 | 
					  belongs_to :contest
 | 
				
			||||||
 | 
					  has_many :completions, dependent: :destroy
 | 
				
			||||||
 | 
					  has_and_belongs_to_many :categories
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  before_validation :initialize_time_seconds_if_empty
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  validates :name, presence: true
 | 
				
			||||||
 | 
					  validates :time_seconds, presence: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def form_name
 | 
				
			||||||
 | 
					    if email.present?
 | 
				
			||||||
 | 
					      "#{name} - #{email}"
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      name
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def initialize_time_seconds_if_empty
 | 
				
			||||||
 | 
					    if !self.time_seconds
 | 
				
			||||||
 | 
					      self.time_seconds = 0
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										51
									
								
								app/models/csv_import.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								app/models/csv_import.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
				
			|||||||
 | 
					# == Schema Information
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Table name: csv_imports
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  id         :integer          not null, primary key
 | 
				
			||||||
 | 
					#  content    :string           not null
 | 
				
			||||||
 | 
					#  separator  :string           not null
 | 
				
			||||||
 | 
					#  created_at :datetime         not null
 | 
				
			||||||
 | 
					#  updated_at :datetime         not null
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					class CsvImport < ApplicationRecord
 | 
				
			||||||
 | 
					  enum :separator, { comma: ",", semicolon: ";" }, suffix: true, default: :comma
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  has_one_attached :file
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  validates :file, presence: true
 | 
				
			||||||
 | 
					  validate :acceptable_csv, on: :create
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  before_save :read_csv
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def acceptable_csv
 | 
				
			||||||
 | 
					    return unless file.attached?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if file.blob.byte_size > 5 * 1024 * 1024
 | 
				
			||||||
 | 
					      errors.add(:file, "this csv file is too large, it must be under 5MB")
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if file.content_type != "text/csv"
 | 
				
			||||||
 | 
					      errors.add(:file, :not_a_csv_file)
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    begin
 | 
				
			||||||
 | 
					      csv = CSV.read(attachment_changes["file"].attachable.path, col_sep: separator_for_database)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      errors.add(:file, :empty) if csv.count < 1 || (csv.count == 1 && csv[0].count == 1 && csv[0][0] == "")
 | 
				
			||||||
 | 
					    rescue CSV::MalformedCSVError => e
 | 
				
			||||||
 | 
					      errors.add(:file, e.message)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def read_csv
 | 
				
			||||||
 | 
					    self.content = JSON.dump(CSV.read(attachment_changes["file"].attachable.path, col_sep: separator_for_database))
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def options_for_separator
 | 
				
			||||||
 | 
					    keys = self.class.separators.keys
 | 
				
			||||||
 | 
					    keys.map(&:humanize).zip(keys).to_h
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										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
 | 
				
			||||||
							
								
								
									
										46
									
								
								app/models/offline.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								app/models/offline.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					# == Schema Information
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Table name: offlines
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  id         :integer          not null, primary key
 | 
				
			||||||
 | 
					#  completed  :boolean
 | 
				
			||||||
 | 
					#  end_time   :datetime
 | 
				
			||||||
 | 
					#  name       :string           not null
 | 
				
			||||||
 | 
					#  start_time :datetime         not null
 | 
				
			||||||
 | 
					#  created_at :datetime         not null
 | 
				
			||||||
 | 
					#  updated_at :datetime         not null
 | 
				
			||||||
 | 
					#  contest_id :integer          not null
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Indexes
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  index_offlines_on_contest_id  (contest_id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Foreign Keys
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  contest_id  (contest_id => contests.id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					class Offline < ApplicationRecord
 | 
				
			||||||
 | 
					  belongs_to :contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  has_many_attached :images
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  generates_token_for :token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  validates :name, presence: true
 | 
				
			||||||
 | 
					  validates :start_time, presence: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  validate :end_image_is_present
 | 
				
			||||||
 | 
					  validate :start_image_is_present
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def end_image_is_present
 | 
				
			||||||
 | 
					    if self.completed && self.images.length < 2
 | 
				
			||||||
 | 
					      errors.add(:end_image, I18n.t("activerecord.errors.models.offline.attributes.end_image.blank"))
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def start_image_is_present
 | 
				
			||||||
 | 
					    if !self.images.attached?
 | 
				
			||||||
 | 
					      errors.add(:images, I18n.t("activerecord.errors.models.offline.attributes.start_image.blank"))
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -1,4 +1,29 @@
 | 
				
			|||||||
 | 
					# == Schema Information
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Table name: puzzles
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  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
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Indexes
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  index_puzzles_on_contest_id  (contest_id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Foreign Keys
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  contest_id  (contest_id => contests.id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
class Puzzle < ApplicationRecord
 | 
					class Puzzle < ApplicationRecord
 | 
				
			||||||
 | 
					  belongs_to :contest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  has_many :completions, dependent: :destroy
 | 
				
			||||||
  has_one_attached :image
 | 
					  has_one_attached :image
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  validates :name, presence: true
 | 
					  validates :name, presence: true
 | 
				
			||||||
 | 
					  validates :pieces, presence: true
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,22 @@
 | 
				
			|||||||
 | 
					# == Schema Information
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Table name: sessions
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  id         :integer          not null, primary key
 | 
				
			||||||
 | 
					#  ip_address :string
 | 
				
			||||||
 | 
					#  user_agent :string
 | 
				
			||||||
 | 
					#  created_at :datetime         not null
 | 
				
			||||||
 | 
					#  updated_at :datetime         not null
 | 
				
			||||||
 | 
					#  user_id    :integer          not null
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Indexes
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  index_sessions_on_user_id  (user_id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Foreign Keys
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  user_id  (user_id => users.id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
class Session < ApplicationRecord
 | 
					class Session < ApplicationRecord
 | 
				
			||||||
  belongs_to :user
 | 
					  belongs_to :user
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,20 @@
 | 
				
			|||||||
 | 
					# == Schema Information
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Table name: users
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  id              :integer          not null, primary key
 | 
				
			||||||
 | 
					#  admin           :boolean          default(FALSE), not null
 | 
				
			||||||
 | 
					#  email_address   :string           not null
 | 
				
			||||||
 | 
					#  lang            :string           default("en")
 | 
				
			||||||
 | 
					#  password_digest :string           not null
 | 
				
			||||||
 | 
					#  username        :string
 | 
				
			||||||
 | 
					#  created_at      :datetime         not null
 | 
				
			||||||
 | 
					#  updated_at      :datetime         not null
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Indexes
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  index_users_on_email_address  (email_address) UNIQUE
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
class User < ApplicationRecord
 | 
					class User < ApplicationRecord
 | 
				
			||||||
  has_many :contests, dependent: :destroy
 | 
					  has_many :contests, dependent: :destroy
 | 
				
			||||||
  has_many :sessions, dependent: :destroy
 | 
					  has_many :sessions, dependent: :destroy
 | 
				
			||||||
@@ -6,4 +23,6 @@ class User < ApplicationRecord
 | 
				
			|||||||
  normalizes :email_address, with: ->(e) { e.strip.downcase }
 | 
					  normalizes :email_address, with: ->(e) { e.strip.downcase }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  validates :username, presence: true, uniqueness: true
 | 
					  validates :username, presence: true, uniqueness: true
 | 
				
			||||||
 | 
					  validates :email_address, presence: true, uniqueness: true
 | 
				
			||||||
 | 
					  validates :lang, inclusion: { in: Languages::AVAILABLE_LANGUAGES.map { |lang| lang[:id] } }
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										53
									
								
								app/policies/application_policy.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								app/policies/application_policy.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ApplicationPolicy
 | 
				
			||||||
 | 
					  attr_reader :user, :record
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def initialize(user, record)
 | 
				
			||||||
 | 
					    @user = user
 | 
				
			||||||
 | 
					    @record = record
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def index?
 | 
				
			||||||
 | 
					    false
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def show?
 | 
				
			||||||
 | 
					    false
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def create?
 | 
				
			||||||
 | 
					    false
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def new?
 | 
				
			||||||
 | 
					    create?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def update?
 | 
				
			||||||
 | 
					    false
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def edit?
 | 
				
			||||||
 | 
					    update?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def destroy?
 | 
				
			||||||
 | 
					    false
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  class Scope
 | 
				
			||||||
 | 
					    def initialize(user, scope)
 | 
				
			||||||
 | 
					      @user = user
 | 
				
			||||||
 | 
					      @scope = scope
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def resolve
 | 
				
			||||||
 | 
					      raise NoMethodError, "You must define #resolve in #{self.class}"
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    attr_reader :user, :scope
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										9
									
								
								app/policies/completion_policy.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/policies/completion_policy.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					class CompletionPolicy < ContestPolicy
 | 
				
			||||||
 | 
					  def index?
 | 
				
			||||||
 | 
					    false
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def show?
 | 
				
			||||||
 | 
					    false
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										81
									
								
								app/policies/contest_policy.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								app/policies/contest_policy.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
				
			|||||||
 | 
					class ContestPolicy < ApplicationPolicy
 | 
				
			||||||
 | 
					  def index?
 | 
				
			||||||
 | 
					    true
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def show?
 | 
				
			||||||
 | 
					    record.user.id == user.id || user.admin?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def new?
 | 
				
			||||||
 | 
					    true
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def create?
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def destroy?
 | 
				
			||||||
 | 
					    record.user.id == user.id || user.admin?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def import?
 | 
				
			||||||
 | 
					    record.user.id == user.id || user.admin?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def export?
 | 
				
			||||||
 | 
					    record.user.id == user.id || user.admin?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def offline?
 | 
				
			||||||
 | 
					    record.offline_form
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def offline_new?
 | 
				
			||||||
 | 
					    offline?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def offline_create?
 | 
				
			||||||
 | 
					    offline?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def offline_edit?
 | 
				
			||||||
 | 
					    offline?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def offline_update?
 | 
				
			||||||
 | 
					    offline?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def offline_completed?
 | 
				
			||||||
 | 
					    offline?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def scoreboard?
 | 
				
			||||||
 | 
					    record.public
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def upload_csv?
 | 
				
			||||||
 | 
					    record.user.id == user.id || user.admin?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										9
									
								
								app/policies/contestant_policy.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/policies/contestant_policy.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					class ContestantPolicy < ContestPolicy
 | 
				
			||||||
 | 
					  def index?
 | 
				
			||||||
 | 
					    false
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def show?
 | 
				
			||||||
 | 
					    false
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										9
									
								
								app/policies/puzzle_policy.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/policies/puzzle_policy.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					class PuzzlePolicy < ContestPolicy
 | 
				
			||||||
 | 
					  def index?
 | 
				
			||||||
 | 
					    false
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def show?
 | 
				
			||||||
 | 
					    false
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										29
									
								
								app/policies/user_policy.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								app/policies/user_policy.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					class UserPolicy < ApplicationPolicy
 | 
				
			||||||
 | 
					  def index?
 | 
				
			||||||
 | 
					    user.admin?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def show?
 | 
				
			||||||
 | 
					    user.admin? || user.id == record.id
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def new?
 | 
				
			||||||
 | 
					    user.admin?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def create?
 | 
				
			||||||
 | 
					    user.admin?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def edit?
 | 
				
			||||||
 | 
					    user.admin? || user.id == record.id
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def update?
 | 
				
			||||||
 | 
					    user.admin? || user.id == record.id
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def destroy?
 | 
				
			||||||
 | 
					    user.admin?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										48
									
								
								app/views/completions/_form.html.slim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								app/views/completions/_form.html.slim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					= form_with model: completion, url: url, method: method do |form|
 | 
				
			||||||
 | 
					  - if @message
 | 
				
			||||||
 | 
					    = form.hidden_field :message_id, value: @message.id
 | 
				
			||||||
 | 
					    .row.mb-3
 | 
				
			||||||
 | 
					      .col
 | 
				
			||||||
 | 
					        h4 = t("messages.singular").capitalize
 | 
				
			||||||
 | 
					        .alert.alert-secondary
 | 
				
			||||||
 | 
					          b
 | 
				
			||||||
 | 
					            = @message.author
 | 
				
			||||||
 | 
					          br
 | 
				
			||||||
 | 
					          = @message.text
 | 
				
			||||||
 | 
					  .row
 | 
				
			||||||
 | 
					    .col
 | 
				
			||||||
 | 
					      h4 = t("completions.singular").capitalize
 | 
				
			||||||
 | 
					  .row.mb-3
 | 
				
			||||||
 | 
					    .col
 | 
				
			||||||
 | 
					      .form-floating
 | 
				
			||||||
 | 
					        = form.select :contestant_id, @contestants.map { |contestant| [contestant.form_name, contestant.id] }, {}, class: "form-select"
 | 
				
			||||||
 | 
					        = form.label :contestant_id
 | 
				
			||||||
 | 
					  - if @closest_contestant
 | 
				
			||||||
 | 
					    javascript:
 | 
				
			||||||
 | 
					      el = document.querySelector('select[name="completion[contestant_id]"]');
 | 
				
			||||||
 | 
					      el.value = "#{@closest_contestant.id}"
 | 
				
			||||||
 | 
					  - if @puzzles.size > 1
 | 
				
			||||||
 | 
					    .row.mb-3
 | 
				
			||||||
 | 
					      .col
 | 
				
			||||||
 | 
					        .form-floating
 | 
				
			||||||
 | 
					          = form.select :puzzle_id, @puzzles.map { |puzzle| ["#{puzzle.name} - #{puzzle.brand}", puzzle.id] }, {}, class: "form-select"
 | 
				
			||||||
 | 
					          = form.label :puzzle_id
 | 
				
			||||||
 | 
					  - elsif @puzzles.size == 1
 | 
				
			||||||
 | 
					    = form.hidden_field :puzzle_id, value: @puzzles.first.id
 | 
				
			||||||
 | 
					  - else
 | 
				
			||||||
 | 
					    = form.hidden_field :puzzle_id
 | 
				
			||||||
 | 
					  .row.mb-3
 | 
				
			||||||
 | 
					    .col
 | 
				
			||||||
 | 
					      .form-floating
 | 
				
			||||||
 | 
					        = form.text_field :display_time_from_start, autocomplete: "off", class: "form-control"
 | 
				
			||||||
 | 
					        = form.label :display_time_from_start, class: "required"
 | 
				
			||||||
 | 
					  .row.mb-3
 | 
				
			||||||
 | 
					    .col
 | 
				
			||||||
 | 
					      .form-floating
 | 
				
			||||||
 | 
					        = form.text_field :remaining_pieces, autocomplete: "off", class: "form-control"
 | 
				
			||||||
 | 
					        = form.label :remaining_pieces
 | 
				
			||||||
 | 
					      .form-text
 | 
				
			||||||
 | 
					        = t("activerecord.attributes.completion.remaining_pieces_description")
 | 
				
			||||||
 | 
					  .row
 | 
				
			||||||
 | 
					    .col
 | 
				
			||||||
 | 
					      = form.submit submit_text, class: "btn btn-primary"
 | 
				
			||||||
							
								
								
									
										1
									
								
								app/views/completions/edit.html.slim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								app/views/completions/edit.html.slim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					= render "form", contest: @contest, completion: @completion, submit_text: t("helpers.buttons.save"), method: :patch, url: "/contests/#{@contest.id}/completions/#{@completion.id}"
 | 
				
			||||||
							
								
								
									
										1
									
								
								app/views/completions/new.html.slim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								app/views/completions/new.html.slim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					= render "form", completion: @completion, submit_text: t("helpers.buttons.create"), method: :post, url: "/contests/#{@contest.id}/completions"
 | 
				
			||||||
							
								
								
									
										74
									
								
								app/views/contestants/_form.html.slim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								app/views/contestants/_form.html.slim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
				
			|||||||
 | 
					.row
 | 
				
			||||||
 | 
					  .col
 | 
				
			||||||
 | 
					    h3 Informations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					= form_with model: contestant, url: url, method: method do |form|
 | 
				
			||||||
 | 
					  .row.mb-3
 | 
				
			||||||
 | 
					    .col
 | 
				
			||||||
 | 
					      .form-floating
 | 
				
			||||||
 | 
					        = form.text_field :name, autocomplete: "off", class: "form-control"
 | 
				
			||||||
 | 
					        = form.label :name, class: "required"
 | 
				
			||||||
 | 
					  .row.mb-3
 | 
				
			||||||
 | 
					    .col
 | 
				
			||||||
 | 
					      .form-floating
 | 
				
			||||||
 | 
					        = form.text_field :email, autocomplete: "off", class: "form-control"
 | 
				
			||||||
 | 
					        = form.label :email
 | 
				
			||||||
 | 
					    .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 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
 | 
				
			||||||
 | 
					  .row.mt-5
 | 
				
			||||||
 | 
					    .col
 | 
				
			||||||
 | 
					      h3 Completions
 | 
				
			||||||
 | 
					  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"
 | 
				
			||||||
 | 
					          = t("activerecord.attributes.completion.remaining_pieces")
 | 
				
			||||||
 | 
					        th scope="col"
 | 
				
			||||||
 | 
					          = t("activerecord.attributes.completion.puzzle")
 | 
				
			||||||
 | 
					    tbody
 | 
				
			||||||
 | 
					      - @completions.each do |completion|
 | 
				
			||||||
 | 
					        tr scope="row"
 | 
				
			||||||
 | 
					          td
 | 
				
			||||||
 | 
					            = completion.display_time_from_start
 | 
				
			||||||
 | 
					          - if @contest.puzzles.size > 1
 | 
				
			||||||
 | 
					            td
 | 
				
			||||||
 | 
					              = completion.display_relative_time
 | 
				
			||||||
 | 
					          td
 | 
				
			||||||
 | 
					            = completion.remaining_pieces
 | 
				
			||||||
 | 
					          td
 | 
				
			||||||
 | 
					            - if !completion.puzzle.brand.blank?
 | 
				
			||||||
 | 
					              | #{completion.puzzle.name} - #{completion.puzzle.brand}
 | 
				
			||||||
 | 
					            - else
 | 
				
			||||||
 | 
					              | #{completion.puzzle.name}
 | 
				
			||||||
 | 
					          td
 | 
				
			||||||
 | 
					            a.btn.btn-sm.btn-secondary.me-2 href=edit_contest_completion_path(@contest, completion, contestant_id: contestant.id)
 | 
				
			||||||
 | 
					              = t("helpers.buttons.edit")
 | 
				
			||||||
 | 
					            = link_to t("helpers.buttons.delete"), contest_completion_path(contest, completion, contestant_id: contestant.id),
 | 
				
			||||||
 | 
					                data: { turbo_method: :delete }, class: "btn btn-sm btn-secondary"
 | 
				
			||||||
 | 
					  .row
 | 
				
			||||||
 | 
					    .col
 | 
				
			||||||
 | 
					      a.btn.btn-primary href=new_contest_completion_path(@contest, contestant_id: contestant.id)
 | 
				
			||||||
 | 
					        = 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
									
								
								app/views/contestants/edit.html.slim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								app/views/contestants/edit.html.slim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					= render "form", contest: @contest, contestant: @contestant, submit_text: t("helpers.buttons.save"), method: :patch, url: "/contests/#{@contest.id}/contestants/#{@contestant.id}"
 | 
				
			||||||
							
								
								
									
										4
									
								
								app/views/contestants/export.csv.slim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								app/views/contestants/export.csv.slim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					= CSV.generate_line [t("helpers.rank"), t("activerecord.attributes.contestant.name"), t("activerecord.attributes.contestant.display_time"), t("activerecord.attributes.contestant.completions")]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- @contestants.each_with_index do |contestant, index|
 | 
				
			||||||
 | 
					  = CSV.generate_line([index + 1, contestant.name, contestant.completions.size > 0 && contestant.completions[-1].remaining_pieces ? "#{contestant.completions.map{|completion| completion.puzzle.pieces}.sum - contestant.completions[-1].remaining_pieces}p" : contestant.display_time, contestant.completions.where(remaining_pieces: nil).length])
 | 
				
			||||||
							
								
								
									
										19
									
								
								app/views/contestants/import.html.slim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								app/views/contestants/import.html.slim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					= form_with(model: @csv_import, url: contest_import_path(@contest), html: { novalidate: true }) do |form|
 | 
				
			||||||
 | 
					  .row.g-3
 | 
				
			||||||
 | 
					    .col
 | 
				
			||||||
 | 
					      .mb-3
 | 
				
			||||||
 | 
					        .form-floating
 | 
				
			||||||
 | 
					          = form.file_field :file, class: "form-control", accept: ".csv, text/csv"
 | 
				
			||||||
 | 
					          = form.label :file
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .row.g-3
 | 
				
			||||||
 | 
					    .col
 | 
				
			||||||
 | 
					      .mb-3
 | 
				
			||||||
 | 
					        .form-floating
 | 
				
			||||||
 | 
					          = form.select :separator, @csv_import.options_for_separator, {}, class: "form-select"
 | 
				
			||||||
 | 
					          = form.label :separator
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .row.g-3
 | 
				
			||||||
 | 
					    .col
 | 
				
			||||||
 | 
					      .mb-3
 | 
				
			||||||
 | 
					        = form.submit t("helpers.buttons.import"), class: "btn btn-primary"
 | 
				
			||||||
							
								
								
									
										1
									
								
								app/views/contestants/new.html.slim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								app/views/contestants/new.html.slim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					= 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,22 +1,70 @@
 | 
				
			|||||||
 | 
					h4.mt-5 = t("contests.form.general")
 | 
				
			||||||
= form_with model: contest do |form|
 | 
					= form_with model: contest do |form|
 | 
				
			||||||
  .row.mb-3
 | 
					  .row.mb-3
 | 
				
			||||||
    .col
 | 
					    .col
 | 
				
			||||||
      .form-floating
 | 
					      .form-floating
 | 
				
			||||||
        = form.text_field :name, autocomplete: "off", class: "form-control"
 | 
					        = form.text_field :name, autocomplete: "off", class: "form-control"
 | 
				
			||||||
        = form.label :name, class: "required"
 | 
					        = form.label :name, class: "required"
 | 
				
			||||||
 | 
					  .row.mb-3
 | 
				
			||||||
 | 
					    .col
 | 
				
			||||||
 | 
					      .form-floating
 | 
				
			||||||
 | 
					        = form.select :lang, Languages::AVAILABLE_LANGUAGES.map { |lang| [ lang[:name], lang[:id] ] }, {}, class: "form-select"
 | 
				
			||||||
 | 
					        = form.label :lang
 | 
				
			||||||
 | 
					  .row.mb-3
 | 
				
			||||||
 | 
					    .col
 | 
				
			||||||
 | 
					      .form-check.form-switch
 | 
				
			||||||
 | 
					        = form.check_box :public, class: "form-check-input"
 | 
				
			||||||
 | 
					        = form.label :public
 | 
				
			||||||
 | 
					  .row.mb-3
 | 
				
			||||||
 | 
					    .col
 | 
				
			||||||
 | 
					      .form-check.form-switch
 | 
				
			||||||
 | 
					        = form.check_box :offline_form, class: "form-check-input"
 | 
				
			||||||
 | 
					        = form.label :offline_form
 | 
				
			||||||
  .row.mb-3
 | 
					  .row.mb-3
 | 
				
			||||||
    .col
 | 
					    .col
 | 
				
			||||||
      .form-check.form-switch
 | 
					      .form-check.form-switch
 | 
				
			||||||
        = form.check_box :team, class: "form-check-input"
 | 
					        = form.check_box :team, class: "form-check-input"
 | 
				
			||||||
        = form.label :team
 | 
					        = form.label :team
 | 
				
			||||||
          | Team contest
 | 
					        .form-text = t("activerecord.attributes.contest.team_description")
 | 
				
			||||||
        .form-text For registration and UI display purposes
 | 
					  .row.mb-3 style="display: none"
 | 
				
			||||||
  .row.mb-3
 | 
					 | 
				
			||||||
    .col
 | 
					    .col
 | 
				
			||||||
      .form-check.form-switch
 | 
					      .form-check.form-switch
 | 
				
			||||||
        = form.check_box :allow_registration, class: "form-check-input"
 | 
					        = form.check_box :allow_registration, class: "form-check-input"
 | 
				
			||||||
        = form.label :allow_registration
 | 
					        = form.label :allow_registration
 | 
				
			||||||
        .form-text Generates a registration form for this contest
 | 
					        .form-text = t("activerecord.attributes.contest.allow_registration_description")
 | 
				
			||||||
  .row
 | 
					  .row
 | 
				
			||||||
    .col
 | 
					    .col
 | 
				
			||||||
      = form.submit submit_text, class: "btn btn-primary"
 | 
					      = form.submit submit_text, class: "btn btn-primary"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					h4.mt-5 = t("contests.form.categories")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					= form_with model: Category, url: "/contests/#{@contest.id}/categories" do |form|
 | 
				
			||||||
 | 
					  - if @contest.categories.size > 0
 | 
				
			||||||
 | 
					    .row
 | 
				
			||||||
 | 
					      .col-6
 | 
				
			||||||
 | 
					        table.table.table-striped.table-hover
 | 
				
			||||||
 | 
					          thead
 | 
				
			||||||
 | 
					            tr
 | 
				
			||||||
 | 
					              th
 | 
				
			||||||
 | 
					                = t("activerecord.attributes.category.name")
 | 
				
			||||||
 | 
					              th
 | 
				
			||||||
 | 
					                = t("activerecord.attributes.category.contestant_count")
 | 
				
			||||||
 | 
					          tbody
 | 
				
			||||||
 | 
					            - @contest.categories.each do |category|
 | 
				
			||||||
 | 
					              tr.align-middle scope="row"
 | 
				
			||||||
 | 
					                td
 | 
				
			||||||
 | 
					                  = category.name
 | 
				
			||||||
 | 
					                td
 | 
				
			||||||
 | 
					                  = category.contestants.size
 | 
				
			||||||
 | 
					                td
 | 
				
			||||||
 | 
					                  = link_to t("helpers.buttons.delete"), contest_category_path(@contest, category), data: { turbo_method: :delete }, class: "btn btn-sm btn-danger ms-2"
 | 
				
			||||||
 | 
					  .row.mt-3
 | 
				
			||||||
 | 
					    .col-4
 | 
				
			||||||
 | 
					      .form-floating
 | 
				
			||||||
 | 
					        = form.text_field :name, autocomplete: "off", value: nil, class: "form-control"
 | 
				
			||||||
 | 
					        = form.label :name, class: "required"
 | 
				
			||||||
 | 
					          = t("activerecord.attributes.category.new")
 | 
				
			||||||
 | 
					  .row.mt-3
 | 
				
			||||||
 | 
					    .col
 | 
				
			||||||
 | 
					      = form.submit t("helpers.buttons.add"), class: "btn btn-primary"
 | 
				
			||||||
@@ -1 +1 @@
 | 
				
			|||||||
= render "form", contest: @contest, submit_text: "Save"
 | 
					= render "form", contest: @contest, submit_text: t("helpers.buttons.save")
 | 
				
			||||||
@@ -1,16 +1,28 @@
 | 
				
			|||||||
h4.mb-3 Manage your contests
 | 
					.row
 | 
				
			||||||
 | 
					  .col
 | 
				
			||||||
a.btn.btn-primary.mb-4 href=new_contest_path
 | 
					    h4.mb-3
 | 
				
			||||||
  | Create a new contest
 | 
					      = t("contests.index.manage_contests")
 | 
				
			||||||
 | 
					      .float-end
 | 
				
			||||||
 | 
					        a.btn.btn-primary.mb-4 href=new_contest_path
 | 
				
			||||||
 | 
					          = t("contests.index.new_contest")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.row.row-cols-1.row-cols-md-3.g-4
 | 
					.row.row-cols-1.row-cols-md-3.g-4
 | 
				
			||||||
  - @contests.each do |contest|
 | 
					  - @contests.each do |contest|
 | 
				
			||||||
    .col
 | 
					    .col
 | 
				
			||||||
 | 
					      css:
 | 
				
			||||||
 | 
					        .card:hover { background-color: lightblue; }
 | 
				
			||||||
      .card.h-100
 | 
					      .card.h-100
 | 
				
			||||||
        .card-header
 | 
					        .card-header
 | 
				
			||||||
          = contest.name
 | 
					          = contest.name
 | 
				
			||||||
        .card-body
 | 
					        .card-body
 | 
				
			||||||
          p.card-text
 | 
					          .card-text.mb-2
 | 
				
			||||||
            | Description text comes here.
 | 
					            = "#{contest.puzzles.length} #{t('puzzles.singular')}" if contest.puzzles.length <= 1
 | 
				
			||||||
          a.btn.btn-primary href=contest_path(contest)
 | 
					            = "#{contest.puzzles.length} #{t('puzzles.plural')}" if contest.puzzles.length > 1
 | 
				
			||||||
            | Open
 | 
					            = " - #{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|
 | 
				
			||||||
 | 
					                - if puzzle.image.attached?
 | 
				
			||||||
 | 
					                  = image_tag puzzle.image, style: "max-height: 50px;", class: "mb-2 me-2"
 | 
				
			||||||
 | 
					          a.stretched-link href=contest_path(contest)
 | 
				
			||||||
@@ -1 +1 @@
 | 
				
			|||||||
= render "form", contest: @contest, submit_text: "Create"
 | 
					= render "form", contest: @contest, submit_text: t("helpers.buttons.create")
 | 
				
			||||||
							
								
								
									
										2
									
								
								app/views/contests/offline_already_submitted.html.slim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								app/views/contests/offline_already_submitted.html.slim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					.mt-3
 | 
				
			||||||
 | 
					  = t("offlines.form.already_submitted")
 | 
				
			||||||
							
								
								
									
										5
									
								
								app/views/contests/offline_completed.html.slim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/views/contests/offline_completed.html.slim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					h3 = t("offlines.form.completed_message")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- if @contest.public
 | 
				
			||||||
 | 
					  a.mt-3.btn.btn-success href="/public/#{@contest.slug}"
 | 
				
			||||||
 | 
					    = t("contests.show.open_public_scoreboard")
 | 
				
			||||||
							
								
								
									
										40
									
								
								app/views/contests/offline_edit.html.slim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								app/views/contests/offline_edit.html.slim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					= form_with model: @offline, url: "/public/#{@contest.friendly_id}/offline/#{@offline.generate_token_for(:token)}" do |form|
 | 
				
			||||||
 | 
					  = form.hidden_field :completed
 | 
				
			||||||
 | 
					  h3 = t("offlines.form.start_message")
 | 
				
			||||||
 | 
					  h1 id="display-time" style="font-size: 80px;"
 | 
				
			||||||
 | 
					  javascript:
 | 
				
			||||||
 | 
					    const startTime = #{@offline.start_time.to_i};
 | 
				
			||||||
 | 
					    function updateTime() {
 | 
				
			||||||
 | 
					      const displayTimeEl = document.getElementById('display-time');
 | 
				
			||||||
 | 
					      const s = Math.floor((Date.now() - 1000 * startTime) / 1000);
 | 
				
			||||||
 | 
					      let ss = s % 60;
 | 
				
			||||||
 | 
					      let mm = Math.floor(s / 60) % 60;
 | 
				
			||||||
 | 
					      let hh = Math.floor(s / 3600);
 | 
				
			||||||
 | 
					      displayTimeEl.innerHTML = `${hh < 10 ? `0${hh}` : hh}:${mm < 10 ? `0${mm}` : mm}:${ss < 10 ? `0${ss}` : ss}`;
 | 
				
			||||||
 | 
					      setTimeout(updateTime, 1000);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    setTimeout(updateTime, 5);
 | 
				
			||||||
 | 
					  .row.mt-5.mb-3
 | 
				
			||||||
 | 
					    .col
 | 
				
			||||||
 | 
					      .form-text.mb-1
 | 
				
			||||||
 | 
					        = t("offlines.form.end_image_select")
 | 
				
			||||||
 | 
					      = form.file_field :end_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
 | 
				
			||||||
 | 
					      = form.submit t("helpers.buttons.end"), class: "btn btn-primary"
 | 
				
			||||||
							
								
								
									
										30
									
								
								app/views/contests/offline_new.html.slim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/views/contests/offline_new.html.slim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					= form_with model: @offline, url: "/public/#{@contest.friendly_id}/offline" do |form|
 | 
				
			||||||
 | 
					  .row.mb-3
 | 
				
			||||||
 | 
					    .col
 | 
				
			||||||
 | 
					      .form-floating
 | 
				
			||||||
 | 
					        = form.text_field :name, autocomplete: "off", class: "form-control"
 | 
				
			||||||
 | 
					        = form.label :name, class: "required"
 | 
				
			||||||
 | 
					  .row.mb-3
 | 
				
			||||||
 | 
					    .col
 | 
				
			||||||
 | 
					      .form-text.mb-1
 | 
				
			||||||
 | 
					        = t("offlines.form.start_image_select")
 | 
				
			||||||
 | 
					      = form.file_field :images, 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
 | 
				
			||||||
 | 
					      = form.submit t("helpers.buttons.start"), class: "btn btn-primary"
 | 
				
			||||||
							
								
								
									
										94
									
								
								app/views/contests/scoreboard.html.slim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								app/views/contests/scoreboard.html.slim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,94 @@
 | 
				
			|||||||
 | 
					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.where(remaining_pieces: nil).length
 | 
				
			||||||
 | 
					                td style="position: relative"
 | 
				
			||||||
 | 
					                  - if index > 0 && contestant.time_seconds > 0 && contestant.completions.where(remaining_pieces: nil).size > 0
 | 
				
			||||||
 | 
					                    .relative-time style="position:absolute; margin: 1px 0 0 64px; font-size: 14px; color: grey"
 | 
				
			||||||
 | 
					                      |> +
 | 
				
			||||||
 | 
					                      = display_time(contestant.time_seconds - @contestants[index - 1].time_seconds)
 | 
				
			||||||
 | 
					                  = contestant.completions.size > 0 && contestant.completions[-1].remaining_pieces ? "#{contestant.completions.map{|completion| completion.puzzle.pieces}.sum - contestant.completions[-1].remaining_pieces}p" : contestant.display_time
 | 
				
			||||||
 | 
					    .col-1
 | 
				
			||||||
 | 
					    .col-5
 | 
				
			||||||
 | 
					      - @contest.puzzles.each do |puzzle|
 | 
				
			||||||
 | 
					        = image_tag(puzzle.image, class: "img-fluid ms-3 me-3") if puzzle.image.attached?
 | 
				
			||||||
 | 
					        .mt-3.fs-4 style="margin-left: 15px"
 | 
				
			||||||
 | 
					          = puzzle.name
 | 
				
			||||||
 | 
					        .fs-6 style="margin-left: 15px"
 | 
				
			||||||
 | 
					          b
 | 
				
			||||||
 | 
					            = "#{puzzle.brand} - #{puzzle.pieces}p"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- else
 | 
				
			||||||
 | 
					  .d-flex.flex-column style="height: calc(100vh - 180px)"
 | 
				
			||||||
 | 
					    .d-flex.flex-row.justify-content-center.mb-5
 | 
				
			||||||
 | 
					      - @contest.puzzles.each do |puzzle|
 | 
				
			||||||
 | 
					        = image_tag(puzzle.image, class: "img-fluid ms-3 me-3", style: "max-height: 220px") if puzzle.image.attached?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    = render "category_selector"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .d-flex.flex-column style="overflow-y: auto"
 | 
				
			||||||
 | 
					      table.table.table-striped.table-hover
 | 
				
			||||||
 | 
					        thead
 | 
				
			||||||
 | 
					          tr
 | 
				
			||||||
 | 
					            th scope="col"
 | 
				
			||||||
 | 
					              = t("helpers.rank")
 | 
				
			||||||
 | 
					            th scope="col"
 | 
				
			||||||
 | 
					              = t("activerecord.attributes.contestant.name")
 | 
				
			||||||
 | 
					            - if @contest.puzzles.size > 1
 | 
				
			||||||
 | 
					              th scope="col"
 | 
				
			||||||
 | 
					                = t("activerecord.attributes.contestant.completions")
 | 
				
			||||||
 | 
					            th scope="col"
 | 
				
			||||||
 | 
					              = t("activerecord.attributes.contestant.display_time")
 | 
				
			||||||
 | 
					        tbody
 | 
				
			||||||
 | 
					          - @contestants.each_with_index do |contestant, index|
 | 
				
			||||||
 | 
					            tr scope="row"
 | 
				
			||||||
 | 
					              td
 | 
				
			||||||
 | 
					                = index + 1
 | 
				
			||||||
 | 
					              td
 | 
				
			||||||
 | 
					                = contestant.name
 | 
				
			||||||
 | 
					              - if @contest.puzzles.size > 1
 | 
				
			||||||
 | 
					                td
 | 
				
			||||||
 | 
					                  = contestant.completions.where(remaining_pieces: nil).length
 | 
				
			||||||
 | 
					              td
 | 
				
			||||||
 | 
					                = contestant.completions.size > 0 && contestant.completions[-1].remaining_pieces ? "#{contestant.completions.map{|completion| completion.puzzle.pieces}.sum - contestant.completions[-1].remaining_pieces}p" : contestant.display_time
 | 
				
			||||||
@@ -1,2 +1,176 @@
 | 
				
			|||||||
a.btn.btn-primary href=edit_contest_path(@contest)
 | 
					- if @badges.size > 0 && false
 | 
				
			||||||
  | Edit
 | 
					  .row.mb-4
 | 
				
			||||||
 | 
					    .col
 | 
				
			||||||
 | 
					      .badges style="margin-top: -18px; position: absolute"
 | 
				
			||||||
 | 
					        - @badges.each do |badge|
 | 
				
			||||||
 | 
					          span.badge.text-bg-info.me-2
 | 
				
			||||||
 | 
					            = badge
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
 | 
					    - 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")
 | 
				
			||||||
 | 
					    - if @contest.offline_form
 | 
				
			||||||
 | 
					      a.ms-3.btn.btn-success href="/public/#{@contest.slug}/offline"
 | 
				
			||||||
 | 
					        = t("contests.show.open_offline_form")
 | 
				
			||||||
 | 
					    - else
 | 
				
			||||||
 | 
					      a.ms-3.btn.btn-success.disabled
 | 
				
			||||||
 | 
					        = t("contests.show.offline_form_disabled")
 | 
				
			||||||
 | 
					    button.btn.btn-success.ms-3 onclick="copyExtensionUrlToClipboard()"
 | 
				
			||||||
 | 
					      css:
 | 
				
			||||||
 | 
					        button > svg {
 | 
				
			||||||
 | 
					          margin-right: 2px;
 | 
				
			||||||
 | 
					          margin-top: -3px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16">
 | 
				
			||||||
 | 
					        <path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1z"/>
 | 
				
			||||||
 | 
					        <path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0z"/>
 | 
				
			||||||
 | 
					      </svg>
 | 
				
			||||||
 | 
					      =< t("contests.show.copy_extension_url")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					.row.mb-4 style="height: calc(100vh - 280px)"
 | 
				
			||||||
 | 
					  .col-6.d-flex.flex-column style="height: 100%"
 | 
				
			||||||
 | 
					    .row
 | 
				
			||||||
 | 
					      .col
 | 
				
			||||||
 | 
					        h4
 | 
				
			||||||
 | 
					          = t("contestants.plural").capitalize
 | 
				
			||||||
 | 
					          a.ms-3.btn.btn-sm.btn-primary href=new_contest_contestant_path(@contest) style="margin-top: -3px"
 | 
				
			||||||
 | 
					            | + #{t("helpers.buttons.add")}
 | 
				
			||||||
 | 
					          a.ms-2.btn.btn-sm.btn.btn-primary href=contest_import_path(@contest) style="margin-top: -3px"
 | 
				
			||||||
 | 
					            | #{t("helpers.buttons.import")}
 | 
				
			||||||
 | 
					          a.ms-2.btn.btn-sm.btn.btn-primary href="/contests/#{@contest.id}/export.csv" style="margin-top: -3px"
 | 
				
			||||||
 | 
					            | #{t("helpers.buttons.export")}
 | 
				
			||||||
 | 
					    - if @contest.categories.size > 0
 | 
				
			||||||
 | 
					      .row
 | 
				
			||||||
 | 
					        .col
 | 
				
			||||||
 | 
					          select.mt-2.mb-2 id="categories" style="padding: 5px"
 | 
				
			||||||
 | 
					            option value=-1
 | 
				
			||||||
 | 
					              | Tous.tes les participant.e.s
 | 
				
			||||||
 | 
					            option value=-2
 | 
				
			||||||
 | 
					              | Participant.e.s sans catégorie
 | 
				
			||||||
 | 
					            - @contest.categories.each do |category|
 | 
				
			||||||
 | 
					              option value=category.id
 | 
				
			||||||
 | 
					                = category.name
 | 
				
			||||||
 | 
					          javascript:
 | 
				
			||||||
 | 
					            categorySelectEl = document.getElementById('categories');
 | 
				
			||||||
 | 
					            urlParams = new URLSearchParams(window.location.search);
 | 
				
			||||||
 | 
					            selectedCategory = urlParams.get('category');
 | 
				
			||||||
 | 
					            Array.from(categorySelectEl.children).forEach((option) => {
 | 
				
			||||||
 | 
					              if (option.value == selectedCategory) option.selected = true;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            categorySelectEl.addEventListener('change', (e) => {
 | 
				
			||||||
 | 
					              window.location.replace(`#{contest_path(@contest)}?category=${e.target.value}`);
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					    .d-flex.flex-column style="overflow-y: auto"
 | 
				
			||||||
 | 
					      table.table.table-striped.table-hover
 | 
				
			||||||
 | 
					        thead
 | 
				
			||||||
 | 
					          tr
 | 
				
			||||||
 | 
					            th
 | 
				
			||||||
 | 
					              = t("helpers.rank")
 | 
				
			||||||
 | 
					            th
 | 
				
			||||||
 | 
					              = t("activerecord.attributes.contestant.name")
 | 
				
			||||||
 | 
					            th
 | 
				
			||||||
 | 
					              = t("activerecord.attributes.contestant.completions")
 | 
				
			||||||
 | 
					            th
 | 
				
			||||||
 | 
					              = t("activerecord.attributes.contestant.display_time")
 | 
				
			||||||
 | 
					        tbody
 | 
				
			||||||
 | 
					          - @contestants.each_with_index do |contestant, index|
 | 
				
			||||||
 | 
					            tr scope="row"
 | 
				
			||||||
 | 
					              td
 | 
				
			||||||
 | 
					                = index + 1
 | 
				
			||||||
 | 
					              td
 | 
				
			||||||
 | 
					                = contestant.name
 | 
				
			||||||
 | 
					              td
 | 
				
			||||||
 | 
					                = contestant.completions.where(remaining_pieces: nil).length
 | 
				
			||||||
 | 
					              td
 | 
				
			||||||
 | 
					                = contestant.completions.size > 0 && contestant.completions[-1].remaining_pieces ? "#{contestant.completions.map{|completion| completion.puzzle.pieces}.sum - contestant.completions[-1].remaining_pieces}p" : contestant.display_time
 | 
				
			||||||
 | 
					              td
 | 
				
			||||||
 | 
					                a.btn.btn-sm.btn-secondary href=edit_contest_contestant_path(@contest, contestant)
 | 
				
			||||||
 | 
					                  = t("helpers.buttons.open")
 | 
				
			||||||
 | 
					  .col-6.d-flex.flex-column style="height: 100%"
 | 
				
			||||||
 | 
					    .row
 | 
				
			||||||
 | 
					      .col
 | 
				
			||||||
 | 
					        h4
 | 
				
			||||||
 | 
					          = t("puzzles.plural").capitalize
 | 
				
			||||||
 | 
					          a.ms-3.btn.btn-sm.btn-primary href=new_contest_puzzle_path(@contest) style="margin-top: -3px"
 | 
				
			||||||
 | 
					            | + #{t("helpers.buttons.add")}
 | 
				
			||||||
 | 
					    table.table.table-striped.table-hover
 | 
				
			||||||
 | 
					      thead
 | 
				
			||||||
 | 
					        tr
 | 
				
			||||||
 | 
					          th
 | 
				
			||||||
 | 
					            = t("activerecord.attributes.puzzle.image")
 | 
				
			||||||
 | 
					          th
 | 
				
			||||||
 | 
					            = t("activerecord.attributes.puzzle.name")
 | 
				
			||||||
 | 
					          th
 | 
				
			||||||
 | 
					            = t("activerecord.attributes.puzzle.brand")
 | 
				
			||||||
 | 
					          th
 | 
				
			||||||
 | 
					            = t("activerecord.attributes.puzzle.pieces")
 | 
				
			||||||
 | 
					      tbody
 | 
				
			||||||
 | 
					        - @puzzles.each do |puzzle|
 | 
				
			||||||
 | 
					          tr.align-middle scope="row"
 | 
				
			||||||
 | 
					            td
 | 
				
			||||||
 | 
					              = image_tag(puzzle.image, class: "img-fluid", style: "max-height: 48px;") if puzzle.image.attached?
 | 
				
			||||||
 | 
					            td
 | 
				
			||||||
 | 
					              = puzzle.name
 | 
				
			||||||
 | 
					            td
 | 
				
			||||||
 | 
					              = puzzle.brand
 | 
				
			||||||
 | 
					            td
 | 
				
			||||||
 | 
					              = puzzle.pieces
 | 
				
			||||||
 | 
					            td
 | 
				
			||||||
 | 
					              a.btn.btn-sm.btn-secondary href=edit_contest_puzzle_path(@contest, puzzle)
 | 
				
			||||||
 | 
					                = 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"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,14 +4,49 @@ html
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  body
 | 
					  body
 | 
				
			||||||
    .container.mt-5
 | 
					    .container.mt-5
 | 
				
			||||||
      .float-end style="margin-top: -8px;"
 | 
					      - if @current_user
 | 
				
			||||||
        nav.navbar.bg-body-primary
 | 
					        .float-end style="margin-top: -8px;"
 | 
				
			||||||
          a.navbar-brand href=contests_path
 | 
					          nav.navbar.bg-body-primary
 | 
				
			||||||
            | Home
 | 
					            - if @current_user.admin
 | 
				
			||||||
          a.navbar-brand href=user_path(@current_user)
 | 
					              a.navbar-brand href=users_path class="btn btn-light" style="margin-right: 0"
 | 
				
			||||||
            | Settings
 | 
					                = t("nav.users")
 | 
				
			||||||
          = button_to "Log out", session_path, method: :delete
 | 
					            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
 | 
					      = yield
 | 
				
			||||||
							
								
								
									
										48
									
								
								app/views/puzzles/_form.html.slim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								app/views/puzzles/_form.html.slim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					= 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
 | 
				
			||||||
 | 
					        = form.text_field :name, autocomplete: "off", class: "form-control"
 | 
				
			||||||
 | 
					        = form.label :name, class: "required"
 | 
				
			||||||
 | 
					  .row.mb-3
 | 
				
			||||||
 | 
					    .col
 | 
				
			||||||
 | 
					      .form-floating
 | 
				
			||||||
 | 
					        = form.text_field :brand, autocomplete: "off", class: "form-control"
 | 
				
			||||||
 | 
					        = form.label :brand, class: "required"
 | 
				
			||||||
 | 
					  .row.mb-3
 | 
				
			||||||
 | 
					    .col
 | 
				
			||||||
 | 
					      .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 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
									
								
								app/views/puzzles/edit.html.slim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								app/views/puzzles/edit.html.slim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					= render "form", contest: @contest, puzzle: @puzzle, submit_text: t("helpers.buttons.save"), method: :patch, url: "/contests/#{@contest.id}/puzzles/#{@puzzle.id}"
 | 
				
			||||||
@@ -1,8 +0,0 @@
 | 
				
			|||||||
h1 Puzzles
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
= link_to "New puzzle", new_puzzle_path
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div
 | 
					 | 
				
			||||||
  - @puzzles.each do |puzzle|
 | 
					 | 
				
			||||||
    div
 | 
					 | 
				
			||||||
      = link_to puzzle.name, puzzle
 | 
					 | 
				
			||||||
@@ -1,11 +1 @@
 | 
				
			|||||||
h1 New puzzle
 | 
					= render "form", contest: @contest, puzzle: @puzzle, submit_text: t("helpers.buttons.add"), method: :post, url: "/contests/#{@contest.id}/puzzles"
 | 
				
			||||||
 | 
					 | 
				
			||||||
= form_with model: @puzzle do |form|
 | 
					 | 
				
			||||||
  div
 | 
					 | 
				
			||||||
    = form.label :name
 | 
					 | 
				
			||||||
    = form.text_field :name
 | 
					 | 
				
			||||||
  div
 | 
					 | 
				
			||||||
    = form.label :image, style: "display: block"
 | 
					 | 
				
			||||||
    = form.file_field :image, accept: "image/*"
 | 
					 | 
				
			||||||
  div
 | 
					 | 
				
			||||||
    = form.submit
 | 
					 | 
				
			||||||
@@ -1,7 +0,0 @@
 | 
				
			|||||||
h1 = @puzzle.name
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
= link_to "Back", puzzles_path
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
= image_tag @puzzle.image if @puzzle.image.attached?
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
= button_to "Delete", @puzzle, method: :delete, data: { turbo_confirm: "Are you suuure??" }
 | 
					 | 
				
			||||||
@@ -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,4 +1,7 @@
 | 
				
			|||||||
= form_with model: user do |form|
 | 
					= form_with model: user, method: method do |form|
 | 
				
			||||||
 | 
					  - if method == :patch
 | 
				
			||||||
 | 
					    h4 = t("users.edit.general_section")
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
  .row.mb-3
 | 
					  .row.mb-3
 | 
				
			||||||
    .col
 | 
					    .col
 | 
				
			||||||
      .input-group
 | 
					      .input-group
 | 
				
			||||||
@@ -6,10 +9,34 @@
 | 
				
			|||||||
        .form-floating
 | 
					        .form-floating
 | 
				
			||||||
          = form.text_field :username, autocomplete: "off", class: "form-control"
 | 
					          = form.text_field :username, autocomplete: "off", class: "form-control"
 | 
				
			||||||
          = form.label :username, class: "required"
 | 
					          = form.label :username, class: "required"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .row.mb-3
 | 
					  .row.mb-3
 | 
				
			||||||
    .col
 | 
					    .col
 | 
				
			||||||
      .form-floating
 | 
					      .form-floating
 | 
				
			||||||
        = form.text_field :email_address, autocomplete: "off", class: "form-control", type: "email"
 | 
					        = form.text_field :email_address, autocomplete: "off", class: "form-control"
 | 
				
			||||||
        = form.label :email_address, class: "required"
 | 
					        = form.label :email_address, class: "required"
 | 
				
			||||||
  div
 | 
					
 | 
				
			||||||
    = form.submit "Save", class: "btn btn-primary"
 | 
					  .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 t("helpers.buttons.save"), class: "btn btn-primary"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  - if method == :patch
 | 
				
			||||||
 | 
					    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"
 | 
				
			||||||
 | 
					      = form.submit t("helpers.buttons.save"), class: "btn btn-primary"
 | 
				
			||||||
@@ -1 +1 @@
 | 
				
			|||||||
= render "form", user: @user
 | 
					= render "form", user: @user, method: :patch
 | 
				
			||||||
@@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					table.table.table-striped.table-hover
 | 
				
			||||||
 | 
					  thead
 | 
				
			||||||
 | 
					    tr
 | 
				
			||||||
 | 
					      th scope="col"
 | 
				
			||||||
 | 
					        | Id
 | 
				
			||||||
 | 
					      th scope="col"
 | 
				
			||||||
 | 
					        | Name
 | 
				
			||||||
 | 
					      th scope="col"
 | 
				
			||||||
 | 
					        | Admin?
 | 
				
			||||||
 | 
					      th scope="col"
 | 
				
			||||||
 | 
					        | # contests
 | 
				
			||||||
 | 
					  tbody
 | 
				
			||||||
 | 
					    - @users.each do |user|
 | 
				
			||||||
 | 
					      tr scope="row"
 | 
				
			||||||
 | 
					        td
 | 
				
			||||||
 | 
					          = user.id
 | 
				
			||||||
 | 
					        td
 | 
				
			||||||
 | 
					          = user.username
 | 
				
			||||||
 | 
					        td
 | 
				
			||||||
 | 
					          = user.admin ? "Yes" : "No"
 | 
				
			||||||
 | 
					        td
 | 
				
			||||||
 | 
					          = user.contests.length
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					.row
 | 
				
			||||||
 | 
					  .col
 | 
				
			||||||
 | 
					    a.btn.btn-primary href=new_user_path
 | 
				
			||||||
 | 
					      | New user
 | 
				
			||||||
@@ -1 +1 @@
 | 
				
			|||||||
= render "form", user: @user
 | 
					= render "form", user: @user, method: :post
 | 
				
			||||||
@@ -14,6 +14,7 @@ module PuzzleScoreboard
 | 
				
			|||||||
    # Please, add to the `ignore` list any other `lib` subdirectories that do
 | 
					    # 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.
 | 
					    # not contain `.rb` files, or that should not be reloaded or eager loaded.
 | 
				
			||||||
    # Common ones are `templates`, `generators`, or `middleware`, for example.
 | 
					    # Common ones are `templates`, `generators`, or `middleware`, for example.
 | 
				
			||||||
 | 
					    config.autoload_paths << Rails.root.join("lib")
 | 
				
			||||||
    config.autoload_lib(ignore: %w[assets tasks])
 | 
					    config.autoload_lib(ignore: %w[assets tasks])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Configuration for the application, engines, and railties goes here.
 | 
					    # Configuration for the application, engines, and railties goes here.
 | 
				
			||||||
@@ -23,5 +24,8 @@ module PuzzleScoreboard
 | 
				
			|||||||
    #
 | 
					    #
 | 
				
			||||||
    # config.time_zone = "Central Time (US & Canada)"
 | 
					    # config.time_zone = "Central Time (US & Canada)"
 | 
				
			||||||
    # config.eager_load_paths << Rails.root.join("extras")
 | 
					    # config.eager_load_paths << Rails.root.join("extras")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    config.i18n.default_locale = :en
 | 
				
			||||||
 | 
					    config.i18n.available_locales = [ :en, :fr ]
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										13
									
								
								config/initializers/form_errors.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								config/initializers/form_errors.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					ActionView::Base.field_error_proc = proc do |html_tag, instance|
 | 
				
			||||||
 | 
					  if (html_tag.include? "<label") || (html_tag.include? "<input accept=\"image")
 | 
				
			||||||
 | 
					    appended_html = ""
 | 
				
			||||||
 | 
					    if instance.error_message.is_a?(Array)
 | 
				
			||||||
 | 
					      appended_html = "<div class='error-message form-text'>#{instance.error_message.map(&:capitalize).uniq.join(", ")}</div>"
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      appended_html = "<div class='error-message form-text'>#{instance.error_message.capitalize}</div>"
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					    html_tag + appended_html.html_safe
 | 
				
			||||||
 | 
					  else
 | 
				
			||||||
 | 
					    html_tag
 | 
				
			||||||
 | 
					  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,256 @@
 | 
				
			|||||||
#       enabled: "ON"
 | 
					#       enabled: "ON"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
en:
 | 
					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
 | 
				
			||||||
 | 
					        remaining_pieces: Remaining pieces
 | 
				
			||||||
 | 
					        remaining_pieces_description: When this field is filled, the above time will not be taken into account
 | 
				
			||||||
 | 
					      contest:
 | 
				
			||||||
 | 
					        lang: Language for the public scoreboard
 | 
				
			||||||
 | 
					        name: Name
 | 
				
			||||||
 | 
					        offline_form: Enable the offline participation form
 | 
				
			||||||
 | 
					        offline_form_description: Offline participants will have to fill the form by providing an image taken of the undone puzzle, and validate their finish time with an upload of an image of the completed puzzle
 | 
				
			||||||
 | 
					        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
 | 
				
			||||||
 | 
					      offline:
 | 
				
			||||||
 | 
					        name: Your name
 | 
				
			||||||
 | 
					      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"
 | 
				
			||||||
 | 
					        offline:
 | 
				
			||||||
 | 
					          attributes:
 | 
				
			||||||
 | 
					            end_image:
 | 
				
			||||||
 | 
					              blank: Please upload an image
 | 
				
			||||||
 | 
					            name:
 | 
				
			||||||
 | 
					              blank: Please enter a name for your participation
 | 
				
			||||||
 | 
					            start_image:
 | 
				
			||||||
 | 
					              blank: Please upload an image
 | 
				
			||||||
 | 
					        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_offline_form: Open offline form
 | 
				
			||||||
 | 
					      open_public_scoreboard: Open public scoreboard
 | 
				
			||||||
 | 
					      offline_form_disabled: The offline form is disabled
 | 
				
			||||||
 | 
					      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"
 | 
				
			||||||
 | 
					      end: Click here to submit your completion
 | 
				
			||||||
 | 
					      export: Export
 | 
				
			||||||
 | 
					      import: CSV Import
 | 
				
			||||||
 | 
					      open: Open
 | 
				
			||||||
 | 
					      refresh: Refresh
 | 
				
			||||||
 | 
					      sign_in: Sign in
 | 
				
			||||||
 | 
					      save: Save
 | 
				
			||||||
 | 
					      start: Click here to start your participation
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					  offlines:
 | 
				
			||||||
 | 
					    form:
 | 
				
			||||||
 | 
					      already_submitted: You have already completed the puzzle
 | 
				
			||||||
 | 
					      completed_message: Thanks for your participation!
 | 
				
			||||||
 | 
					      end_image_select: Take a photo of your completed puzzle
 | 
				
			||||||
 | 
					      start_image_select: Take a photo of the puzzle with the provided code written on a paper before starting it
 | 
				
			||||||
 | 
					      start_message: Let's go!
 | 
				
			||||||
 | 
					  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"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										254
									
								
								config/locales/fr.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										254
									
								
								config/locales/fr.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,254 @@
 | 
				
			|||||||
 | 
					fr:
 | 
				
			||||||
 | 
					  activemodel:
 | 
				
			||||||
 | 
					    errors:
 | 
				
			||||||
 | 
					      models:
 | 
				
			||||||
 | 
					        forms/csv_conversion_form:
 | 
				
			||||||
 | 
					          attributes:
 | 
				
			||||||
 | 
					            name_column:
 | 
				
			||||||
 | 
					              blank: "Choisir une colonne pour les noms des participant.e.s est nécessaire"
 | 
				
			||||||
 | 
					              greater_than: "Choisir une colonne pour les noms des participant.e.s est nécessaire"
 | 
				
			||||||
 | 
					  activerecord:
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      category:
 | 
				
			||||||
 | 
					        contestant_count: Nombre de participant.e.s
 | 
				
			||||||
 | 
					        new: Nouvelle catégorie
 | 
				
			||||||
 | 
					        name: Catégorie
 | 
				
			||||||
 | 
					      completion:
 | 
				
			||||||
 | 
					        contestant_id: Participant.e
 | 
				
			||||||
 | 
					        display_time: Temps
 | 
				
			||||||
 | 
					        display_time_from_start: Temps depuis le début
 | 
				
			||||||
 | 
					        display_relative_time: Temps pour ce puzzle
 | 
				
			||||||
 | 
					        puzzle: Puzzle
 | 
				
			||||||
 | 
					        remaining_pieces: Nombre de pièces restantes
 | 
				
			||||||
 | 
					        remaining_pieces_description: Si ce champ est rempli, le temps ci-dessus ne sera pas pris en compte
 | 
				
			||||||
 | 
					      contest:
 | 
				
			||||||
 | 
					        lang: Langue pour le classement public
 | 
				
			||||||
 | 
					        name: Nom
 | 
				
			||||||
 | 
					        offline_form: Activer le formulaire de participation hors-ligne
 | 
				
			||||||
 | 
					        offline_form_description: Les participant.e.s hors-ligne pourront participer en prenant une photo du puzzle non fait, puis valider leur temps avec une photo du puzzle une fois complété
 | 
				
			||||||
 | 
					        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
 | 
				
			||||||
 | 
					      offline:
 | 
				
			||||||
 | 
					        name: Ton nom ou pseudo
 | 
				
			||||||
 | 
					      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"
 | 
				
			||||||
 | 
					        offline:
 | 
				
			||||||
 | 
					          attributes:
 | 
				
			||||||
 | 
					            end_image:
 | 
				
			||||||
 | 
					              blank: Tu dois inclure cette image pour pouvoir valider ton puzzle complété
 | 
				
			||||||
 | 
					            name:
 | 
				
			||||||
 | 
					              blank: Tu dois entrer un nom pour pouvoir participer
 | 
				
			||||||
 | 
					            start_image:
 | 
				
			||||||
 | 
					              blank: Tu dois inclure cette image pour pouvoir participer
 | 
				
			||||||
 | 
					        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_offline_form: Ouvrir le formulaire hors-ligne
 | 
				
			||||||
 | 
					      open_public_scoreboard: Ouvrir le classement public
 | 
				
			||||||
 | 
					      offline_form_disabled: Le formulaire hors-ligne n'est pas activé
 | 
				
			||||||
 | 
					      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"
 | 
				
			||||||
 | 
					      end: Clique ici pour valider ta complétion du puzzle
 | 
				
			||||||
 | 
					      export: Exporter
 | 
				
			||||||
 | 
					      import: Importer un CSV
 | 
				
			||||||
 | 
					      open: Détails
 | 
				
			||||||
 | 
					      refresh: Rafraîchir
 | 
				
			||||||
 | 
					      sign_in: Se connecter
 | 
				
			||||||
 | 
					      save: Modifier
 | 
				
			||||||
 | 
					      start: Clique ici pour démarrer ta participation
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					  offlines:
 | 
				
			||||||
 | 
					    form:
 | 
				
			||||||
 | 
					      already_submitted: Tu as déjà complété ton puzzle
 | 
				
			||||||
 | 
					      completed_message: Merci pour ta participation !
 | 
				
			||||||
 | 
					      end_image_select: Prends une photo du puzzle une fois complété
 | 
				
			||||||
 | 
					      start_image_select: Prends une photo du puzzle avant de le commencer, avec le code donné par l'organisateur.ice écrit sur du papier
 | 
				
			||||||
 | 
					      start_message: C'est parti !
 | 
				
			||||||
 | 
					  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"
 | 
				
			||||||
@@ -8,9 +8,33 @@ Rails.application.routes.draw do
 | 
				
			|||||||
  # Defines the root path route ("/")
 | 
					  # Defines the root path route ("/")
 | 
				
			||||||
  root "contests#index"
 | 
					  root "contests#index"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  resources :contests
 | 
					  resources :contests do
 | 
				
			||||||
 | 
					    resources :categories, only: [ :create, :destroy ]
 | 
				
			||||||
 | 
					    resources :completions
 | 
				
			||||||
 | 
					    resources :contestants
 | 
				
			||||||
 | 
					    resources :puzzles
 | 
				
			||||||
 | 
					    resources :messages, only: :destroy do
 | 
				
			||||||
 | 
					      get "convert", to: "messages#convert"
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					    get "import", to: "contestants#import"
 | 
				
			||||||
 | 
					    post "import", to: "contestants#upload_csv"
 | 
				
			||||||
 | 
					    get "import/:id", to: "contestants#convert_csv"
 | 
				
			||||||
 | 
					    post "import/:id", to: "contestants#finalize_import"
 | 
				
			||||||
 | 
					    get "export", to: "contestants#export"
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
  resources :passwords, param: :token
 | 
					  resources :passwords, param: :token
 | 
				
			||||||
  resources :puzzles
 | 
					 | 
				
			||||||
  resource :session
 | 
					  resource :session
 | 
				
			||||||
  resources :users
 | 
					  resources :users
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  options "connect", to: "messages#cors_preflight_check"
 | 
				
			||||||
 | 
					  options "message", to: "messages#cors_preflight_check"
 | 
				
			||||||
 | 
					  post "connect", to: "messages#connect"
 | 
				
			||||||
 | 
					  post "message", to: "messages#create"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get "public/:id", to: "contests#scoreboard"
 | 
				
			||||||
 | 
					  get "public/:id/offline", to: "contests#offline_new"
 | 
				
			||||||
 | 
					  post "public/:id/offline", to: "contests#offline_create"
 | 
				
			||||||
 | 
					  get "public/:id/offline/:token", to: "contests#offline_edit"
 | 
				
			||||||
 | 
					  patch "public/:id/offline/:token", to: "contests#offline_update"
 | 
				
			||||||
 | 
					  get "public/:id/offline/:token/completed", to: "contests#offline_completed"
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										5
									
								
								db/migrate/20250315124339_add_contest_ref_to_puzzle.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								db/migrate/20250315124339_add_contest_ref_to_puzzle.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					class AddContestRefToPuzzle < ActiveRecord::Migration[8.0]
 | 
				
			||||||
 | 
					  def change
 | 
				
			||||||
 | 
					    add_reference :puzzles, :contest, null: false, foreign_key: true
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										5
									
								
								db/migrate/20250320075601_add_brand_to_puzzle.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								db/migrate/20250320075601_add_brand_to_puzzle.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					class AddBrandToPuzzle < ActiveRecord::Migration[8.0]
 | 
				
			||||||
 | 
					  def change
 | 
				
			||||||
 | 
					    add_column :puzzles, :brand, :string
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										11
									
								
								db/migrate/20250320080142_create_contestants.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								db/migrate/20250320080142_create_contestants.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					class CreateContestants < ActiveRecord::Migration[8.0]
 | 
				
			||||||
 | 
					  def change
 | 
				
			||||||
 | 
					    create_table :contestants do |t|
 | 
				
			||||||
 | 
					      t.string :name
 | 
				
			||||||
 | 
					      t.string :email
 | 
				
			||||||
 | 
					      t.belongs_to :contest, null: false, foreign_key: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      t.timestamps
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										11
									
								
								db/migrate/20250320082658_create_completions.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								db/migrate/20250320082658_create_completions.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					class CreateCompletions < ActiveRecord::Migration[8.0]
 | 
				
			||||||
 | 
					  def change
 | 
				
			||||||
 | 
					    create_table :completions do |t|
 | 
				
			||||||
 | 
					      t.integer :time_seconds
 | 
				
			||||||
 | 
					      t.belongs_to :contestant, null: false, foreign_key: true
 | 
				
			||||||
 | 
					      t.belongs_to :puzzle, null: false, foreign_key: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      t.timestamps
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					class AddContestRefToCompletion < ActiveRecord::Migration[8.0]
 | 
				
			||||||
 | 
					  def change
 | 
				
			||||||
 | 
					    add_reference :completions, :contest, null: false, foreign_key: true
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										5
									
								
								db/migrate/20250322071308_add_admin_to_user.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								db/migrate/20250322071308_add_admin_to_user.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					class AddAdminToUser < ActiveRecord::Migration[8.0]
 | 
				
			||||||
 | 
					  def change
 | 
				
			||||||
 | 
					    add_column :users, :admin, :boolean, null: false, default: false
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										5
									
								
								db/migrate/20250322164205_add_slug_to_contest.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								db/migrate/20250322164205_add_slug_to_contest.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					class AddSlugToContest < ActiveRecord::Migration[8.0]
 | 
				
			||||||
 | 
					  def change
 | 
				
			||||||
 | 
					    add_column :contests, :slug, :string
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user