From be2a93525069de2dfa3c23b0c23e7a9f7ad4c03d Mon Sep 17 00:00:00 2001 From: Marius Peter Date: Sun, 29 Dec 2024 15:14:43 +0100 Subject: First commit. --- app/assets/images/.keep | 0 app/assets/images/blason_Savoie.png | Bin 0 -> 33610 bytes app/assets/stylesheets/application.css | 27 +++++++++++ .../stylesheets/components/code_of_honor.css | 11 +++++ app/assets/stylesheets/components/footer.css | 6 +++ app/assets/stylesheets/components/nav_top.css | 40 +++++++++++++++ .../stylesheets/components/notifications.css | 18 +++++++ app/assets/stylesheets/pages/home.css | 4 ++ app/channels/application_cable/connection.rb | 16 ++++++ app/controllers/admin/dashboard_controller.rb | 13 +++++ app/controllers/admin/scores_controller.rb | 14 ++++++ app/controllers/application_controller.rb | 5 ++ app/controllers/code_of_honor_controller.rb | 12 +++++ app/controllers/concerns/.keep | 0 app/controllers/concerns/authentication.rb | 52 ++++++++++++++++++++ app/controllers/home_controller.rb | 7 +++ app/controllers/passwords_controller.rb | 33 +++++++++++++ app/controllers/registrations_controller.rb | 21 ++++++++ app/controllers/scores_controller.rb | 49 +++++++++++++++++++ app/controllers/sessions_controller.rb | 21 ++++++++ app/controllers/tartiflettes_controller.rb | 26 ++++++++++ app/helpers/application_helper.rb | 2 + app/helpers/home_helper.rb | 2 + app/jobs/application_job.rb | 7 +++ app/mailers/application_mailer.rb | 4 ++ app/mailers/passwords_mailer.rb | 6 +++ app/models/application_record.rb | 3 ++ app/models/concerns/.keep | 0 app/models/current.rb | 4 ++ app/models/score.rb | 6 +++ app/models/scoring_criterium.rb | 9 ++++ app/models/session.rb | 3 ++ app/models/tartiflette.rb | 3 ++ app/models/user.rb | 6 +++ app/services/tartiflette_score_export_service.rb | 19 ++++++++ app/services/tartiflette_scoring_service.rb | 54 +++++++++++++++++++++ app/views/admin/dashboard/index.html.erb | 10 ++++ app/views/admin/dashboard/tmp | 19 ++++++++ app/views/home/_code_of_honor.html.erb | 14 ++++++ app/views/home/index.html.erb | 42 ++++++++++++++++ app/views/layouts/_footer.html.erb | 3 ++ app/views/layouts/_notifications.html.erb | 8 +++ app/views/layouts/_topnav.html.erb | 5 ++ app/views/layouts/application.html.erb | 33 +++++++++++++ app/views/layouts/mailer.html.erb | 13 +++++ app/views/layouts/mailer.text.erb | 1 + app/views/passwords/edit.html.erb | 9 ++++ app/views/passwords/new.html.erb | 8 +++ app/views/passwords_mailer/reset.html.erb | 4 ++ app/views/passwords_mailer/reset.text.erb | 2 + app/views/pwa/manifest.json.erb | 22 +++++++++ app/views/pwa/service-worker.js | 26 ++++++++++ app/views/registrations/new.html.erb | 17 +++++++ app/views/scores/_form.html.erb | 19 ++++++++ app/views/scores/edit_all.html.erb | 7 +++ app/views/scores/new.html.erb | 7 +++ app/views/sessions/new.html.erb | 20 ++++++++ app/views/tartiflettes/index.html.erb | 3 ++ app/views/tartiflettes/show.html.erb | 3 ++ 59 files changed, 798 insertions(+) create mode 100644 app/assets/images/.keep create mode 100644 app/assets/images/blason_Savoie.png create mode 100644 app/assets/stylesheets/application.css create mode 100644 app/assets/stylesheets/components/code_of_honor.css create mode 100644 app/assets/stylesheets/components/footer.css create mode 100644 app/assets/stylesheets/components/nav_top.css create mode 100644 app/assets/stylesheets/components/notifications.css create mode 100644 app/assets/stylesheets/pages/home.css create mode 100644 app/channels/application_cable/connection.rb create mode 100644 app/controllers/admin/dashboard_controller.rb create mode 100644 app/controllers/admin/scores_controller.rb create mode 100644 app/controllers/application_controller.rb create mode 100644 app/controllers/code_of_honor_controller.rb create mode 100644 app/controllers/concerns/.keep create mode 100644 app/controllers/concerns/authentication.rb create mode 100644 app/controllers/home_controller.rb create mode 100644 app/controllers/passwords_controller.rb create mode 100644 app/controllers/registrations_controller.rb create mode 100644 app/controllers/scores_controller.rb create mode 100644 app/controllers/sessions_controller.rb create mode 100644 app/controllers/tartiflettes_controller.rb create mode 100644 app/helpers/application_helper.rb create mode 100644 app/helpers/home_helper.rb create mode 100644 app/jobs/application_job.rb create mode 100644 app/mailers/application_mailer.rb create mode 100644 app/mailers/passwords_mailer.rb create mode 100644 app/models/application_record.rb create mode 100644 app/models/concerns/.keep create mode 100644 app/models/current.rb create mode 100644 app/models/score.rb create mode 100644 app/models/scoring_criterium.rb create mode 100644 app/models/session.rb create mode 100644 app/models/tartiflette.rb create mode 100644 app/models/user.rb create mode 100644 app/services/tartiflette_score_export_service.rb create mode 100644 app/services/tartiflette_scoring_service.rb create mode 100644 app/views/admin/dashboard/index.html.erb create mode 100644 app/views/admin/dashboard/tmp create mode 100644 app/views/home/_code_of_honor.html.erb create mode 100644 app/views/home/index.html.erb create mode 100644 app/views/layouts/_footer.html.erb create mode 100644 app/views/layouts/_notifications.html.erb create mode 100644 app/views/layouts/_topnav.html.erb create mode 100644 app/views/layouts/application.html.erb create mode 100644 app/views/layouts/mailer.html.erb create mode 100644 app/views/layouts/mailer.text.erb create mode 100644 app/views/passwords/edit.html.erb create mode 100644 app/views/passwords/new.html.erb create mode 100644 app/views/passwords_mailer/reset.html.erb create mode 100644 app/views/passwords_mailer/reset.text.erb create mode 100644 app/views/pwa/manifest.json.erb create mode 100644 app/views/pwa/service-worker.js create mode 100644 app/views/registrations/new.html.erb create mode 100644 app/views/scores/_form.html.erb create mode 100644 app/views/scores/edit_all.html.erb create mode 100644 app/views/scores/new.html.erb create mode 100644 app/views/sessions/new.html.erb create mode 100644 app/views/tartiflettes/index.html.erb create mode 100644 app/views/tartiflettes/show.html.erb (limited to 'app') diff --git a/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/images/blason_Savoie.png b/app/assets/images/blason_Savoie.png new file mode 100644 index 0000000..51761d0 Binary files /dev/null and b/app/assets/images/blason_Savoie.png 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 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 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 @@ +

