diff options
author | Marius Peter <marius.peter@tutanota.com> | 2024-12-29 15:14:43 +0100 |
---|---|---|
committer | Marius Peter <marius.peter@tutanota.com> | 2024-12-29 15:14:43 +0100 |
commit | be2a93525069de2dfa3c23b0c23e7a9f7ad4c03d (patch) | |
tree | b5493e9d35d024ce7be072ec2168b4a98ba0e63f |
First commit.
150 files changed, 3457 insertions, 0 deletions
diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7540593 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,47 @@ +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. + +# Ignore git directory. +/.git/ +/.gitignore + +# Ignore bundler config. +/.bundle + +# Ignore all environment files. +/.env* + +# Ignore all default key files. +/config/master.key +/config/credentials/*.key + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/.keep + +# Ignore assets. +/node_modules/ +/app/assets/builds/* +!/app/assets/builds/.keep +/public/assets + +# Ignore CI service files. +/.github + +# Ignore development files +/.devcontainer + +# Ignore Docker-related files +/.dockerignore +/Dockerfile* diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8dc4323 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored +config/credentials/*.yml.enc diff=rails_credentials +config/credentials.yml.enc diff=rails_credentials diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f92525c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# Temporary files generated by your text editor or operating system +# belong in git's global ignore instead: +# `$XDG_CONFIG_HOME/git/ignore` or `~/.config/git/ignore` + +# Ignore bundler config. +/.bundle + +# Ignore all environment files. +/.env* + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep + +/public/assets + +# Ignore master key for decrypting credentials and more. +/config/master.key diff --git a/.kamal/hooks/docker-setup.sample b/.kamal/hooks/docker-setup.sample new file mode 100755 index 0000000..2fb07d7 --- /dev/null +++ b/.kamal/hooks/docker-setup.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Docker set up on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-deploy.sample b/.kamal/hooks/post-deploy.sample new file mode 100755 index 0000000..75efafc --- /dev/null +++ b/.kamal/hooks/post-deploy.sample @@ -0,0 +1,14 @@ +#!/bin/sh + +# A sample post-deploy hook +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" diff --git a/.kamal/hooks/post-proxy-reboot.sample b/.kamal/hooks/post-proxy-reboot.sample new file mode 100755 index 0000000..1435a67 --- /dev/null +++ b/.kamal/hooks/post-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooted kamal-proxy on $KAMAL_HOSTS" diff --git a/.kamal/hooks/pre-build.sample b/.kamal/hooks/pre-build.sample new file mode 100755 index 0000000..f87d811 --- /dev/null +++ b/.kamal/hooks/pre-build.sample @@ -0,0 +1,51 @@ +#!/bin/sh + +# A sample pre-build hook +# +# Checks: +# 1. We have a clean checkout +# 2. A remote is configured +# 3. The branch has been pushed to the remote +# 4. The version we are deploying matches the remote +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) + +if [ -n "$(git status --porcelain)" ]; then + echo "Git checkout is not clean, aborting..." >&2 + git status --porcelain >&2 + exit 1 +fi + +first_remote=$(git remote) + +if [ -z "$first_remote" ]; then + echo "No git remote set, aborting..." >&2 + exit 1 +fi + +current_branch=$(git branch --show-current) + +if [ -z "$current_branch" ]; then + echo "Not on a git branch, aborting..." >&2 + exit 1 +fi + +remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) + +if [ -z "$remote_head" ]; then + echo "Branch not pushed to remote, aborting..." >&2 + exit 1 +fi + +if [ "$KAMAL_VERSION" != "$remote_head" ]; then + echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 + exit 1 +fi + +exit 0 diff --git a/.kamal/hooks/pre-connect.sample b/.kamal/hooks/pre-connect.sample new file mode 100755 index 0000000..18e61d7 --- /dev/null +++ b/.kamal/hooks/pre-connect.sample @@ -0,0 +1,47 @@ +#!/usr/bin/env ruby + +# A sample pre-connect check +# +# Warms DNS before connecting to hosts in parallel +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +hosts = ENV["KAMAL_HOSTS"].split(",") +results = nil +max = 3 + +elapsed = Benchmark.realtime do + results = hosts.map do |host| + Thread.new do + tries = 1 + + begin + Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) + rescue SocketError + if tries < max + puts "Retrying DNS warmup: #{host}" + tries += 1 + sleep rand + retry + else + puts "DNS warmup failed: #{host}" + host + end + end + + tries + end + end.map(&:value) +end + +retries = results.sum - hosts.size +nopes = results.count { |r| r == max } + +puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] diff --git a/.kamal/hooks/pre-deploy.sample b/.kamal/hooks/pre-deploy.sample new file mode 100755 index 0000000..1b280c7 --- /dev/null +++ b/.kamal/hooks/pre-deploy.sample @@ -0,0 +1,109 @@ +#!/usr/bin/env ruby + +# A sample pre-deploy hook +# +# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. +# +# Fails unless the combined status is "success" +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_COMMAND +# KAMAL_SUBCOMMAND +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) + +# Only check the build status for production deployments +if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" + exit 0 +end + +require "bundler/inline" + +# true = install gems so this is fast on repeat invocations +gemfile(true, quiet: true) do + source "https://rubygems.org" + + gem "octokit" + gem "faraday-retry" +end + +MAX_ATTEMPTS = 72 +ATTEMPTS_GAP = 10 + +def exit_with_error(message) + $stderr.puts message + exit 1 +end + +class GithubStatusChecks + attr_reader :remote_url, :git_sha, :github_client, :combined_status + + def initialize + @remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/") + @git_sha = `git rev-parse HEAD`.strip + @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) + refresh! + end + + def refresh! + @combined_status = github_client.combined_status(remote_url, git_sha) + end + + def state + combined_status[:state] + end + + def first_status_url + first_status = combined_status[:statuses].find { |status| status[:state] == state } + first_status && first_status[:target_url] + end + + def complete_count + combined_status[:statuses].count { |status| status[:state] != "pending"} + end + + def total_count + combined_status[:statuses].count + end + + def current_status + if total_count > 0 + "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." + else + "Build not started..." + end + end +end + + +$stdout.sync = true + +puts "Checking build status..." +attempts = 0 +checks = GithubStatusChecks.new + +begin + loop do + case checks.state + when "success" + puts "Checks passed, see #{checks.first_status_url}" + exit 0 + when "failure" + exit_with_error "Checks failed, see #{checks.first_status_url}" + when "pending" + attempts += 1 + end + + exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS + + puts checks.current_status + sleep(ATTEMPTS_GAP) + checks.refresh! + end +rescue Octokit::NotFound + exit_with_error "Build status could not be found" +end diff --git a/.kamal/hooks/pre-proxy-reboot.sample b/.kamal/hooks/pre-proxy-reboot.sample new file mode 100755 index 0000000..061f805 --- /dev/null +++ b/.kamal/hooks/pre-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." diff --git a/.kamal/secrets b/.kamal/secrets new file mode 100644 index 0000000..9a771a3 --- /dev/null +++ b/.kamal/secrets @@ -0,0 +1,17 @@ +# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets, +# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either +# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. + +# Example of extracting secrets from 1password (or another compatible pw manager) +# SECRETS=$(kamal secrets fetch --adapter 1password --account your-account --from Vault/Item KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY) +# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS}) +# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY ${SECRETS}) + +# Use a GITHUB_TOKEN if private repositories are needed for the image +# GITHUB_TOKEN=$(gh config get -h github.com oauth_token) + +# Grab the registry password from ENV +KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD + +# Improve security by using a password manager. Never check config/master.key into git! +RAILS_MASTER_KEY=$(cat config/master.key) diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..f9d86d4 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,8 @@ +# Omakase Ruby styling for Rails +inherit_gem: { rubocop-rails-omakase: rubocop.yml } + +# Overwrite or add rules to create your own house style +# +# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` +# Layout/SpaceInsideArrayLiteralBrackets: +# Enabled: false diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..e391e18 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-3.3.6 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2d0c287 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,72 @@ +# syntax=docker/dockerfile:1 +# check=error=true + +# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: +# docker build -t wtt . +# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name wtt wtt + +# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html + +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version +ARG RUBY_VERSION=3.3.6 +FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base + +# Rails app lives here +WORKDIR /rails + +# Install base packages +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Set production environment +ENV RAILS_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development" + +# Throw-away build stage to reduce size of final image +FROM base AS build + +# Install packages needed to build gems +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential git pkg-config && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Install application gems +COPY Gemfile Gemfile.lock ./ +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ + bundle exec bootsnap precompile --gemfile + +# Copy application code +COPY . . + +# Precompile bootsnap code for faster boot times +RUN bundle exec bootsnap precompile app/ lib/ + +# Precompiling assets for production without requiring secret RAILS_MASTER_KEY +RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile + + + + +# Final stage for app image +FROM base + +# Copy built artifacts: gems, application +COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --from=build /rails /rails + +# Run and own only the runtime files as a non-root user for security +RUN groupadd --system --gid 1000 rails && \ + useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ + chown -R rails:rails db log storage tmp +USER 1000:1000 + +# Entrypoint prepares the database. +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start server via Thruster by default, this can be overwritten at runtime +EXPOSE 80 +CMD ["./bin/thrust", "./bin/rails", "server"] @@ -0,0 +1,63 @@ +source "https://rubygems.org" + +# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" +gem "rails", "~> 8.0.0" +# The modern asset pipeline for Rails [https://github.com/rails/propshaft] +gem "propshaft" +# Use sqlite3 as the database for Active Record +gem "sqlite3", ">= 2.1" +# Use the Puma web server [https://github.com/puma/puma] +gem "puma", ">= 5.0" +# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] +gem "importmap-rails" +# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] +gem "turbo-rails" +# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] +gem "stimulus-rails" +# Build JSON APIs with ease [https://github.com/rails/jbuilder] +gem "jbuilder" + +# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] +gem "bcrypt", "~> 3.1.7" + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem "tzinfo-data", platforms: %i[ windows jruby ] + +# Use the database-backed adapters for Rails.cache, Active Job, and Action Cable +gem "solid_cache" +gem "solid_queue" +gem "solid_cable" + +# Reduces boot times through caching; required in config/boot.rb +gem "bootsnap", require: false + +# Deploy this application anywhere as a Docker container [https://kamal-deploy.org] +gem "kamal", require: false + +# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/] +gem "thruster", require: false + +# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] +# gem "image_processing", "~> 1.2" + +group :development, :test do + # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem + gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" + + # Static analysis for security vulnerabilities [https://brakemanscanner.org/] + gem "brakeman", require: false + + # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] + gem "rubocop-rails-omakase", require: false +end + +group :development do + # Use console on exceptions pages [https://github.com/rails/web-console] + gem "web-console" +end + +group :test do + # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] + gem "capybara" + gem "selenium-webdriver" +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..0e50bba --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,389 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (8.0.1) + actionpack (= 8.0.1) + activesupport (= 8.0.1) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.0.1) + actionpack (= 8.0.1) + activejob (= 8.0.1) + activerecord (= 8.0.1) + activestorage (= 8.0.1) + activesupport (= 8.0.1) + mail (>= 2.8.0) + actionmailer (8.0.1) + actionpack (= 8.0.1) + actionview (= 8.0.1) + activejob (= 8.0.1) + activesupport (= 8.0.1) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.0.1) + actionview (= 8.0.1) + activesupport (= 8.0.1) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.0.1) + actionpack (= 8.0.1) + activerecord (= 8.0.1) + activestorage (= 8.0.1) + activesupport (= 8.0.1) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.0.1) + activesupport (= 8.0.1) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.0.1) + activesupport (= 8.0.1) + globalid (>= 0.3.6) + activemodel (8.0.1) + activesupport (= 8.0.1) + activerecord (8.0.1) + activemodel (= 8.0.1) + activesupport (= 8.0.1) + timeout (>= 0.4.0) + activestorage (8.0.1) + actionpack (= 8.0.1) + activejob (= 8.0.1) + activerecord (= 8.0.1) + activesupport (= 8.0.1) + marcel (~> 1.0) + activesupport (8.0.1) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + ast (2.4.2) + base64 (0.2.0) + bcrypt (3.1.20) + bcrypt_pbkdf (1.1.1) + bcrypt_pbkdf (1.1.1-arm64-darwin) + bcrypt_pbkdf (1.1.1-x86_64-darwin) + benchmark (0.4.0) + bigdecimal (3.1.8) + bindex (0.8.1) + bootsnap (1.18.4) + msgpack (~> 1.2) + brakeman (6.2.2) + racc + builder (3.3.0) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + concurrent-ruby (1.3.4) + connection_pool (2.4.1) + crass (1.0.6) + date (3.4.1) + debug (1.10.0) + irb (~> 1.10) + reline (>= 0.3.8) + dotenv (3.1.7) + drb (2.2.1) + ed25519 (1.3.0) + erubi (1.13.1) + et-orbi (1.2.11) + tzinfo + fugit (1.11.1) + et-orbi (~> 1, >= 1.2.11) + raabro (~> 1.4) + globalid (1.2.1) + activesupport (>= 6.1) + i18n (1.14.6) + concurrent-ruby (~> 1.0) + importmap-rails (2.1.0) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + io-console (0.8.0) + irb (1.14.3) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jbuilder (2.13.0) + actionview (>= 5.0.0) + activesupport (>= 5.0.0) + json (2.9.1) + kamal (2.4.0) + activesupport (>= 7.0) + base64 (~> 0.2) + bcrypt_pbkdf (~> 1.0) + concurrent-ruby (~> 1.2) + dotenv (~> 3.1) + ed25519 (~> 1.2) + net-ssh (~> 7.3) + sshkit (>= 1.23.0, < 2.0) + thor (~> 1.3) + zeitwerk (>= 2.6.18, < 3.0) + language_server-protocol (3.17.0.3) + logger (1.6.4) + loofah (2.23.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.8.1) + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.0.4) + matrix (0.4.2) + mini_mime (1.1.5) + minitest (5.25.4) + msgpack (1.7.5) + net-imap (0.5.4) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-scp (4.0.0) + net-ssh (>= 2.6.5, < 8.0.0) + net-sftp (4.0.0) + net-ssh (>= 5.0.0, < 8.0.0) + net-smtp (0.5.0) + net-protocol + net-ssh (7.3.0) + nio4r (2.7.4) + nokogiri (1.17.2-aarch64-linux) + racc (~> 1.4) + nokogiri (1.17.2-arm-linux) + racc (~> 1.4) + nokogiri (1.17.2-arm64-darwin) + racc (~> 1.4) + nokogiri (1.17.2-x86-linux) + racc (~> 1.4) + nokogiri (1.17.2-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.17.2-x86_64-linux) + racc (~> 1.4) + ostruct (0.6.1) + parallel (1.26.3) + parser (3.3.6.0) + ast (~> 2.4.1) + racc + propshaft (1.1.0) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack + railties (>= 7.0.0) + psych (5.2.2) + date + stringio + public_suffix (6.0.1) + puma (6.5.0) + nio4r (~> 2.0) + raabro (1.4.0) + racc (1.8.1) + rack (3.1.8) + rack-session (2.0.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.2.1) + rack (>= 3) + rails (8.0.1) + actioncable (= 8.0.1) + actionmailbox (= 8.0.1) + actionmailer (= 8.0.1) + actionpack (= 8.0.1) + actiontext (= 8.0.1) + actionview (= 8.0.1) + activejob (= 8.0.1) + activemodel (= 8.0.1) + activerecord (= 8.0.1) + activestorage (= 8.0.1) + activesupport (= 8.0.1) + bundler (>= 1.15.0) + railties (= 8.0.1) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.2) + 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) + railties (8.0.1) + actionpack (= 8.0.1) + activesupport (= 8.0.1) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.2.1) + rdoc (6.10.0) + psych (>= 4.0.0) + regexp_parser (2.9.3) + reline (0.6.0) + io-console (~> 0.5) + rexml (3.4.0) + rubocop (1.69.2) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.36.2, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.37.0) + parser (>= 3.3.1.0) + rubocop-minitest (0.36.0) + rubocop (>= 1.61, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-performance (1.23.0) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rails (2.27.0) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.52.0, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rails-omakase (1.0.0) + rubocop + rubocop-minitest + rubocop-performance + rubocop-rails + ruby-progressbar (1.13.0) + rubyzip (2.3.2) + securerandom (0.4.1) + selenium-webdriver (4.27.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) + solid_cable (3.0.5) + actioncable (>= 7.2) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_cache (1.0.6) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_queue (1.1.0) + activejob (>= 7.1) + activerecord (>= 7.1) + concurrent-ruby (>= 1.3.1) + fugit (~> 1.11.0) + railties (>= 7.1) + thor (~> 1.3.1) + sqlite3 (2.4.1-aarch64-linux-gnu) + sqlite3 (2.4.1-aarch64-linux-musl) + sqlite3 (2.4.1-arm-linux-gnu) + sqlite3 (2.4.1-arm-linux-musl) + sqlite3 (2.4.1-arm64-darwin) + sqlite3 (2.4.1-x86-linux-gnu) + sqlite3 (2.4.1-x86-linux-musl) + sqlite3 (2.4.1-x86_64-darwin) + sqlite3 (2.4.1-x86_64-linux-gnu) + sqlite3 (2.4.1-x86_64-linux-musl) + sshkit (1.23.2) + base64 + net-scp (>= 1.1.2) + net-sftp (>= 2.1.2) + net-ssh (>= 2.8.0) + ostruct + stimulus-rails (1.3.4) + railties (>= 6.0.0) + stringio (3.1.2) + thor (1.3.2) + thruster (0.1.9) + thruster (0.1.9-aarch64-linux) + thruster (0.1.9-arm64-darwin) + thruster (0.1.9-x86_64-darwin) + thruster (0.1.9-x86_64-linux) + timeout (0.4.3) + turbo-rails (2.0.11) + actionpack (>= 6.0.0) + railties (>= 6.0.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.1.2) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) + uri (1.0.2) + useragent (0.16.11) + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) + bindex (>= 0.4.0) + railties (>= 6.0.0) + websocket (1.2.11) + websocket-driver (0.7.6) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.7.1) + +PLATFORMS + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86-linux + x86-linux-gnu + x86-linux-musl + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + bcrypt (~> 3.1.7) + bootsnap + brakeman + capybara + debug + importmap-rails + jbuilder + kamal + propshaft + puma (>= 5.0) + rails (~> 8.0.0) + rubocop-rails-omakase + selenium-webdriver + solid_cable + solid_cache + solid_queue + sqlite3 (>= 2.1) + stimulus-rails + thruster + turbo-rails + tzinfo-data + web-console + +BUNDLED WITH + 2.5.23 diff --git a/README.md b/README.md new file mode 100644 index 0000000..7db80e4 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# README + +This README would normally document whatever steps are necessary to get the +application up and running. + +Things you may want to cover: + +* Ruby version + +* System dependencies + +* Configuration + +* Database creation + +* Database initialization + +* How to run the test suite + +* Services (job queues, cache servers, search engines, etc.) + +* Deployment instructions + +* ... diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..9a5ea73 --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/app/assets/images/.keep diff --git a/app/assets/images/blason_Savoie.png b/app/assets/images/blason_Savoie.png Binary files differnew file mode 100644 index 0000000..51761d0 --- /dev/null +++ b/app/assets/images/blason_Savoie.png diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css new file mode 100644 index 0000000..d7fc4f9 --- /dev/null +++ b/app/assets/stylesheets/application.css @@ -0,0 +1,27 @@ +/* + * This is a manifest file that'll be compiled into application.css. + * + * With Propshaft, assets are served efficiently without preprocessing steps. You can still include + * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard + * cascading order, meaning styles declared later in the document or manifest will override earlier ones, + * depending on specificity. + * + * Consider organizing styles into separate files for maintainability. + */ + +body { + margin: 0; + font-family: sans-serif; + background: moccasin; +} + +main { + margin: 0 auto; + padding: 0 1em; + max-width: 50em; +} + +h1 { + text-align: center; + /* font-family: 'Old London', sans-serif; */ +} diff --git a/app/assets/stylesheets/components/code_of_honor.css b/app/assets/stylesheets/components/code_of_honor.css new file mode 100644 index 0000000..dbbd267 --- /dev/null +++ b/app/assets/stylesheets/components/code_of_honor.css @@ -0,0 +1,11 @@ +#code-of-honor { + width: 50%; + margin: 1em auto; + padding: 1em; + color: white; + + border-radius: 1em; +} +#code-of-honor.accepted { background-color: forestgreen; } +#code-of-honor.rejected { background-color: firebrick; } + diff --git a/app/assets/stylesheets/components/footer.css b/app/assets/stylesheets/components/footer.css new file mode 100644 index 0000000..de96a5c --- /dev/null +++ b/app/assets/stylesheets/components/footer.css @@ -0,0 +1,6 @@ +footer { + text-align: center; + margin: 3em 0 0; + color: dimgrey; + +} diff --git a/app/assets/stylesheets/components/nav_top.css b/app/assets/stylesheets/components/nav_top.css new file mode 100644 index 0000000..a3247a4 --- /dev/null +++ b/app/assets/stylesheets/components/nav_top.css @@ -0,0 +1,40 @@ +nav#top { + width: 100%; + background-color: firebrick; + display: flex; + justify-content: space-between; + align-items: center; + + ul { + display: flex; + flex-wrap: wrap; + list-style-type: none; + padding: 0; + margin: 0; + } + + li { + padding: 12px 0; + } + + a { + padding: 12px; + color: white; + text-decoration: none; + } + + a:hover { + background-color: tomato; + } + + #authentication { + display: flex; + flex-wrap: wrap; + justify-content: right; + } + + button { + margin: 0.5rem 0.5rem 0.5rem 0; + } +} + diff --git a/app/assets/stylesheets/components/notifications.css b/app/assets/stylesheets/components/notifications.css new file mode 100644 index 0000000..93d8cc2 --- /dev/null +++ b/app/assets/stylesheets/components/notifications.css @@ -0,0 +1,18 @@ +#notifications { + max-width: 30rem; + margin: 1em auto; +} + +.notice, .alert { + margin: 0 1em; + padding: 1em; + border-radius: 1em; +} + +.notice { + background: lightblue; +} + +.alert { + background: tomato; +} diff --git a/app/assets/stylesheets/pages/home.css b/app/assets/stylesheets/pages/home.css new file mode 100644 index 0000000..5b2e6a9 --- /dev/null +++ b/app/assets/stylesheets/pages/home.css @@ -0,0 +1,4 @@ +#blason { + margin: 0 auto; + display: block; +} diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 0000000..4264c74 --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,16 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + identified_by :current_user + + def connect + set_current_user || reject_unauthorized_connection + end + + private + def set_current_user + if session = Session.find_by(id: cookies.signed[:session_id]) + self.current_user = session.user + end + end + end +end diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb new file mode 100644 index 0000000..695c2ca --- /dev/null +++ b/app/controllers/admin/dashboard_controller.rb @@ -0,0 +1,13 @@ +class Admin::DashboardController < ApplicationController + def index + @tartiflettes = Tartiflette.includes(:scores) + end + + private + + def require_admin + unless logged_in? && current_user.admin? + redirect_to root_path, alert: "Access denied." + end + end +end diff --git a/app/controllers/admin/scores_controller.rb b/app/controllers/admin/scores_controller.rb new file mode 100644 index 0000000..b4755e9 --- /dev/null +++ b/app/controllers/admin/scores_controller.rb @@ -0,0 +1,14 @@ +class Admin::ScoresController < ApplicationController + def export + csv_data = TartifletteScoreExportService.generate_csv + send_data csv_data, filename: "scores-#{Date.today}.csv" + end + + private + + def require_admin + unless logged_in? && current_user.admin? + redirect_to root_path, alert: "Access denied." + end + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000..94e7183 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,5 @@ +class ApplicationController < ActionController::Base + include Authentication + # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. + allow_browser versions: :modern +end diff --git a/app/controllers/code_of_honor_controller.rb b/app/controllers/code_of_honor_controller.rb new file mode 100644 index 0000000..d0d9044 --- /dev/null +++ b/app/controllers/code_of_honor_controller.rb @@ -0,0 +1,12 @@ +class CodeOfHonorController < ApplicationController + allow_unauthenticated_access + + def toggle + session[:agreed_to_code_of_honor] = !session[:agreed_to_code_of_honor] + if session[:agreed_to_code_of_honor] + redirect_to root_path, notice: "Vous acceptez le code d'honneur." + else + redirect_to root_path, alert: "Vous n'acceptez pas le code d'honneur." + end + end +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/app/controllers/concerns/.keep diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb new file mode 100644 index 0000000..3538f48 --- /dev/null +++ b/app/controllers/concerns/authentication.rb @@ -0,0 +1,52 @@ +module Authentication + extend ActiveSupport::Concern + + included do + before_action :require_authentication + helper_method :authenticated? + end + + class_methods do + def allow_unauthenticated_access(**options) + skip_before_action :require_authentication, **options + end + end + + private + def authenticated? + resume_session + end + + def require_authentication + resume_session || request_authentication + end + + def resume_session + Current.session ||= find_session_by_cookie + end + + def find_session_by_cookie + Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id] + end + + def request_authentication + session[:return_to_after_authenticating] = request.url + redirect_to new_session_path + end + + def after_authentication_url + session.delete(:return_to_after_authenticating) || root_url + end + + def start_new_session_for(user) + user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session| + Current.session = session + cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax } + end + end + + def terminate_session + Current.session.destroy + cookies.delete(:session_id) + end +end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb new file mode 100644 index 0000000..cd27f53 --- /dev/null +++ b/app/controllers/home_controller.rb @@ -0,0 +1,7 @@ +class HomeController < ApplicationController + allow_unauthenticated_access + + def index + @tartiflettes = Tartiflette.all + end +end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb new file mode 100644 index 0000000..0c4b4a8 --- /dev/null +++ b/app/controllers/passwords_controller.rb @@ -0,0 +1,33 @@ +class PasswordsController < ApplicationController + allow_unauthenticated_access + before_action :set_user_by_token, only: %i[ edit update ] + + def new + end + + def create + if user = User.find_by(email_address: params[:email_address]) + PasswordsMailer.reset(user).deliver_later + end + + redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)." + end + + def edit + end + + def update + if @user.update(params.permit(:password, :password_confirmation)) + redirect_to new_session_path, notice: "Password has been reset." + else + redirect_to edit_password_path(params[:token]), alert: "Passwords did not match." + end + end + + private + def set_user_by_token + @user = User.find_by_password_reset_token!(params[:token]) + rescue ActiveSupport::MessageVerifier::InvalidSignature + redirect_to new_password_path, alert: "Password reset link is invalid or has expired." + end +end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb new file mode 100644 index 0000000..d2a6822 --- /dev/null +++ b/app/controllers/registrations_controller.rb @@ -0,0 +1,21 @@ +class RegistrationsController < ApplicationController + def new + @user = User.new + end + + def create + @user = User.new(user_params) + if @user.save + start_new_session_for @user + redirect_to root_path, notice: "Successfully signed up!" + else + render :new + end + end + + private + + def user_params + params.require(:user).permit(:email_address, :password, :password_confirmation) + end +end diff --git a/app/controllers/scores_controller.rb b/app/controllers/scores_controller.rb new file mode 100644 index 0000000..650c4e6 --- /dev/null +++ b/app/controllers/scores_controller.rb @@ -0,0 +1,49 @@ +class ScoresController < ApplicationController + allow_unauthenticated_access + before_action :set_tartiflette, only: [ :new, :create, :edit_all, :update_all ] + before_action :scores_params, only: [ :create, :update_all ] + + def new + end + + def create + if TartifletteScoringService.scored?(@tartiflette, session) + redirect_to root_path, alert: "Vous avez déja noté cette tartiflette." + return + end + + TartifletteScoringService.submit_scores(@tartiflette, scores_params, session) + redirect_to root_path, + notice: "Vos scores pour la tartiflette #{@tartiflette.scoring_id} ont été enregistrés." + rescue StandardError => e + redirect_to root_path, + status: :unprocessable_entity, + alert: "Erreur lors de l'enregistrement de vos scores : #{e.message}" + end + + def edit_all + @scores = @tartiflette.scores + end + + def update_all + scores_params.each do |score_id, score_params| + score = @tartiflette.scores.find(score_id) + score.update!(value: score_params[:value]) + end + redirect_to root_path, + notice: "Vos scores pour la tartiflette #{@tartiflette.scoring_id} ont été mis à jour." + rescue StandardError => e + redirect_to edit_tartiflette_scores_path(@tartiflette), + alert: "Erreur lors de l'enregistrement de vos scores : #{e.message}" + end + + private + + def set_tartiflette + @tartiflette = Tartiflette.find(params[:tartiflette_id]) + end + + def scores_params + params.require(:scores).permit!.to_h + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000..9785c92 --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,21 @@ +class SessionsController < ApplicationController + 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." } + + def new + end + + def create + if user = User.authenticate_by(params.permit(:email_address, :password)) + start_new_session_for user + redirect_to after_authentication_url + else + redirect_to new_session_path, alert: "Try another email address or password." + end + end + + def destroy + terminate_session + redirect_to new_session_path + end +end diff --git a/app/controllers/tartiflettes_controller.rb b/app/controllers/tartiflettes_controller.rb new file mode 100644 index 0000000..cbea402 --- /dev/null +++ b/app/controllers/tartiflettes_controller.rb @@ -0,0 +1,26 @@ +class TartiflettesController < ApplicationController + before_action :set_tartiflette, only: [ :show ] + + def index + @tartiflettes = Tartiflette.all + end + + def show + end + + def new + @tartiflette = Tartiflette.new + end + + private + + def tartiflette_params + params.require(:tartiflette) + end + + def set_tartiflette + @tartiflette = Tartiflette.find(params[:id]) + rescue ActiveRecord::RecordNotFound + redirect_to root_path, alert: "Tartiflette introuvable." + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 0000000..de6be79 --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb new file mode 100644 index 0000000..23de56a --- /dev/null +++ b/app/helpers/home_helper.rb @@ -0,0 +1,2 @@ +module HomeHelper +end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000..d394c3d --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 0000000..3c34c81 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout "mailer" +end diff --git a/app/mailers/passwords_mailer.rb b/app/mailers/passwords_mailer.rb new file mode 100644 index 0000000..4f0ac7f --- /dev/null +++ b/app/mailers/passwords_mailer.rb @@ -0,0 +1,6 @@ +class PasswordsMailer < ApplicationMailer + def reset(user) + @user = user + mail subject: "Reset your password", to: user.email_address + end +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000..b63caeb --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/app/models/concerns/.keep diff --git a/app/models/current.rb b/app/models/current.rb new file mode 100644 index 0000000..2bef56d --- /dev/null +++ b/app/models/current.rb @@ -0,0 +1,4 @@ +class Current < ActiveSupport::CurrentAttributes + attribute :session + delegate :user, to: :session, allow_nil: true +end diff --git a/app/models/score.rb b/app/models/score.rb new file mode 100644 index 0000000..5147ce0 --- /dev/null +++ b/app/models/score.rb @@ -0,0 +1,6 @@ +class Score < ApplicationRecord + belongs_to :tartiflette + belongs_to :scoring_criterium + + validates :value, presence: true, inclusion: { in: 1..5 } +end diff --git a/app/models/scoring_criterium.rb b/app/models/scoring_criterium.rb new file mode 100644 index 0000000..a94ad77 --- /dev/null +++ b/app/models/scoring_criterium.rb @@ -0,0 +1,9 @@ +class ScoringCriterium < ApplicationRecord + has_many :scores, dependent: :destroy + + validates :name, :category, presence: true + + def self.grouped_by_category + ScoringCriterium.all.group_by(&:category) + end +end diff --git a/app/models/session.rb b/app/models/session.rb new file mode 100644 index 0000000..cf376fb --- /dev/null +++ b/app/models/session.rb @@ -0,0 +1,3 @@ +class Session < ApplicationRecord + belongs_to :user +end diff --git a/app/models/tartiflette.rb b/app/models/tartiflette.rb new file mode 100644 index 0000000..4abfe74 --- /dev/null +++ b/app/models/tartiflette.rb @@ -0,0 +1,3 @@ +class Tartiflette < ApplicationRecord + has_many :scores +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..c88d5b0 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,6 @@ +class User < ApplicationRecord + has_secure_password + has_many :sessions, dependent: :destroy + + normalizes :email_address, with: ->(e) { e.strip.downcase } +end diff --git a/app/services/tartiflette_score_export_service.rb b/app/services/tartiflette_score_export_service.rb new file mode 100644 index 0000000..26334a0 --- /dev/null +++ b/app/services/tartiflette_score_export_service.rb @@ -0,0 +1,19 @@ +require "csv" + +class TartifletteScoreExportService + def self.generate_csv + CSV.generate(headers: true) do |csv| + csv << [ "Identifiant", "Critère", "Score", "Création" ] + Tartiflette.all.each do |tartiflette| + tartiflette.scores.each do |score| + csv << [ + tartiflette.scoring_id, + score.scoring_criterium.name, + score.value, + score.created_at + ] + end + end + end + end +end diff --git a/app/services/tartiflette_scoring_service.rb b/app/services/tartiflette_scoring_service.rb new file mode 100644 index 0000000..3514eb1 --- /dev/null +++ b/app/services/tartiflette_scoring_service.rb @@ -0,0 +1,54 @@ +class TartifletteScoringService + def self.scored?(tartiflette, session) + session[:scored_tartiflettes]&.include?(tartiflette.id) + end + + def self.mark_as_scored(tartiflette, session) + session[:scored_tartiflettes] ||= [] + unless scored?(tartiflette, session) + session[:scored_tartiflettes] << tartiflette.id + end + end + + def self.submit_scores(tartiflette, scores, session) + scores.each do |criterium_id, value| + Score.create!( + tartiflette: tartiflette, + scoring_criterium_id: criterium_id, + value: value[:value] + ) + end + mark_as_scored(tartiflette, session) + end + + def self.average_score(tartiflette) + tartiflette.scores.average(:value).to_f + end + + def self.average_score_by_category(tartiflette) + tartiflette + .scores + .group_by { |score| score.scoring_criterium.category } + .transform_values do |scores| + (scores.sum(&:value).to_f / scores.size).round(2) + end + end + + def self.total_score_by_category(tartiflette) + tartiflette + .scores + .group_by { |score| score.scoring_criterium.category } + .transform_values do |scores| + (scores.sum(&:value).to_f / scores.size).round(2) + end + end + + def self.leaderboard + Tartiflette + .joins(:scores) + .select("tartiflettes.*, SUM(scores.value) AS total_score") + .group("tartiflettes.id") + .order("total_score DESC") + .map { |tartiflette| [ tartiflette, tartiflette.total_score.to_f ] } + end +end diff --git a/app/views/admin/dashboard/index.html.erb b/app/views/admin/dashboard/index.html.erb new file mode 100644 index 0000000..30cf866 --- /dev/null +++ b/app/views/admin/dashboard/index.html.erb @@ -0,0 +1,10 @@ +<h1>Administrateur</h1> + +<!-- <h2>Tartiflettes</h2> --> + +<p> + <%= button_to "Déconnexion", + session_path(session), + method: :delete %> +</p> +<p><%= link_to "Télécharger tous les scores en format CSV", admin_scores_export_path %></p> diff --git a/app/views/admin/dashboard/tmp b/app/views/admin/dashboard/tmp new file mode 100644 index 0000000..986fc28 --- /dev/null +++ b/app/views/admin/dashboard/tmp @@ -0,0 +1,19 @@ +<% ScoringCriterium.grouped_by_category.each do |category, criteria| %> + <h3><%= category.capitalize %></h3> + <table> + <thead> + <tr> + <th>Numéro</th> + <th>Score</th> + </tr> + </thead> + <tbody> + <% @tartiflettes.each do |tartiflette| %> + <tr> + <td><%= tartiflette.scoring_id %></td> + <td><%= tartiflette.scores.where(&:scoring_criterium.include? criteria) %></td> + </tr> + <% end %> + </tbody> + </table> +<% end %> diff --git a/app/views/home/_code_of_honor.html.erb b/app/views/home/_code_of_honor.html.erb new file mode 100644 index 0000000..517a73e --- /dev/null +++ b/app/views/home/_code_of_honor.html.erb @@ -0,0 +1,14 @@ +<div id="code-of-honor" class="<%= session[:agreed_to_code_of_honor] ? 'accepted' : 'rejected' %>"> + <h2>Code d'honneur</h2> + <p> + Tout Tartifleur s'engage à voter dans le respect de la + tradition de la WTT. Il ou elle se doit de voter en toute + honnêteté intellectuelle afin de favoriser un résultat mérité + ! + </p> + <% if session[:agreed_to_code_of_honor] %> + <%= button_to "Renéguer", toggle_code_of_honor_path %> + <% else %> + <%= button_to "Accepter", toggle_code_of_honor_path %> + <% end %> +</div> diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb new file mode 100644 index 0000000..ca8f3a3 --- /dev/null +++ b/app/views/home/index.html.erb @@ -0,0 +1,42 @@ +<h1>World Tartiflette Tour 2024</h1> + +<%= image_tag("blason_Savoie.png", :alt => "blason de la Savoie", id: "blason", width: 80) %> + +<%= render "code_of_honor" %> + +<h2>Noter les Tartiflettes</h2> +<ul> + <% @tartiflettes.each do |tartiflette| %> + <li><%= tartiflette.scoring_id %> + <% if session[:agreed_to_code_of_honor] %> + <% if TartifletteScoringService.scored?(tartiflette, session) %> + <%= link_to "modifier", tartiflette_edit_scores_path(tartiflette) %> + <% else %> + <%= link_to "noter", new_tartiflette_score_path(tartiflette) %> + <% end %> + <% end %> + </li> + <% end %> +</ul> +<!-- + <h2>Résultats</h2> + <table> + <thead> + <tr> + <th>Tartiflette</th> + <th>Points</th> + </tr> + </thead> + <tbody> + <% TartifletteScoringService.leaderboard.each do |tartiflette, total_score| %> + <tr> + <td><%= tartiflette.scoring_id %></td> + <td><%= total_score %></td> + </tr> + <% end %> + </tbody> + </table> +--> +<p> + <%= link_to "Admin", admin_dashboard_path %> +</p> diff --git a/app/views/layouts/_footer.html.erb b/app/views/layouts/_footer.html.erb new file mode 100644 index 0000000..bba04fb --- /dev/null +++ b/app/views/layouts/_footer.html.erb @@ -0,0 +1,3 @@ +<footer> + <p>© <%= Date.current.year %> World Tartiflette Tour</p> +</footer> diff --git a/app/views/layouts/_notifications.html.erb b/app/views/layouts/_notifications.html.erb new file mode 100644 index 0000000..3e1eb83 --- /dev/null +++ b/app/views/layouts/_notifications.html.erb @@ -0,0 +1,8 @@ +<div id="notifications"> + <% if alert %> + <p class="alert"><%= alert %></p> + <% end %> + <% if notice %> + <p class="notice"><%= notice %></p> + <% end %> +</div> diff --git a/app/views/layouts/_topnav.html.erb b/app/views/layouts/_topnav.html.erb new file mode 100644 index 0000000..f93ed99 --- /dev/null +++ b/app/views/layouts/_topnav.html.erb @@ -0,0 +1,5 @@ +<nav id="top"> + <ul> + <li><%= link_to "Accueil", root_path %></li> + </ul> +</nav> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 0000000..680dec1 --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<html> + <head> + <title><%= content_for(:title) || "WTT" %></title> + <meta name="viewport" content="width=device-width,initial-scale=1"> + <meta name="apple-mobile-web-app-capable" content="yes"> + <meta name="mobile-web-app-capable" content="yes"> + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= yield :head %> + + <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %> + <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %> + + <%= favicon_link_tag "blason_Savoie.png" %> + <!-- <link rel="icon" href="/icon.png" type="image/png"> + <link rel="icon" href="/icon.svg" type="image/svg+xml"> + <link rel="apple-touch-icon" href="/icon.png"> --> + + <%# Includes all stylesheet files in app/assets/stylesheets %> + <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> + </head> + + <body> + <%= render "layouts/topnav" %> + <%= render "layouts/notifications" %> + <main> + <%= yield %> + </main> + <%= render "layouts/footer" %> + </body> +</html> diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000..3aac900 --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + <style> + /* Email styles need to be inline */ + </style> + </head> + + <body> + <%= yield %> + </body> +</html> diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000..37f0bdd --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/app/views/passwords/edit.html.erb b/app/views/passwords/edit.html.erb new file mode 100644 index 0000000..9f0c87c --- /dev/null +++ b/app/views/passwords/edit.html.erb @@ -0,0 +1,9 @@ +<h1>Update your password</h1> + +<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %> + +<%= form_with url: password_path(params[:token]), method: :put do |form| %> + <%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72 %><br> + <%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72 %><br> + <%= form.submit "Save" %> +<% end %> diff --git a/app/views/passwords/new.html.erb b/app/views/passwords/new.html.erb new file mode 100644 index 0000000..44efb2b --- /dev/null +++ b/app/views/passwords/new.html.erb @@ -0,0 +1,8 @@ +<h1>Forgot your password?</h1> + +<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %> + +<%= form_with url: passwords_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.submit "Email reset instructions" %> +<% end %> diff --git a/app/views/passwords_mailer/reset.html.erb b/app/views/passwords_mailer/reset.html.erb new file mode 100644 index 0000000..4a06619 --- /dev/null +++ b/app/views/passwords_mailer/reset.html.erb @@ -0,0 +1,4 @@ +<p> + You can reset your password within the next 15 minutes on + <%= link_to "this password reset page", edit_password_url(@user.password_reset_token) %>. +</p> diff --git a/app/views/passwords_mailer/reset.text.erb b/app/views/passwords_mailer/reset.text.erb new file mode 100644 index 0000000..2cf03fc --- /dev/null +++ b/app/views/passwords_mailer/reset.text.erb @@ -0,0 +1,2 @@ +You can reset your password within the next 15 minutes on this password reset page: +<%= edit_password_url(@user.password_reset_token) %> diff --git a/app/views/pwa/manifest.json.erb b/app/views/pwa/manifest.json.erb new file mode 100644 index 0000000..883d71c --- /dev/null +++ b/app/views/pwa/manifest.json.erb @@ -0,0 +1,22 @@ +{ + "name": "Wtt", + "icons": [ + { + "src": "/icon.png", + "type": "image/png", + "sizes": "512x512" + }, + { + "src": "/icon.png", + "type": "image/png", + "sizes": "512x512", + "purpose": "maskable" + } + ], + "start_url": "/", + "display": "standalone", + "scope": "/", + "description": "Wtt.", + "theme_color": "red", + "background_color": "red" +} diff --git a/app/views/pwa/service-worker.js b/app/views/pwa/service-worker.js new file mode 100644 index 0000000..b3a13fb --- /dev/null +++ b/app/views/pwa/service-worker.js @@ -0,0 +1,26 @@ +// Add a service worker for processing Web Push notifications: +// +// self.addEventListener("push", async (event) => { +// const { title, options } = await event.data.json() +// event.waitUntil(self.registration.showNotification(title, options)) +// }) +// +// self.addEventListener("notificationclick", function(event) { +// event.notification.close() +// event.waitUntil( +// clients.matchAll({ type: "window" }).then((clientList) => { +// for (let i = 0; i < clientList.length; i++) { +// let client = clientList[i] +// let clientPath = (new URL(client.url)).pathname +// +// if (clientPath == event.notification.data.path && "focus" in client) { +// return client.focus() +// } +// } +// +// if (clients.openWindow) { +// return clients.openWindow(event.notification.data.path) +// } +// }) +// ) +// }) diff --git a/app/views/registrations/new.html.erb b/app/views/registrations/new.html.erb new file mode 100644 index 0000000..9d9f9bc --- /dev/null +++ b/app/views/registrations/new.html.erb @@ -0,0 +1,17 @@ +<h1>Sign Up</h1> + +<%= form_with model: @user, url: registration_path, method: :post, local: true do |f| %> + <div> + <%= f.label :email_address %> + <%= f.email_field :email_address, required: true %> + </div> + <div> + <%= f.label :password %> + <%= f.password_field :password, required: true %> + </div> + <div> + <%= f.label :password_confirmation %> + <%= f.password_field :password_confirmation, required: true %> + </div> + <%= f.submit "Sign Up" %> +<% end %> diff --git a/app/views/scores/_form.html.erb b/app/views/scores/_form.html.erb new file mode 100644 index 0000000..9cbcec7 --- /dev/null +++ b/app/views/scores/_form.html.erb @@ -0,0 +1,19 @@ +<%= form_with url: form_url, method: form_method, local: true do |f| %> + <% ScoringCriterium.grouped_by_category.each do |category, criteria| %> + <fieldset> + <legend><%= category.titlecase %></legend> + <% criteria.each do |criterium| %> + <% current_score = existing_scores.find { |score| score.scoring_criterium_id == criterium.id } %> + <p> + <%= select_tag "scores[#{criterium.id}][value]", + options_for_select(1..5, current_score&.value), + required: true, + prompt: "Score" %> + <%= label_tag "scores[#{criterium.id}][value]", + criterium.name.capitalize %> + </p> + <% end %> + </fieldset> + <% end %> + <%= f.submit submit_text %> +<% end %> diff --git a/app/views/scores/edit_all.html.erb b/app/views/scores/edit_all.html.erb new file mode 100644 index 0000000..2be7149 --- /dev/null +++ b/app/views/scores/edit_all.html.erb @@ -0,0 +1,7 @@ +<h1>Modifier les Notes pour la Tartiflette nº<%= @tartiflette.scoring_id %></h1> + +<%= render "form", + form_url: tartiflette_update_scores_path(@tartiflette), + form_method: :patch, + existing_scores: @scores, + submit_text: "Mettre à jour mes scores" %> diff --git a/app/views/scores/new.html.erb b/app/views/scores/new.html.erb new file mode 100644 index 0000000..cf171cc --- /dev/null +++ b/app/views/scores/new.html.erb @@ -0,0 +1,7 @@ +<h1>Noter la Tartiflette nº<%= @tartiflette.scoring_id %></h1> + +<%= render "form", + form_url: tartiflette_scores_path(@tartiflette), + form_method: :post, + existing_scores: [], + submit_text: "Envoyer mes scores" %> diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb new file mode 100644 index 0000000..51029b1 --- /dev/null +++ b/app/views/sessions/new.html.erb @@ -0,0 +1,20 @@ +<%= 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 %> diff --git a/app/views/tartiflettes/index.html.erb b/app/views/tartiflettes/index.html.erb new file mode 100644 index 0000000..09b2748 --- /dev/null +++ b/app/views/tartiflettes/index.html.erb @@ -0,0 +1,3 @@ +<h1>Tartiflettes</h1> + +<p>Toutes les tartiflettes</p> diff --git a/app/views/tartiflettes/show.html.erb b/app/views/tartiflettes/show.html.erb new file mode 100644 index 0000000..3753604 --- /dev/null +++ b/app/views/tartiflettes/show.html.erb @@ -0,0 +1,3 @@ +<h1>Tartiflette nº<%= @tartiflette.scoring_id %></h1> + +<p>Scores obtenus pour une tartiflette donnée.</p> diff --git a/bin/brakeman b/bin/brakeman new file mode 100755 index 0000000..ace1c9b --- /dev/null +++ b/bin/brakeman @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +ARGV.unshift("--ensure-latest") + +load Gem.bin_path("brakeman", "brakeman") @@ -0,0 +1,2 @@ +#!/usr/bin/env ruby +exec "./bin/rails", "server", *ARGV diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint new file mode 100755 index 0000000..57567d6 --- /dev/null +++ b/bin/docker-entrypoint @@ -0,0 +1,14 @@ +#!/bin/bash -e + +# Enable jemalloc for reduced memory usage and latency. +if [ -z "${LD_PRELOAD+x}" ]; then + LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit) + export LD_PRELOAD +fi + +# If running the rails server then create or migrate existing database +if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then + ./bin/rails db:prepare +fi + +exec "${@}" diff --git a/bin/rails b/bin/rails new file mode 100755 index 0000000..efc0377 --- /dev/null +++ b/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/bin/rake b/bin/rake new file mode 100755 index 0000000..4fbf10b --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/bin/rubocop b/bin/rubocop new file mode 100755 index 0000000..40330c0 --- /dev/null +++ b/bin/rubocop @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +# explicit rubocop config increases performance slightly while avoiding config confusion. +ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) + +load Gem.bin_path("rubocop", "rubocop") diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..be3db3c --- /dev/null +++ b/bin/setup @@ -0,0 +1,34 @@ +#!/usr/bin/env ruby +require "fileutils" + +APP_ROOT = File.expand_path("..", __dir__) + +def system!(*args) + system(*args, exception: true) +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system("bundle check") || system!("bundle install") + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # end + + puts "\n== Preparing database ==" + system! "bin/rails db:prepare" + + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" + + unless ARGV.include?("--skip-server") + puts "\n== Starting development server ==" + STDOUT.flush # flush the output before exec(2) so that it displays + exec "bin/dev" + end +end diff --git a/bin/thrust b/bin/thrust new file mode 100755 index 0000000..36bde2d --- /dev/null +++ b/bin/thrust @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("thruster", "thrust") diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..4a3c09a --- /dev/null +++ b/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 0000000..eb9a51a --- /dev/null +++ b/config/application.rb @@ -0,0 +1,27 @@ +require_relative "boot" + +require "rails/all" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module Wtt + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 8.0 + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w[assets tasks]) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000..988a5dd --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,4 @@ +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 0000000..1c09bc0 --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: test + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: wtt_production diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc new file mode 100644 index 0000000..64e1885 --- /dev/null +++ b/config/credentials.yml.enc @@ -0,0 +1 @@ +iKFpQhNINyw1BILCAFy1MJWDKOtLZyVBbYsywi1CnfdosMyNkL6EXg79AfcF0O321lz1/iUX6J6EmajknndFndgioK/o9TvP3vPBSP8mK7dYsa+3Fo+c1yhgIJI5xCHWXaDRQMQuiZiMeQ3VtoiYNmdIO/Fe1ACv8J/oildwzPdMqkf/z45zbJpl0KPfyEfcvYlsARI8Fe0KoL8ugchn6AcVRNl7vYWxO5cZmup9NBGNLOBUWtvpav61Sm3EmlVxOKGFCQMwpd0sYKVAjvpRJud/TU2ayysLecwtFLZWdTfONspx0A+z/ptXzKoGdM9ySmCMyMWhAOw3d/cXiuTWdEqYsb8q6IDxjH95q8kx51rS9XiqqWdAhV+wqNG3u0FESod5MINw5yApGrTiUKFwODmaTOU6Qk5lO2frIrFbAO+ifqVcpQVhCRkfiJmlkcuqGB5GvI2X/l/PiRIvyUCoxilGgbBKxGM/n/+FIWXVm8YB2ImMR1lCeaIA--Qvr/LtgDlCKSbLf2--rVY240eaUZyJ9wJWf9tr5w==
\ No newline at end of file diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 0000000..2640cb5 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,41 @@ +# SQLite. Versions 3.8.0 and up are supported. +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem "sqlite3" +# +default: &default + adapter: sqlite3 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + +development: + <<: *default + database: storage/development.sqlite3 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: storage/test.sqlite3 + + +# Store production database in the storage/ directory, which by default +# is mounted as a persistent Docker volume in config/deploy.yml. +production: + primary: + <<: *default + database: storage/production.sqlite3 + cache: + <<: *default + database: storage/production_cache.sqlite3 + migrations_paths: db/cache_migrate + queue: + <<: *default + database: storage/production_queue.sqlite3 + migrations_paths: db/queue_migrate + cable: + <<: *default + database: storage/production_cable.sqlite3 + migrations_paths: db/cable_migrate diff --git a/config/deploy.yml b/config/deploy.yml new file mode 100644 index 0000000..ca1c23c --- /dev/null +++ b/config/deploy.yml @@ -0,0 +1,116 @@ +# Name of your application. Used to uniquely configure containers. +service: wtt + +# Name of the container image. +image: blendoit/wtt + +# Deploy to these servers. +servers: + web: + - 192.162.71.223 + # job: + # hosts: + # - 192.168.0.1 + # cmd: bin/jobs + +# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server. +# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer. +# +# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. +proxy: + ssl: true + host: wtt.mlnp.fr + +# Credentials for your image host. +registry: + # Specify the registry server, if you're not using Docker Hub + # server: registry.digitalocean.com / ghcr.io / ... + username: blendoit + + # Always use an access token rather than real password when possible. + password: + - KAMAL_REGISTRY_PASSWORD + +# Inject ENV variables into containers (secrets come from .kamal/secrets). +env: + secret: + - RAILS_MASTER_KEY + clear: + # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs. + # When you start using multiple servers, you should split out job processing to a dedicated machine. + SOLID_QUEUE_IN_PUMA: true + + # Set number of processes dedicated to Solid Queue (default: 1) + # JOB_CONCURRENCY: 3 + + # Set number of cores available to the application on each server (default: 1). + # WEB_CONCURRENCY: 2 + + # Match this to any external database server to configure Active Record correctly + # Use wtt-db for a db accessory server on same machine via local kamal docker network. + # DB_HOST: 192.168.0.2 + + # Log everything from Rails + # RAILS_LOG_LEVEL: debug + +# Aliases are triggered with "bin/kamal <alias>". You can overwrite arguments on invocation: +# "bin/kamal logs -r job" will tail logs from the first server in the job section. +aliases: + console: app exec --interactive --reuse "bin/rails console" + shell: app exec --interactive --reuse "bash" + logs: app logs -f + dbc: app exec --interactive --reuse "bin/rails dbconsole" + + +# Use a persistent storage volume for sqlite database files and local Active Storage files. +# Recommended to change this to a mounted volume path that is backed up off server. +volumes: + - "wtt_storage:/rails/storage" + + +# Bridge fingerprinted assets, like JS and CSS, between versions to avoid +# hitting 404 on in-flight requests. Combines all files from new and old +# version inside the asset_path. +asset_path: /rails/public/assets + +# Configure the image builder. +builder: + arch: amd64 + + # # Build image via remote server (useful for faster amd64 builds on arm64 computers) + # remote: ssh://docker@docker-builder-server + # + # # Pass arguments and secrets to the Docker build process + # args: + # RUBY_VERSION: ruby-3.3.6 + # secrets: + # - GITHUB_TOKEN + # - RAILS_MASTER_KEY + +# Use a different ssh user than root +ssh: + user: mpeter + +# Use accessory services (secrets come from .kamal/secrets). +# accessories: +# db: +# image: mysql:8.0 +# host: 192.168.0.2 +# # Change to 3306 to expose port to the world instead of just local network. +# port: "127.0.0.1:3306:3306" +# env: +# clear: +# MYSQL_ROOT_HOST: '%' +# secret: +# - MYSQL_ROOT_PASSWORD +# files: +# - config/mysql/production.cnf:/etc/mysql/my.cnf +# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql +# directories: +# - data:/var/lib/mysql +# redis: +# image: redis:7.0 +# host: 192.168.0.2 +# port: 6379 +# directories: +# - data:/data diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000..cac5315 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 0000000..4cc21c4 --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,72 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Make code changes take effect immediately without server restart. + config.enable_reloading = true + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing. + config.server_timing = true + + # Enable/disable Action Controller caching. By default Action Controller caching is disabled. + # Run rails dev:cache to toggle Action Controller caching. + if Rails.root.join("tmp/caching-dev.txt").exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" } + else + config.action_controller.perform_caching = false + end + + # Change to :null_store to avoid any caching. + config.cache_store = :memory_store + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + # Make template changes take effect immediately. + config.action_mailer.perform_caching = false + + # Set localhost to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "localhost", port: 3000 } + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Append comments with runtime information tags to SQL queries in logs. + config.active_record.query_log_tags_enabled = true + + # Highlight code that enqueued background job in logs. + config.active_job.verbose_enqueue_logs = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true + + # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. + # config.generators.apply_rubocop_autocorrect_after_generate! +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 0000000..1749607 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,89 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.enable_reloading = false + + # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). + config.eager_load = true + + # Full error reports are disabled. + config.consider_all_requests_local = false + + # Turn on fragment caching in view templates. + config.action_controller.perform_caching = true + + # Cache assets for far-future expiry since they are all digest stamped. + config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" } + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + config.assume_ssl = true + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + config.force_ssl = true + + # Skip http-to-https redirect for the default health check endpoint. + # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } + + # Log to STDOUT with the current request id as a default log tag. + config.log_tags = [ :request_id ] + config.logger = ActiveSupport::TaggedLogging.logger(STDOUT) + + # Change to "debug" to log everything (including potentially personally-identifiable information!) + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + + # Prevent health checks from clogging up the logs. + config.silence_healthcheck_path = "/up" + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Replace the default in-process memory cache store with a durable alternative. + # config.cache_store = :mem_cache_store + + # Replace the default in-process and non-durable queuing backend for Active Job. + # config.active_job.queue_adapter = :resque + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "example.com" } + + # Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit. + # config.action_mailer.smtp_settings = { + # user_name: Rails.application.credentials.dig(:smtp, :user_name), + # password: Rails.application.credentials.dig(:smtp, :password), + # address: "smtp.example.com", + # port: 587, + # authentication: :plain + # } + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Only use :id for inspections in production. + config.active_record.attributes_for_inspect = [ :id ] + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000..c2095b1 --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,53 @@ +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false + + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before deploying your code. + config.eager_load = ENV["CI"].present? + + # Configure public file server for tests with cache-control for performance. + config.public_file_server.headers = { "cache-control" => "public, max-age=3600" } + + # Show full error reports. + config.consider_all_requests_local = true + config.cache_store = :null_store + + # Render exception templates for rescuable exceptions and raise for other exceptions. + config.action_dispatch.show_exceptions = :rescuable + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "example.com" } + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb new file mode 100644 index 0000000..4873244 --- /dev/null +++ b/config/initializers/assets.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = "1.0" + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb new file mode 100644 index 0000000..b3076b3 --- /dev/null +++ b/config/initializers/content_security_policy.rb @@ -0,0 +1,25 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header + +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src style-src) +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true +# end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000..c0b717f --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc +] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 0000000..3860f65 --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym "RESTful" +# end diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000..6c349ae --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,31 @@ +# Files in the config/locales directory are used for internationalization and +# are automatically loaded by Rails. If you want to use locales other than +# English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more about the API, please read the Rails Internationalization guide +# at https://guides.rubyonrails.org/i18n.html. +# +# Be aware that YAML interprets the following case-insensitive strings as +# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings +# must be quoted to be interpreted as strings. For example: +# +# en: +# "yes": yup +# enabled: "ON" + +en: + hello: "Hello world" diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000..a248513 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,41 @@ +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. +# +# Puma starts a configurable number of processes (workers) and each process +# serves each request in a thread from an internal thread pool. +# +# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You +# should only set this value when you want to run 2 or more workers. The +# default is already 1. +# +# The ideal number of threads per worker depends both on how much time the +# application spends waiting for IO operations and on how much you wish to +# prioritize throughput over latency. +# +# As a rule of thumb, increasing the number of threads will increase how much +# traffic a given process can handle (throughput), but due to CRuby's +# Global VM Lock (GVL) it has diminishing returns and will degrade the +# response time (latency) of the application. +# +# The default is set to 3 threads as it's deemed a decent compromise between +# throughput and latency for the average Rails application. +# +# Any libraries that use a connection pool or another resource pool should +# be configured to provide at least as many connections as the number of +# threads. This includes Active Record's `pool` parameter in `database.yml`. +threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +port ENV.fetch("PORT", 3000) + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart + +# Run the Solid Queue supervisor inside of Puma for single-server deployments +plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] + +# Specify the PID file. Defaults to tmp/pids/server.pid in development. +# In other environments, only set the PID file if requested. +pidfile ENV["PIDFILE"] if ENV["PIDFILE"] diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..efa9589 --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,46 @@ +Rails.application.routes.draw do + root to: "home#index" + + resource :session + resource :registration, only: [ "new", "create" ] + resources :passwords, param: :token + # get "/sign_up", to: "registrations#new", as: :sign_up + # post "/sign_up", to: "registrations#create" + # delete "/log_out", to: "sessions#destroy", as: :log_out + + post "/code_of_honor/toggle", + to: "code_of_honor#toggle", + as: :toggle_code_of_honor + + resources :tartiflettes do + get "scores/edit", + to: "scores#edit_all", + as: :edit_scores + patch "scores", + to: "scores#update_all", + as: :update_scores + resources :scores, only: [ :new, :create ] + end + + namespace :admin do + get "dashboard", + to: "dashboard#index", + as: :dashboard + get "scores/export", + to: "scores#export", + as: :scores_export + end + + # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html + + # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. + # Can be used by load balancers and uptime monitors to verify that the app is live. + get "up" => "rails/health#show", as: :rails_health_check + + # Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb) + # get "manifest" => "rails/pwa#manifest", as: :pwa_manifest + # get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker + + # Defines the root path route ("/") + # root "posts#index" +end diff --git a/config/storage.yml b/config/storage.yml new file mode 100644 index 0000000..4942ab6 --- /dev/null +++ b/config/storage.yml @@ -0,0 +1,34 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket-<%= Rails.env %> + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket-<%= Rails.env %> + +# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name-<%= Rails.env %> + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/db/migrate/20241226110255_create_tartiflettes.rb b/db/migrate/20241226110255_create_tartiflettes.rb new file mode 100644 index 0000000..17ee93f --- /dev/null +++ b/db/migrate/20241226110255_create_tartiflettes.rb @@ -0,0 +1,9 @@ +class CreateTartiflettes < ActiveRecord::Migration[8.0] + def change + create_table :tartiflettes do |t| + t.integer :scoring_id + + t.timestamps + end + end +end diff --git a/db/migrate/20241226113752_create_scores.rb b/db/migrate/20241226113752_create_scores.rb new file mode 100644 index 0000000..488baaa --- /dev/null +++ b/db/migrate/20241226113752_create_scores.rb @@ -0,0 +1,11 @@ +class CreateScores < ActiveRecord::Migration[8.0] + def change + create_table :scores do |t| + t.references :tartiflette, null: false, foreign_key: true + t.references :scoring_criterium, null: false, foreign_key: true + t.integer :value + + t.timestamps + end + end +end diff --git a/db/migrate/20241226113815_create_scoring_criteria.rb b/db/migrate/20241226113815_create_scoring_criteria.rb new file mode 100644 index 0000000..8370529 --- /dev/null +++ b/db/migrate/20241226113815_create_scoring_criteria.rb @@ -0,0 +1,10 @@ +class CreateScoringCriteria < ActiveRecord::Migration[8.0] + def change + create_table :scoring_criteria do |t| + t.string :name + t.string :category + + t.timestamps + end + end +end diff --git a/db/migrate/20241227180909_create_users.rb b/db/migrate/20241227180909_create_users.rb new file mode 100644 index 0000000..2075edf --- /dev/null +++ b/db/migrate/20241227180909_create_users.rb @@ -0,0 +1,11 @@ +class CreateUsers < ActiveRecord::Migration[8.0] + def change + create_table :users do |t| + t.string :email_address, null: false + t.string :password_digest, null: false + + t.timestamps + end + add_index :users, :email_address, unique: true + end +end diff --git a/db/migrate/20241227180910_create_sessions.rb b/db/migrate/20241227180910_create_sessions.rb new file mode 100644 index 0000000..8102f13 --- /dev/null +++ b/db/migrate/20241227180910_create_sessions.rb @@ -0,0 +1,11 @@ +class CreateSessions < ActiveRecord::Migration[8.0] + def change + create_table :sessions do |t| + t.references :user, null: false, foreign_key: true + t.string :ip_address + t.string :user_agent + + t.timestamps + end + end +end diff --git a/db/migrate/20241227181134_add_admin_to_users.rb b/db/migrate/20241227181134_add_admin_to_users.rb new file mode 100644 index 0000000..c1f08cf --- /dev/null +++ b/db/migrate/20241227181134_add_admin_to_users.rb @@ -0,0 +1,5 @@ +class AddAdminToUsers < ActiveRecord::Migration[8.0] + def change + add_column :users, :admin, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..eb5b7e8 --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,58 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[8.0].define(version: 2024_12_27_181134) do + create_table "scores", force: :cascade do |t| + t.integer "tartiflette_id", null: false + t.integer "scoring_criterium_id", null: false + t.integer "value" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["scoring_criterium_id"], name: "index_scores_on_scoring_criterium_id" + t.index ["tartiflette_id"], name: "index_scores_on_tartiflette_id" + end + + create_table "scoring_criteria", force: :cascade do |t| + t.string "name" + t.string "category" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "sessions", force: :cascade do |t| + t.integer "user_id", null: false + t.string "ip_address" + t.string "user_agent" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_sessions_on_user_id" + end + + create_table "tartiflettes", force: :cascade do |t| + t.integer "scoring_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "users", force: :cascade do |t| + t.string "email_address", null: false + t.string "password_digest", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "admin", default: false + t.index ["email_address"], name: "index_users_on_email_address", unique: true + end + + add_foreign_key "scores", "scoring_criteria" + add_foreign_key "scores", "tartiflettes" + add_foreign_key "sessions", "users" +end diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 0000000..7fb3253 --- /dev/null +++ b/db/seeds.rb @@ -0,0 +1,41 @@ +# This file should ensure the existence of records required to run the application in every environment (production, +# development, test). The code here should be idempotent so that it can be executed at any point in every environment. +# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). +# +# Example: +# +# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| +# MovieGenre.find_or_create_by!(name: genre_name) +# end + +User.create!( + email_address: "wtt@marius-peter.com", + password: "password", + password_confirmation: "password", + admin: true +) +puts "Seeded admin account" + +(1..8).each do |id| + Tartiflette.create!(scoring_id: id) +end +puts "Seeded tartiflettes" + +{ visuel: [ "présence de la pastille", + "croûte dorée", + "lardons apparents" ], + texture: [ "pommes de terre fondantes", + "lardons grillés", + "oignons biens cuits" ], + goût: [ "reblochon savoureux", + "lardons appétants", + "vin blanc équilibré" ], + special: [ "on en reveut !", + "quelqu'un cherche à dépasser les maestros...", + "on reconnaît la patte du tartifleur" ] +}.each do |category, names| + names.each do |name| + ScoringCriterium.create!(name: name, category: category) + end +end +puts "Seeded scoring criteria" diff --git a/lib/tasks/.keep b/lib/tasks/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/tasks/.keep diff --git a/log/.keep b/log/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/log/.keep diff --git a/public/400.html b/public/400.html new file mode 100644 index 0000000..282dbc8 --- /dev/null +++ b/public/400.html @@ -0,0 +1,114 @@ +<!doctype html> + +<html lang="en"> + + <head> + + <title>The server cannot process the request due to a client error (400 Bad Request)</title> + + <meta charset="utf-8"> + <meta name="viewport" content="initial-scale=1, width=device-width"> + <meta name="robots" content="noindex, nofollow"> + + <style> + + *, *::before, *::after { + box-sizing: border-box; + } + + * { + margin: 0; + } + + html { + font-size: 16px; + } + + body { + background: #FFF; + color: #261B23; + display: grid; + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Aptos, Roboto, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-size: clamp(1rem, 2.5vw, 2rem); + -webkit-font-smoothing: antialiased; + font-style: normal; + font-weight: 400; + letter-spacing: -0.0025em; + line-height: 1.4; + min-height: 100vh; + place-items: center; + text-rendering: optimizeLegibility; + -webkit-text-size-adjust: 100%; + } + + a { + color: inherit; + font-weight: 700; + text-decoration: underline; + text-underline-offset: 0.0925em; + } + + b, strong { + font-weight: 700; + } + + i, em { + font-style: italic; + } + + main { + display: grid; + gap: 1em; + padding: 2em; + place-items: center; + text-align: center; + } + + main header { + width: min(100%, 12em); + } + + main header svg { + height: auto; + max-width: 100%; + width: 100%; + } + + main article { + width: min(100%, 30em); + } + + main article p { + font-size: 75%; + } + + main article br { + + display: none; + + @media(min-width: 48em) { + display: inline; + } + + } + + </style> + + </head> + + <body> + + <!-- This file lives in public/400.html --> + + <main> + <header> + <svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm140.456 133.2831c-40.823 0-64.884-35.146-64.884-85.7015 0-50.5554 24.061-85.700907 64.884-85.700907 40.822 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.062 85.7015-64.884 85.7015zm0-133.2831c-17.573 0-22.71 21.8984-22.71 47.5816 0 25.6835 5.137 47.5815 22.71 47.5815 17.302 0 22.709-21.898 22.709-47.5815 0-25.6832-5.407-47.5816-22.709-47.5816z" fill="#f0eff0"/><path d="m123.606 85.4445c3.212 1.0523 5.538 4.2089 5.538 8.0301 0 6.1472-4.209 9.5254-11.298 9.5254h-15.617v-34.0033h14.565c7.089 0 11.353 3.1566 11.353 9.2484 0 3.6551-2.049 6.3134-4.541 7.1994zm-12.904-2.9905h5.095c2.603 0 3.988-.9968 3.988-3.1013 0-2.1044-1.385-3.0459-3.988-3.0459h-5.095zm0 6.6456v6.5902h5.981c2.492 0 3.877-1.3291 3.877-3.2674 0-2.049-1.385-3.3228-3.877-3.3228zm43.786 13.9004h-8.362v-1.274c-.831.831-3.323 1.717-5.981 1.717-4.929 0-9.083-2.769-9.083-8.0301 0-4.818 4.154-7.9193 9.581-7.9193 2.049 0 4.486.6646 5.483 1.3845v-1.606c0-1.606-.942-2.9905-3.046-2.9905-1.606 0-2.548.7199-2.935 1.8275h-8.197c.72-4.8181 4.985-8.6393 11.409-8.6393 7.088 0 11.131 3.7659 11.131 10.2453zm-8.362-6.9779v-1.4399c-.554-1.0522-2.049-1.7167-3.655-1.7167-1.717 0-3.434.7199-3.434 2.3813 0 1.7168 1.717 2.4367 3.434 2.4367 1.606 0 3.101-.6645 3.655-1.6614zm27.996 6.9779v-1.994c-1.163 1.329-3.599 2.548-6.147 2.548-7.199 0-11.131-5.8151-11.131-13.0145s3.932-13.0143 11.131-13.0143c2.548 0 4.984 1.2184 6.147 2.5475v-13.0697h8.695v35.997zm0-9.1931v-6.5902c-.664-1.3291-2.159-2.326-3.821-2.326-2.99 0-4.763 2.4368-4.763 5.6488s1.773 5.5934 4.763 5.5934c1.717 0 3.157-.9415 3.821-2.326zm35.471-2.049h-3.101v11.2421h-8.806v-34.0033h15.285c7.31 0 12.35 4.1535 12.35 11.5744 0 5.1503-2.603 8.6947-6.757 10.2453l7.975 12.1836h-9.858zm-3.101-15.2849v8.1962h5.538c3.156 0 4.596-1.606 4.596-4.0981s-1.44-4.0981-4.596-4.0981zm36.957 17.8323h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.515-13.0143 7.643 0 11.962 5.095 11.962 12.5159v2.1598h-16.115c.277 2.9905 1.827 4.5965 4.32 4.5965 1.772 0 3.156-.7753 3.655-2.4921zm-3.822-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm30.98 27.5234v-10.799c-1.163 1.329-3.6 2.548-6.147 2.548-7.2 0-11.132-5.9259-11.132-13.0145 0-7.144 3.932-13.0143 11.132-13.0143 2.547 0 4.984 1.2184 6.147 2.5475v-1.9937h8.695v33.726zm0-17.9981v-6.5902c-.665-1.3291-2.105-2.326-3.821-2.326-2.991 0-4.763 2.4368-4.763 5.6488s1.772 5.5934 4.763 5.5934c1.661 0 3.156-.9415 3.821-2.326zm36.789-15.7279v24.921h-8.695v-2.16c-1.329 1.551-3.821 2.714-6.646 2.714-5.482 0-8.75-3.5999-8.75-9.1379v-16.3371h8.64v14.288c0 2.1045.996 3.5997 3.212 3.5997 1.606 0 3.101-1.0522 3.544-2.769v-15.1187zm19.084 16.2263h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.515-13.0143 7.643 0 11.963 5.095 11.963 12.5159v2.1598h-16.116c.277 2.9905 1.828 4.5965 4.32 4.5965 1.772 0 3.156-.7753 3.655-2.4921zm-3.822-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm13.428 11.0206h8.474c.387 1.3845 1.606 2.1598 3.156 2.1598 1.44 0 2.548-.5538 2.548-1.7168 0-.9414-.72-1.2737-1.939-1.5506l-4.873-.9969c-4.154-.886-6.867-2.8797-6.867-7.2547 0-5.3165 4.762-8.4178 10.633-8.4178 6.812 0 10.522 3.1567 11.297 8.0855h-8.03c-.277-1.0522-1.052-1.9937-3.046-1.9937-1.273 0-2.326.5538-2.326 1.6614 0 .7753.554 1.163 1.717 1.3845l4.929 1.163c4.541 1.0522 6.978 3.4335 6.978 7.4763 0 5.3168-4.818 8.2518-10.91 8.2518-6.369 0-10.965-2.88-11.741-8.2518zm27.538-.8861v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.993-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.871 0-9.193-2.769-9.193-9.0819z" fill="#d30001"/></svg> + </header> + <article> + <p><strong>The server cannot process the request due to a client error.</strong> Please check the request and try again. If you’re the application owner check the logs for more information.</p> + </article> + </main> + + </body> + +</html> diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000..c0670bc --- /dev/null +++ b/public/404.html @@ -0,0 +1,114 @@ +<!doctype html> + +<html lang="en"> + + <head> + + <title>The page you were looking for doesn’t exist (404 Not found)</title> + + <meta charset="utf-8"> + <meta name="viewport" content="initial-scale=1, width=device-width"> + <meta name="robots" content="noindex, nofollow"> + + <style> + + *, *::before, *::after { + box-sizing: border-box; + } + + * { + margin: 0; + } + + html { + font-size: 16px; + } + + body { + background: #FFF; + color: #261B23; + display: grid; + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Aptos, Roboto, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-size: clamp(1rem, 2.5vw, 2rem); + -webkit-font-smoothing: antialiased; + font-style: normal; + font-weight: 400; + letter-spacing: -0.0025em; + line-height: 1.4; + min-height: 100vh; + place-items: center; + text-rendering: optimizeLegibility; + -webkit-text-size-adjust: 100%; + } + + a { + color: inherit; + font-weight: 700; + text-decoration: underline; + text-underline-offset: 0.0925em; + } + + b, strong { + font-weight: 700; + } + + i, em { + font-style: italic; + } + + main { + display: grid; + gap: 1em; + padding: 2em; + place-items: center; + text-align: center; + } + + main header { + width: min(100%, 12em); + } + + main header svg { + height: auto; + max-width: 100%; + width: 100%; + } + + main article { + width: min(100%, 30em); + } + + main article p { + font-size: 75%; + } + + main article br { + + display: none; + + @media(min-width: 48em) { + display: inline; + } + + } + + </style> + + </head> + + <body> + + <!-- This file lives in public/404.html --> + + <main> + <header> + <svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm165.328-35.41581-45.689 100.02991h26.224v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.184v-31.901l50.285-103.27391z" fill="#f0eff0"/><path d="m157.758 68.9967v34.0033h-7.199l-14.233-19.8814v19.8814h-8.584v-34.0033h8.307l13.125 18.7184v-18.7184zm28.454 21.5428c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.528 0c0-3.4336-1.496-5.8703-4.209-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.209-2.3813 4.209-5.8149zm13.184 3.8766v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm37.027 8.5839h-8.806v-34.0033h23.924v7.6978h-15.118v6.7564h13.9v7.5316h-13.9zm41.876-12.4605c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.529 0c0-3.4336-1.495-5.8703-4.208-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.208-2.3813 4.208-5.8149zm35.337-12.4605v24.921h-8.695v-2.16c-1.329 1.551-3.821 2.714-6.646 2.714-5.482 0-8.75-3.5999-8.75-9.1379v-16.3371h8.64v14.288c0 2.1045.997 3.5997 3.212 3.5997 1.606 0 3.101-1.0522 3.544-2.769v-15.1187zm4.076 24.921v-24.921h8.694v2.1598c1.385-1.5506 3.822-2.7136 6.701-2.7136 5.538 0 8.806 3.5997 8.806 9.1377v16.3371h-8.639v-14.2327c0-2.049-1.053-3.5443-3.268-3.5443-1.717 0-3.156.9969-3.6 2.7136v15.0634zm44.113 0v-1.994c-1.163 1.329-3.6 2.548-6.147 2.548-7.2 0-11.132-5.8151-11.132-13.0145s3.932-13.0143 11.132-13.0143c2.547 0 4.984 1.2184 6.147 2.5475v-13.0697h8.695v35.997zm0-9.1931v-6.5902c-.665-1.3291-2.16-2.326-3.821-2.326-2.991 0-4.763 2.4368-4.763 5.6488s1.772 5.5934 4.763 5.5934c1.717 0 3.156-.9415 3.821-2.326z" fill="#d30001"/></svg> + </header> + <article> + <p><strong>The page you were looking for doesn’t exist.</strong> You may have mistyped the address or the page may have moved. If you’re the application owner check the logs for more information.</p> + </article> + </main> + + </body> + +</html> diff --git a/public/406-unsupported-browser.html b/public/406-unsupported-browser.html new file mode 100644 index 0000000..9532a9c --- /dev/null +++ b/public/406-unsupported-browser.html @@ -0,0 +1,114 @@ +<!doctype html> + +<html lang="en"> + + <head> + + <title>Your browser is not supported (406 Not Acceptable)</title> + + <meta charset="utf-8"> + <meta name="viewport" content="initial-scale=1, width=device-width"> + <meta name="robots" content="noindex, nofollow"> + + <style> + + *, *::before, *::after { + box-sizing: border-box; + } + + * { + margin: 0; + } + + html { + font-size: 16px; + } + + body { + background: #FFF; + color: #261B23; + display: grid; + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Aptos, Roboto, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-size: clamp(1rem, 2.5vw, 2rem); + -webkit-font-smoothing: antialiased; + font-style: normal; + font-weight: 400; + letter-spacing: -0.0025em; + line-height: 1.4; + min-height: 100vh; + place-items: center; + text-rendering: optimizeLegibility; + -webkit-text-size-adjust: 100%; + } + + a { + color: inherit; + font-weight: 700; + text-decoration: underline; + text-underline-offset: 0.0925em; + } + + b, strong { + font-weight: 700; + } + + i, em { + font-style: italic; + } + + main { + display: grid; + gap: 1em; + padding: 2em; + place-items: center; + text-align: center; + } + + main header { + width: min(100%, 12em); + } + + main header svg { + height: auto; + max-width: 100%; + width: 100%; + } + + main article { + width: min(100%, 30em); + } + + main article p { + font-size: 75%; + } + + main article br { + + display: none; + + @media(min-width: 48em) { + display: inline; + } + + } + + </style> + + </head> + + <body> + + <!-- This file lives in public/406-unsupported-browser.html --> + + <main> + <header> + <svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm202.906 9.7326h-41.093c-2.433-7.2994-7.84-12.4361-17.302-12.4361-16.221 0-25.413 17.5728-25.954 34.8752v1.3517c5.137-7.0291 16.221-12.4361 30.82-12.4361 33.524 0 54.881 24.0612 54.881 53.7998 0 33.253-23.791 58.396-61.64 58.396-21.628 0-39.741-10.003-50.825-27.576-9.733-14.599-13.788-32.442-13.788-54.3406 0-51.9072 24.331-89.485807 66.236-89.485807 32.712 0 53.258 18.654107 58.665 47.851907zm-82.727 66.2355c0 13.247 9.463 22.439 22.71 22.439 12.977 0 22.439-9.192 22.439-22.439 0-13.517-9.462-22.7091-22.439-22.7091-13.247 0-22.71 9.1921-22.71 22.7091z" fill="#f0eff0"/><path d="m100.761 68.9967v34.0033h-7.1991l-14.2326-19.8814v19.8814h-8.5839v-34.0033h8.307l13.125 18.7184v-18.7184zm28.454 21.5428c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.529 0c0-3.4336-1.495-5.8703-4.208-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.208-2.3813 4.208-5.8149zm13.185 3.8766v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm39.02-25.4194h9.083l12.958 34.0033h-9.027l-2.436-6.5902h-12.35l-2.381 6.5902h-8.806zm4.431 10.5222-3.489 9.5807h6.978zm17.44 11.0206c0-7.6978 5.095-13.0143 12.572-13.0143 6.701 0 10.854 3.9874 11.574 9.8023h-8.418c-.221-1.4953-1.384-2.6029-3.156-2.6029-2.437 0-3.988 2.2706-3.988 5.8149s1.551 5.7595 3.988 5.7595c1.772 0 2.935-1.0522 3.156-2.5475h8.418c-.72 5.7596-4.873 9.8025-11.574 9.8025-7.477 0-12.572-5.3167-12.572-13.0145zm25.676 0c0-7.6978 5.095-13.0143 12.572-13.0143 6.701 0 10.854 3.9874 11.574 9.8023h-8.418c-.221-1.4953-1.384-2.6029-3.156-2.6029-2.437 0-3.988 2.2706-3.988 5.8149s1.551 5.7595 3.988 5.7595c1.772 0 2.935-1.0522 3.156-2.5475h8.418c-.72 5.7596-4.873 9.8025-11.574 9.8025-7.477 0-12.572-5.3167-12.572-13.0145zm42.013 3.7658h8.031c-.887 5.7597-5.206 9.2487-11.686 9.2487-7.642 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.317-13.0143 12.516-13.0143 7.643 0 11.962 5.095 11.962 12.5159v2.1598h-16.115c.277 2.9905 1.827 4.5965 4.319 4.5965 1.773 0 3.157-.7753 3.655-2.4921zm-3.821-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm23.4 16.7244v10.799h-8.694v-33.726h8.694v1.9937c1.163-1.3291 3.6-2.5475 6.148-2.5475 7.199 0 11.131 5.8703 11.131 13.0143 0 7.0886-3.932 13.0145-11.131 13.0145-2.548 0-4.985-1.219-6.148-2.548zm0-13.7893v6.5902c.665 1.3845 2.16 2.326 3.822 2.326 2.99 0 4.762-2.3814 4.762-5.5934s-1.772-5.6488-4.762-5.6488c-1.717 0-3.157.9969-3.822 2.326zm21.892 7.1994v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.206v6.7564h-5.206v8.307c0 1.9383.941 2.769 2.658 2.769.942 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm39.458 8.5839h-8.363v-1.274c-.83.831-3.322 1.717-5.981 1.717-4.928 0-9.082-2.769-9.082-8.0301 0-4.818 4.154-7.9193 9.581-7.9193 2.049 0 4.486.6646 5.482 1.3845v-1.606c0-1.606-.941-2.9905-3.045-2.9905-1.606 0-2.548.7199-2.936 1.8275h-8.196c.72-4.8181 4.984-8.6393 11.408-8.6393 7.089 0 11.132 3.7659 11.132 10.2453zm-8.363-6.9779v-1.4399c-.553-1.0522-2.049-1.7167-3.655-1.7167-1.716 0-3.433.7199-3.433 2.3813 0 1.7168 1.717 2.4367 3.433 2.4367 1.606 0 3.102-.6645 3.655-1.6614zm20.742 4.9839v1.994h-8.694v-35.997h8.694v13.0697c1.163-1.3291 3.6-2.5475 6.148-2.5475 7.199 0 11.131 5.8149 11.131 13.0143s-3.932 13.0145-11.131 13.0145c-2.548 0-4.985-1.219-6.148-2.548zm0-13.7893v6.5902c.665 1.3845 2.105 2.326 3.822 2.326 2.99 0 4.762-2.3814 4.762-5.5934s-1.772-5.6488-4.762-5.6488c-1.662 0-3.157.9969-3.822 2.326zm28.759-20.2137v35.997h-8.695v-35.997zm19.172 27.3023h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.516-13.0143 7.642 0 11.962 5.095 11.962 12.5159v2.1598h-16.116c.277 2.9905 1.828 4.5965 4.32 4.5965 1.772 0 3.157-.7753 3.655-2.4921zm-3.821-10.0237c-2.049 0-3.434 1.2737-3.988 3.5997h7.532c-.111-2.0491-1.384-3.5997-3.544-3.5997z" fill="#d30001"/></svg> + </header> + <article> + <p><strong>Your browser is not supported.</strong><br> Please upgrade your browser to continue.</p> + </article> + </main> + + </body> + +</html> diff --git a/public/422.html b/public/422.html new file mode 100644 index 0000000..8bcf060 --- /dev/null +++ b/public/422.html @@ -0,0 +1,114 @@ +<!doctype html> + +<html lang="en"> + + <head> + + <title>The change you wanted was rejected (422 Unprocessable Entity)</title> + + <meta charset="utf-8"> + <meta name="viewport" content="initial-scale=1, width=device-width"> + <meta name="robots" content="noindex, nofollow"> + + <style> + + *, *::before, *::after { + box-sizing: border-box; + } + + * { + margin: 0; + } + + html { + font-size: 16px; + } + + body { + background: #FFF; + color: #261B23; + display: grid; + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Aptos, Roboto, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-size: clamp(1rem, 2.5vw, 2rem); + -webkit-font-smoothing: antialiased; + font-style: normal; + font-weight: 400; + letter-spacing: -0.0025em; + line-height: 1.4; + min-height: 100vh; + place-items: center; + text-rendering: optimizeLegibility; + -webkit-text-size-adjust: 100%; + } + + a { + color: inherit; + font-weight: 700; + text-decoration: underline; + text-underline-offset: 0.0925em; + } + + b, strong { + font-weight: 700; + } + + i, em { + font-style: italic; + } + + main { + display: grid; + gap: 1em; + padding: 2em; + place-items: center; + text-align: center; + } + + main header { + width: min(100%, 12em); + } + + main header svg { + height: auto; + max-width: 100%; + width: 100%; + } + + main article { + width: min(100%, 30em); + } + + main article p { + font-size: 75%; + } + + main article br { + + display: none; + + @media(min-width: 48em) { + display: inline; + } + + } + + </style> + + </head> + + <body> + + <!-- This file lives in public/422.html --> + + <main> + <header> + <svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm130.453 51.63681c0-8.9215-6.218-15.4099-15.681-15.4099-10.273 0-15.95 7.5698-16.491 16.4913h-44.608c3.244-30.8199 25.683-55.421707 61.099-55.421707 36.498 0 59.477 20.816907 59.477 51.636807 0 21.3577-14.869 36.7676-31.901 52.7186l-27.305 27.035h59.747v37.308h-120.306v-27.846l57.044-56.7736c11.084-11.8954 18.925-20.0059 18.925-29.7385zm140.455 0c0-8.9215-6.218-15.4099-15.68-15.4099-10.274 0-15.951 7.5698-16.492 16.4913h-44.608c3.245-30.8199 25.684-55.421707 61.1-55.421707 36.497 0 59.477 20.816907 59.477 51.636807 0 21.3577-14.87 36.7676-31.902 52.7186l-27.305 27.035h59.747v37.308h-120.305v-27.846l57.043-56.7736c11.085-11.8954 18.925-20.0059 18.925-29.7385z" fill="#f0eff0"/><path d="m19.3936 103.554c-8.9715 0-14.84183-5.0952-14.84183-14.4544v-20.1029h8.86083v19.3276c0 4.8181 2.2706 7.3102 5.981 7.3102 3.6551 0 5.9257-2.4921 5.9257-7.3102v-19.3276h8.8608v20.1583c0 9.3038-5.8149 14.399-14.7865 14.399zm18.734-.554v-24.921h8.6947v2.1598c1.3845-1.5506 3.8212-2.7136 6.701-2.7136 5.538 0 8.8054 3.5997 8.8054 9.1377v16.3371h-8.6393v-14.2327c0-2.049-1.0522-3.5443-3.2674-3.5443-1.7168 0-3.1567.9969-3.5997 2.7136v15.0634zm36.8584-1.994v10.799h-8.6946v-33.726h8.6946v1.9937c1.163-1.3291 3.5997-2.5475 6.1472-2.5475 7.1994 0 11.1314 5.8703 11.1314 13.0143 0 7.0886-3.932 13.0145-11.1314 13.0145-2.5475 0-4.9842-1.219-6.1472-2.548zm0-13.7893v6.5902c.6646 1.3845 2.1599 2.326 3.8213 2.326 2.9905 0 4.7626-2.3814 4.7626-5.5934s-1.7721-5.6488-4.7626-5.6488c-1.7168 0-3.1567.9969-3.8213 2.326zm36.789-9.2485v8.3624c-1.052-.5538-2.215-.7753-3.6-.7753-2.381 0-3.987 1.0522-4.43 2.8244v14.6203h-8.6949v-24.921h8.6949v2.2152c1.218-1.6614 3.156-2.769 5.648-2.769 1.108 0 1.994.2215 2.382.443zm26.769 12.5713c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.528 0c0-3.4336-1.496-5.8703-4.209-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.209-2.3813 4.209-5.8149zm10.352 0c0-7.6978 5.095-13.0143 12.571-13.0143 6.701 0 10.855 3.9874 11.574 9.8023h-8.417c-.222-1.4953-1.385-2.6029-3.157-2.6029-2.437 0-3.987 2.2706-3.987 5.8149s1.55 5.7595 3.987 5.7595c1.772 0 2.935-1.0522 3.157-2.5475h8.417c-.719 5.7596-4.873 9.8025-11.574 9.8025-7.476 0-12.571-5.3167-12.571-13.0145zm42.013 3.7658h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.516-13.0143 7.642 0 11.962 5.095 11.962 12.5159v2.1598h-16.116c.277 2.9905 1.828 4.5965 4.32 4.5965 1.772 0 3.156-.7753 3.655-2.4921zm-3.821-10.0237c-2.049 0-3.434 1.2737-3.988 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.544-3.5997zm13.428 11.0206h8.473c.387 1.3845 1.606 2.1598 3.156 2.1598 1.44 0 2.548-.5538 2.548-1.7168 0-.9414-.72-1.2737-1.938-1.5506l-4.874-.9969c-4.153-.886-6.867-2.8797-6.867-7.2547 0-5.3165 4.763-8.4178 10.633-8.4178 6.812 0 10.522 3.1567 11.297 8.0855h-8.03c-.277-1.0522-1.052-1.9937-3.046-1.9937-1.273 0-2.326.5538-2.326 1.6614 0 .7753.554 1.163 1.717 1.3845l4.929 1.163c4.541 1.0522 6.978 3.4335 6.978 7.4763 0 5.3168-4.818 8.2518-10.91 8.2518-6.369 0-10.965-2.88-11.74-8.2518zm24.269 0h8.474c.387 1.3845 1.606 2.1598 3.156 2.1598 1.44 0 2.548-.5538 2.548-1.7168 0-.9414-.72-1.2737-1.939-1.5506l-4.873-.9969c-4.154-.886-6.867-2.8797-6.867-7.2547 0-5.3165 4.763-8.4178 10.633-8.4178 6.812 0 10.522 3.1567 11.297 8.0855h-8.03c-.277-1.0522-1.052-1.9937-3.046-1.9937-1.273 0-2.326.5538-2.326 1.6614 0 .7753.554 1.163 1.717 1.3845l4.929 1.163c4.541 1.0522 6.978 3.4335 6.978 7.4763 0 5.3168-4.818 8.2518-10.91 8.2518-6.369 0-10.965-2.88-11.741-8.2518zm47.918 7.6978h-8.363v-1.274c-.831.831-3.323 1.717-5.981 1.717-4.929 0-9.082-2.769-9.082-8.0301 0-4.818 4.153-7.9193 9.581-7.9193 2.049 0 4.485.6646 5.482 1.3845v-1.606c0-1.606-.941-2.9905-3.046-2.9905-1.606 0-2.547.7199-2.935 1.8275h-8.196c.72-4.8181 4.984-8.6393 11.408-8.6393 7.089 0 11.132 3.7659 11.132 10.2453zm-8.363-6.9779v-1.4399c-.554-1.0522-2.049-1.7167-3.655-1.7167-1.717 0-3.434.7199-3.434 2.3813 0 1.7168 1.717 2.4367 3.434 2.4367 1.606 0 3.101-.6645 3.655-1.6614zm20.742 4.9839v1.994h-8.695v-35.997h8.695v13.0697c1.163-1.3291 3.6-2.5475 6.147-2.5475 7.2 0 11.132 5.8149 11.132 13.0143s-3.932 13.0145-11.132 13.0145c-2.547 0-4.984-1.219-6.147-2.548zm0-13.7893v6.5902c.665 1.3845 2.105 2.326 3.821 2.326 2.991 0 4.763-2.3814 4.763-5.5934s-1.772-5.6488-4.763-5.6488c-1.661 0-3.156.9969-3.821 2.326zm28.759-20.2137v35.997h-8.695v-35.997zm19.172 27.3023h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.515-13.0143 7.643 0 11.962 5.095 11.962 12.5159v2.1598h-16.115c.277 2.9905 1.827 4.5965 4.32 4.5965 1.772 0 3.156-.7753 3.655-2.4921zm-3.822-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm25.461-15.2849h24.311v7.6424h-15.561v5.3165h14.232v7.4763h-14.232v5.8703h15.561v7.6978h-24.311zm27.942 34.0033v-24.921h8.694v2.1598c1.385-1.5506 3.822-2.7136 6.701-2.7136 5.538 0 8.806 3.5997 8.806 9.1377v16.3371h-8.639v-14.2327c0-2.049-1.053-3.5443-3.268-3.5443-1.717 0-3.157.9969-3.6 2.7136v15.0634zm29.991-8.5839v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.206v6.7564h-5.206v8.307c0 1.9383.941 2.769 2.658 2.769.942 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm26.161-16.3371v24.921h-8.694v-24.921zm.61-6.7564c0 2.8244-2.271 4.652-4.929 4.652s-4.929-1.8276-4.929-4.652c0-2.8797 2.271-4.7073 4.929-4.7073s4.929 1.8276 4.929 4.7073zm5.382 23.0935v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.206v6.7564h-5.206v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm29.22 17.3889h-8.584l3.655-9.414-9.303-24.312h9.026l4.763 14.1773 4.652-14.1773h8.639z" fill="#d30001"/></svg> + </header> + <article> + <p><strong>The change you wanted was rejected.</strong> Maybe you tried to change something you didn’t have access to. If you’re the application owner check the logs for more information.</p> + </article> + </main> + + </body> + +</html> diff --git a/public/500.html b/public/500.html new file mode 100644 index 0000000..d77718c --- /dev/null +++ b/public/500.html @@ -0,0 +1,114 @@ +<!doctype html> + +<html lang="en"> + + <head> + + <title>We’re sorry, but something went wrong (500 Internal Server Error)</title> + + <meta charset="utf-8"> + <meta name="viewport" content="initial-scale=1, width=device-width"> + <meta name="robots" content="noindex, nofollow"> + + <style> + + *, *::before, *::after { + box-sizing: border-box; + } + + * { + margin: 0; + } + + html { + font-size: 16px; + } + + body { + background: #FFF; + color: #261B23; + display: grid; + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Aptos, Roboto, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-size: clamp(1rem, 2.5vw, 2rem); + -webkit-font-smoothing: antialiased; + font-style: normal; + font-weight: 400; + letter-spacing: -0.0025em; + line-height: 1.4; + min-height: 100vh; + place-items: center; + text-rendering: optimizeLegibility; + -webkit-text-size-adjust: 100%; + } + + a { + color: inherit; + font-weight: 700; + text-decoration: underline; + text-underline-offset: 0.0925em; + } + + b, strong { + font-weight: 700; + } + + i, em { + font-style: italic; + } + + main { + display: grid; + gap: 1em; + padding: 2em; + place-items: center; + text-align: center; + } + + main header { + width: min(100%, 12em); + } + + main header svg { + height: auto; + max-width: 100%; + width: 100%; + } + + main article { + width: min(100%, 30em); + } + + main article p { + font-size: 75%; + } + + main article br { + + display: none; + + @media(min-width: 48em) { + display: inline; + } + + } + + </style> + + </head> + + <body> + + <!-- This file lives in public/500.html --> + + <main> + <header> + <svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m101.23 93.8427c-8.1103 0-15.4098 3.7849-19.7354 8.3813h-36.2269v-99.21891h103.8143v37.03791h-68.3984v24.8722c5.1366-2.7035 15.1396-5.9477 24.6014-5.9477 35.146 0 56.233 22.7094 56.233 55.4215 0 34.605-23.791 57.315-60.558 57.315-37.8492 0-61.64-22.169-63.8028-55.963h42.9857c1.0814 10.814 9.1919 19.195 21.6281 19.195 11.355 0 19.465-8.381 19.465-20.547 0-11.625-7.299-20.5463-20.006-20.5463zm138.833 77.8613c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm140.456 133.2831c-40.823 0-64.884-35.146-64.884-85.7015 0-50.5554 24.061-85.700907 64.884-85.700907 40.822 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.062 85.7015-64.884 85.7015zm0-133.2831c-17.573 0-22.71 21.8984-22.71 47.5816 0 25.6835 5.137 47.5815 22.71 47.5815 17.302 0 22.709-21.898 22.709-47.5815 0-25.6832-5.407-47.5816-22.709-47.5816z" fill="#f0eff0"/><path d="m23.1377 68.9967v34.0033h-8.9162v-34.0033zm4.3157 34.0033v-24.921h8.6947v2.1598c1.3845-1.5506 3.8212-2.7136 6.701-2.7136 5.538 0 8.8054 3.5997 8.8054 9.1377v16.3371h-8.6393v-14.2327c0-2.049-1.0522-3.5443-3.2674-3.5443-1.7168 0-3.1567.9969-3.5997 2.7136v15.0634zm29.9913-8.5839v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.5839v6.8671h5.2058v6.7564h-5.2058v8.307c0 1.9383.9415 2.769 2.6583 2.769.9414 0 1.9937-.2216 2.769-.5538v7.3654c-.9969.443-2.8798.775-4.8181.775-5.8703 0-9.1931-2.769-9.1931-9.0819zm32.3666-.1108h8.0301c-.8861 5.7597-5.2057 9.2487-11.6852 9.2487-7.6424 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.3165-13.0143 12.5159-13.0143 7.6424 0 11.9621 5.095 11.9621 12.5159v2.1598h-16.1156c.2769 2.9905 1.8275 4.5965 4.3196 4.5965 1.7722 0 3.1567-.7753 3.6551-2.4921zm-3.8212-10.0237c-2.0491 0-3.4336 1.2737-3.9874 3.5997h7.5317c-.1107-2.0491-1.3845-3.5997-3.5443-3.5997zm31.4299-6.3134v8.3624c-1.052-.5538-2.215-.7753-3.599-.7753-2.382 0-3.988 1.0522-4.431 2.8244v14.6203h-8.694v-24.921h8.694v2.2152c1.219-1.6614 3.157-2.769 5.649-2.769 1.108 0 1.994.2215 2.381.443zm2.949 25.0318v-24.921h8.694v2.1598c1.385-1.5506 3.821-2.7136 6.701-2.7136 5.538 0 8.806 3.5997 8.806 9.1377v16.3371h-8.64v-14.2327c0-2.049-1.052-3.5443-3.267-3.5443-1.717 0-3.157.9969-3.6 2.7136v15.0634zm50.371 0h-8.363v-1.274c-.83.831-3.323 1.717-5.981 1.717-4.929 0-9.082-2.769-9.082-8.0301 0-4.818 4.153-7.9193 9.581-7.9193 2.049 0 4.485.6646 5.482 1.3845v-1.606c0-1.606-.941-2.9905-3.046-2.9905-1.606 0-2.547.7199-2.935 1.8275h-8.196c.72-4.8181 4.984-8.6393 11.408-8.6393 7.089 0 11.132 3.7659 11.132 10.2453zm-8.363-6.9779v-1.4399c-.554-1.0522-2.049-1.7167-3.655-1.7167-1.717 0-3.433.7199-3.433 2.3813 0 1.7168 1.716 2.4367 3.433 2.4367 1.606 0 3.101-.6645 3.655-1.6614zm20.742-29.0191v35.997h-8.694v-35.997zm13.036 25.9178h9.248c.72 2.326 2.714 3.489 5.483 3.489 2.713 0 4.596-1.163 4.596-3.2674 0-1.6061-1.052-2.326-3.212-2.8244l-6.534-1.3845c-4.985-1.1076-8.751-3.7105-8.751-9.47 0-6.6456 5.538-11.0206 13.07-11.0206 8.307 0 13.014 4.5411 13.956 10.4114h-8.695c-.72-1.8829-2.27-3.3228-5.205-3.3228-2.548 0-4.265 1.1076-4.265 2.9905 0 1.4953 1.052 2.326 2.825 2.7137l6.645 1.5506c5.815 1.3845 9.027 4.5412 9.027 9.8023 0 6.9778-5.87 10.9654-13.291 10.9654-8.141 0-13.679-3.9322-14.897-10.6332zm46.509 1.3845h8.031c-.887 5.7597-5.206 9.2487-11.686 9.2487-7.642 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.317-13.0143 12.516-13.0143 7.643 0 11.962 5.095 11.962 12.5159v2.1598h-16.115c.277 2.9905 1.827 4.5965 4.319 4.5965 1.773 0 3.157-.7753 3.655-2.4921zm-3.821-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm31.431-6.3134v8.3624c-1.053-.5538-2.216-.7753-3.6-.7753-2.381 0-3.988 1.0522-4.431 2.8244v14.6203h-8.694v-24.921h8.694v2.2152c1.219-1.6614 3.157-2.769 5.649-2.769 1.108 0 1.994.2215 2.382.443zm18.288 25.0318h-7.809l-9.47-24.921h8.861l4.763 14.288 4.652-14.288h8.528zm25.614-8.6947h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.642 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.516-13.0143 7.642 0 11.962 5.095 11.962 12.5159v2.1598h-16.116c.277 2.9905 1.828 4.5965 4.32 4.5965 1.772 0 3.157-.7753 3.655-2.4921zm-3.821-10.0237c-2.049 0-3.434 1.2737-3.988 3.5997h7.532c-.111-2.0491-1.384-3.5997-3.544-3.5997zm31.43-6.3134v8.3624c-1.052-.5538-2.215-.7753-3.6-.7753-2.381 0-3.987 1.0522-4.43 2.8244v14.6203h-8.695v-24.921h8.695v2.2152c1.218-1.6614 3.157-2.769 5.649-2.769 1.107 0 1.993.2215 2.381.443zm13.703-8.9715h24.312v7.6424h-15.562v5.3165h14.232v7.4763h-14.232v5.8703h15.562v7.6978h-24.312zm44.667 8.9715v8.3624c-1.052-.5538-2.215-.7753-3.6-.7753-2.381 0-3.987 1.0522-4.43 2.8244v14.6203h-8.695v-24.921h8.695v2.2152c1.218-1.6614 3.156-2.769 5.648-2.769 1.108 0 1.994.2215 2.382.443zm19.673 0v8.3624c-1.053-.5538-2.216-.7753-3.6-.7753-2.381 0-3.987 1.0522-4.43 2.8244v14.6203h-8.695v-24.921h8.695v2.2152c1.218-1.6614 3.156-2.769 5.648-2.769 1.108 0 1.994.2215 2.382.443zm26.769 12.5713c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.529 0c0-3.4336-1.495-5.8703-4.208-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.208-2.3813 4.208-5.8149zm28.082-12.5713v8.3624c-1.052-.5538-2.215-.7753-3.6-.7753-2.381 0-3.987 1.0522-4.43 2.8244v14.6203h-8.695v-24.921h8.695v2.2152c1.218-1.6614 3.157-2.769 5.649-2.769 1.107 0 1.993.2215 2.381.443z" fill="#d30001"/></svg> + </header> + <article> + <p><strong>We’re sorry, but something went wrong.</strong><br> If you’re the application owner check the logs for more information.</p> + </article> + </main> + + </body> + +</html> diff --git a/public/icon.png b/public/icon.png Binary files differnew file mode 100644 index 0000000..c4c9dbf --- /dev/null +++ b/public/icon.png diff --git a/public/icon.svg b/public/icon.svg new file mode 100644 index 0000000..04b34bf --- /dev/null +++ b/public/icon.svg @@ -0,0 +1,3 @@ +<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg"> + <circle cx="256" cy="256" r="256" fill="red"/> +</svg> diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..c19f78a --- /dev/null +++ b/public/robots.txt @@ -0,0 +1 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/script/.keep b/script/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/script/.keep diff --git a/storage/.keep b/storage/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/storage/.keep diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb new file mode 100644 index 0000000..cee29fd --- /dev/null +++ b/test/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ] +end diff --git a/test/controllers/.keep b/test/controllers/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/controllers/.keep diff --git a/test/controllers/home_controller_test.rb b/test/controllers/home_controller_test.rb new file mode 100644 index 0000000..aab02bb --- /dev/null +++ b/test/controllers/home_controller_test.rb @@ -0,0 +1,16 @@ +require "test_helper" + +class HomeControllerTest < ActionDispatch::IntegrationTest + test "should get index" do + get root_url + assert_response :success + assert_select "h1", "World Tartiflette Tour 2024" + end + + test "should list tartiflettes in the index" do + tartiflette = Tartiflette.create!(scoring_id: 1) + get root_url + assert_response :success + assert_select "li", tartiflette.scoring_id.to_s + end +end diff --git a/test/controllers/scores_controller_test.rb b/test/controllers/scores_controller_test.rb new file mode 100644 index 0000000..8fb4cb8 --- /dev/null +++ b/test/controllers/scores_controller_test.rb @@ -0,0 +1,70 @@ +require "test_helper" + +class ScoresControllerTest < ActionDispatch::IntegrationTest + setup do + @tartiflette = Tartiflette.create!(scoring_id: 1) + @criterium = ScoringCriterium.create!(name: "Taste", category: "Flavor") + @score = Score.create!(tartiflette: @tartiflette, scoring_criterium: @criterium, value: 4) + end + + test "should get new score form" do + get new_tartiflette_score_path(@tartiflette) + assert_response :success + assert_select "form" + assert_select "select[name=?]", "scores[#{@criterium.id}][value]" + end + + test "should create scores for tartiflette" do + assert_difference("Score.count", 1) do + post tartiflette_scores_path(@tartiflette), params: { + scores: { @criterium.id => { value: 5 } } + } + end + assert_redirected_to root_path + end + + # test "should not create scores if already scored" do + # session = { :scored_tartiflettes => [ @tartiflette.id ] } + # assert_no_difference("Score.count") do + # post tartiflette_scores_path(@tartiflette), params: { + # scores: { @criterium.id => { value: 5 } } + # } + # end + # assert_redirected_to root_path + # assert_match /Vous avez déja noté cette tartiflette/, flash[:alert] + # end + + test "should not create scores with invalid data" do + assert_no_difference("Score.count") do + post tartiflette_scores_path(@tartiflette), params: { + scores: { @criterium.id => { value: nil } } + } + end + assert_response :unprocessable_entity + end + + test "should get edit scores form" do + get tartiflette_edit_scores_path(@tartiflette) + assert_response :success + assert_select "form" + assert_select "select[name=?]", "scores[#{@criterium.id}][value]" + end + + test "should update scores for tartiflette" do + patch tartiflette_update_scores_path(@tartiflette), params: { + scores: { @score.id => { value: 3 } } + } + assert_redirected_to root_path + @score.reload + assert_equal 3, @score.value + end + + # test "should not update scores with invalid data" do + # patch tartiflette_update_scores_path(@tartiflette), params: { + # scores: { @score.id => { value: nil } } + # } + # @score.reload + # assert_not_equal nil, @score.value + # assert_redirected_to tartiflette_edit_scores_path(@tartiflette) + # end +end diff --git a/test/fixtures/files/.keep b/test/fixtures/files/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/fixtures/files/.keep diff --git a/test/fixtures/scores.yml b/test/fixtures/scores.yml new file mode 100644 index 0000000..b71217b --- /dev/null +++ b/test/fixtures/scores.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + tartiflette: one + scoring_criterium: one + value: 1 + +two: + tartiflette: two + scoring_criterium: two + value: 2 diff --git a/test/fixtures/scoring_criteria.yml b/test/fixtures/scoring_criteria.yml new file mode 100644 index 0000000..e469210 --- /dev/null +++ b/test/fixtures/scoring_criteria.yml @@ -0,0 +1,50 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + category: visuel + name: présence de la pastille + +two: + category: visuel + name: croûte dorée + +three: + category: visuel + name: lardons apparents + + +four: + category: texture + name: Pommes de terre fondantes + +five: + category: texture + name: lardons grillés + +six: + category: texture + name: oignons biens cuits + + +seven: + category: goût + name: Reblochon savoureux + +eight: + category: goût + name: lardons appétants + +nine: + category: goût + name: vin blanc équilibré + + +ten: + category: special + name: on en reveut ! +eleven: + category: special + name: Quelqu'un cherche à dépasser les maestros... +twelve: + category: special + name: on reconnaît la patte du tartifleur diff --git a/test/fixtures/tartiflettes.yml b/test/fixtures/tartiflettes.yml new file mode 100644 index 0000000..ad75608 --- /dev/null +++ b/test/fixtures/tartiflettes.yml @@ -0,0 +1,7 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + scoring_id: 1 + +two: + scoring_id: 2 diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml new file mode 100644 index 0000000..0951563 --- /dev/null +++ b/test/fixtures/users.yml @@ -0,0 +1,9 @@ +<% password_digest = BCrypt::Password.create("password") %> + +one: + email_address: one@example.com + password_digest: <%= password_digest %> + +two: + email_address: two@example.com + password_digest: <%= password_digest %> diff --git a/test/helpers/.keep b/test/helpers/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/helpers/.keep diff --git a/test/integration/.keep b/test/integration/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/integration/.keep diff --git a/test/mailers/.keep b/test/mailers/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/mailers/.keep diff --git a/test/mailers/previews/passwords_mailer_preview.rb b/test/mailers/previews/passwords_mailer_preview.rb new file mode 100644 index 0000000..01d07ec --- /dev/null +++ b/test/mailers/previews/passwords_mailer_preview.rb @@ -0,0 +1,7 @@ +# Preview all emails at http://localhost:3000/rails/mailers/passwords_mailer +class PasswordsMailerPreview < ActionMailer::Preview + # Preview this email at http://localhost:3000/rails/mailers/passwords_mailer/reset + def reset + PasswordsMailer.reset(User.take) + end +end diff --git a/test/models/.keep b/test/models/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/models/.keep diff --git a/test/models/score_test.rb b/test/models/score_test.rb new file mode 100644 index 0000000..a895158 --- /dev/null +++ b/test/models/score_test.rb @@ -0,0 +1,41 @@ +require "test_helper" + +class ScoreTest < ActiveSupport::TestCase + def setup + @score = scores(:one) + end + + test "should be valid with valid attributes" do + assert @score.valid? + end + + test "should have associated tartiflette" do + @score.tartiflette = nil + assert_not @score.valid?, "Score has no associated tartiflette." + end + + test "should have associated scoring_criterium" do + @score.scoring_criterium = nil + assert_not @score.valid?, "Score has no associated scoring_criterium." + end + + test "should have value" do + @score.value = nil + assert_not @score.valid?, "Score has no value." + end + + test "value should be between 1 and 5" do + @score.value = 3 + assert @score.valid?, "Score value is invalid." + end + + test "value should be greater than or equal to 1" do + @score.value = 0 + assert_not @score.valid?, "Score is less than 1." + end + + test "value should be less than or equal to 5" do + @score.value = 6 + assert_not @score.valid?, "Score is greater than 5." + end +end diff --git a/test/models/scoring_criterium_test.rb b/test/models/scoring_criterium_test.rb new file mode 100644 index 0000000..754e876 --- /dev/null +++ b/test/models/scoring_criterium_test.rb @@ -0,0 +1,21 @@ +require "test_helper" + +class ScoringCriteriumTest < ActiveSupport::TestCase + def setup + @scoring_criterium = scoring_criteria(:one) + end + + test "should be valid with valid attributes" do + assert @scoring_criterium.valid? + end + + test "should have category" do + @scoring_criterium.category = nil + assert_not @scoring_criterium.valid?, "Scoring Criterium has no category." + end + + test "should have name" do + @scoring_criterium.name = nil + assert_not @scoring_criterium.valid?, "Scoring Criterium has no name." + end +end diff --git a/test/models/tartiflette_test.rb b/test/models/tartiflette_test.rb new file mode 100644 index 0000000..63a4150 --- /dev/null +++ b/test/models/tartiflette_test.rb @@ -0,0 +1,11 @@ +require "test_helper" + +class TartifletteTest < ActiveSupport::TestCase + def setup + @tartiflette = tartiflettes(:one) + end + + test "should be valid with valid attributes" do + assert @tartiflette.valid? + end +end diff --git a/test/models/user_test.rb b/test/models/user_test.rb new file mode 100644 index 0000000..5c07f49 --- /dev/null +++ b/test/models/user_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class UserTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/services/tartiflette_scoring_service.rb b/test/services/tartiflette_scoring_service.rb new file mode 100644 index 0000000..6796342 --- /dev/null +++ b/test/services/tartiflette_scoring_service.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class TartifletteScoringServiceTest < ActiveSupport::TestCase + setup do + @tartiflette = Tartiflette.new(scoring_id: 1) + @session = {} + end + + test "should check if tartiflette is already scored" do + @session[:scored_tartiflettes] = [ @tartiflette.id ] + assert TartifletteScoringService.scored?(@tartiflette, @session) + end + + test "should mark tartiflette as scored" do + TartifletteScoringService.mark_as_scored(@tartiflette, @session) + assert_includes @session[:scored_tartiflettes], @tartiflette.id + end + + test "should calculate average of all scores for a tartiflette" do + Score.create!(tartiflette: @tartiflette, + scoring_criterium: scoring_criteria(:one), + value: 4) + Score.create!(tartiflette: @tartiflette, + scoring_criterium: scoring_criteria(:two), + value: 5) + assert_equal 4.5, TartifletteScoringService.average_score(@tartiflette) + end + + test "should calculate average score by category for a tartiflette" do + Score.create!(tartiflette: @tartiflette, + scoring_criterium: scoring_criteria(:one), + value: 1) + Score.create!(tartiflette: @tartiflette, + scoring_criterium: scoring_criteria(:one), + value: 2) + Score.create!(tartiflette: @tartiflette, + scoring_criterium: scoring_criteria(:four), + value: 4) + Score.create!(tartiflette: @tartiflette, + scoring_criterium: scoring_criteria(:four), + value: 5) + average_score_by_category = { scoring_criteria(:one)[:category] => 1.5, + scoring_criteria(:four)[:category] => 4.5 + } + assert_equal average_score_by_category, + TartifletteScoringService.average_score_by_category(@tartiflette) + end +end diff --git a/test/system/.keep b/test/system/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/system/.keep diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..0c22470 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,15 @@ +ENV["RAILS_ENV"] ||= "test" +require_relative "../config/environment" +require "rails/test_help" + +module ActiveSupport + class TestCase + # Run tests in parallel with specified workers + parallelize(workers: :number_of_processors) + + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all + + # Add more helper methods to be used by all tests here... + end +end diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tmp/.keep diff --git a/tmp/pids/.keep b/tmp/pids/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tmp/pids/.keep diff --git a/tmp/storage/.keep b/tmp/storage/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tmp/storage/.keep diff --git a/vendor/.gitignore b/vendor/.gitignore new file mode 100644 index 0000000..92d0b93 --- /dev/null +++ b/vendor/.gitignore @@ -0,0 +1 @@ +bundle/
\ No newline at end of file diff --git a/vendor/.keep b/vendor/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/vendor/.keep |