summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/.keep0
-rw-r--r--app/assets/images/blason_Savoie.pngbin0 -> 33610 bytes
-rw-r--r--app/assets/stylesheets/application.css27
-rw-r--r--app/assets/stylesheets/components/code_of_honor.css11
-rw-r--r--app/assets/stylesheets/components/footer.css6
-rw-r--r--app/assets/stylesheets/components/nav_top.css40
-rw-r--r--app/assets/stylesheets/components/notifications.css18
-rw-r--r--app/assets/stylesheets/pages/home.css4
-rw-r--r--app/channels/application_cable/connection.rb16
-rw-r--r--app/controllers/admin/dashboard_controller.rb13
-rw-r--r--app/controllers/admin/scores_controller.rb14
-rw-r--r--app/controllers/application_controller.rb5
-rw-r--r--app/controllers/code_of_honor_controller.rb12
-rw-r--r--app/controllers/concerns/.keep0
-rw-r--r--app/controllers/concerns/authentication.rb52
-rw-r--r--app/controllers/home_controller.rb7
-rw-r--r--app/controllers/passwords_controller.rb33
-rw-r--r--app/controllers/registrations_controller.rb21
-rw-r--r--app/controllers/scores_controller.rb49
-rw-r--r--app/controllers/sessions_controller.rb21
-rw-r--r--app/controllers/tartiflettes_controller.rb26
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/home_helper.rb2
-rw-r--r--app/jobs/application_job.rb7
-rw-r--r--app/mailers/application_mailer.rb4
-rw-r--r--app/mailers/passwords_mailer.rb6
-rw-r--r--app/models/application_record.rb3
-rw-r--r--app/models/concerns/.keep0
-rw-r--r--app/models/current.rb4
-rw-r--r--app/models/score.rb6
-rw-r--r--app/models/scoring_criterium.rb9
-rw-r--r--app/models/session.rb3
-rw-r--r--app/models/tartiflette.rb3
-rw-r--r--app/models/user.rb6
-rw-r--r--app/services/tartiflette_score_export_service.rb19
-rw-r--r--app/services/tartiflette_scoring_service.rb54
-rw-r--r--app/views/admin/dashboard/index.html.erb10
-rw-r--r--app/views/admin/dashboard/tmp19
-rw-r--r--app/views/home/_code_of_honor.html.erb14
-rw-r--r--app/views/home/index.html.erb42
-rw-r--r--app/views/layouts/_footer.html.erb3
-rw-r--r--app/views/layouts/_notifications.html.erb8
-rw-r--r--app/views/layouts/_topnav.html.erb5
-rw-r--r--app/views/layouts/application.html.erb33
-rw-r--r--app/views/layouts/mailer.html.erb13
-rw-r--r--app/views/layouts/mailer.text.erb1
-rw-r--r--app/views/passwords/edit.html.erb9
-rw-r--r--app/views/passwords/new.html.erb8
-rw-r--r--app/views/passwords_mailer/reset.html.erb4
-rw-r--r--app/views/passwords_mailer/reset.text.erb2
-rw-r--r--app/views/pwa/manifest.json.erb22
-rw-r--r--app/views/pwa/service-worker.js26
-rw-r--r--app/views/registrations/new.html.erb17
-rw-r--r--app/views/scores/_form.html.erb19
-rw-r--r--app/views/scores/edit_all.html.erb7
-rw-r--r--app/views/scores/new.html.erb7
-rw-r--r--app/views/sessions/new.html.erb20
-rw-r--r--app/views/tartiflettes/index.html.erb3
-rw-r--r--app/views/tartiflettes/show.html.erb3
59 files changed, 798 insertions, 0 deletions
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
new file mode 100644
index 0000000..51761d0
--- /dev/null
+++ b/app/assets/images/blason_Savoie.png
Binary files differ
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>&copy; <%= 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>
Copyright 2019--2025 Marius PETER