Administrateur

+ + + +

+ <%= button_to "Déconnexion", + session_path(session), + method: :delete %> +

+

<%= link_to "Télécharger tous les scores en format CSV", admin_scores_export_path %>

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| %> +

<%= category.capitalize %>

+ + + + + + + + + <% @tartiflettes.each do |tartiflette| %> + + + + + <% end %> + +
NuméroScore
<%= tartiflette.scoring_id %><%= tartiflette.scores.where(&:scoring_criterium.include? criteria) %>
+<% 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 @@ +
+

Code d'honneur

+

+ 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é + ! +

+ <% 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 %> +
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 @@ +

World Tartiflette Tour 2024

+ +<%= image_tag("blason_Savoie.png", :alt => "blason de la Savoie", id: "blason", width: 80) %> + +<%= render "code_of_honor" %> + +

Noter les Tartiflettes

+ + +

+ <%= link_to "Admin", admin_dashboard_path %> +

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 @@ + 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 @@ +
+ <% if alert %> +

<%= alert %>

+ <% end %> + <% if notice %> +

<%= notice %>

+ <% end %> +
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 @@ + 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 @@ + + + + <%= content_for(:title) || "WTT" %> + + + + <%= 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" %> + + + <%# Includes all stylesheet files in app/assets/stylesheets %> + <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> + + + + <%= render "layouts/topnav" %> + <%= render "layouts/notifications" %> +
+ <%= yield %> +
+ <%= render "layouts/footer" %> + + 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 @@ + + + + + + + + + <%= yield %> + + 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 @@ +

Update your password

+ +<%= 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 %>
+ <%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72 %>
+ <%= 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 @@ +

Forgot your password?

+ +<%= 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] %>
+ <%= 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 @@ +

+ You can reset your password within the next 15 minutes on + <%= link_to "this password reset page", edit_password_url(@user.password_reset_token) %>. +

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 @@ +

Sign Up

+ +<%= form_with model: @user, url: registration_path, method: :post, local: true do |f| %> +
+ <%= f.label :email_address %> + <%= f.email_field :email_address, required: true %> +
+
+ <%= f.label :password %> + <%= f.password_field :password, required: true %> +
+
+ <%= f.label :password_confirmation %> + <%= f.password_field :password_confirmation, required: true %> +
+ <%= 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| %> +
+ <%= category.titlecase %> + <% criteria.each do |criterium| %> + <% current_score = existing_scores.find { |score| score.scoring_criterium_id == criterium.id } %> +

+ <%= 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 %> +

+ <% end %> +
+ <% 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 @@ +

Modifier les Notes pour la Tartiflette nº<%= @tartiflette.scoring_id %>

+ +<%= 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 @@ +

Noter la Tartiflette nº<%= @tartiflette.scoring_id %>

+ +<%= 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] %>
+ <%= form.password_field :password, + required: true, + autocomplete: "current-password", + placeholder: "Enter your password", + maxlength: 72 %>
+ <%= form.submit "Sign in" %> +<% end %> +
+ +<%= 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 @@ +

Tartiflettes

+ +

Toutes les tartiflettes

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 @@ +

Tartiflette nº<%= @tartiflette.scoring_id %>

+ +

Scores obtenus pour une tartiflette donnée.

-- cgit v1.2.3