From f706add16c3e30013bd1d255a0d68aef77bc2ac1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 09:11:12 +0000 Subject: [PATCH 1/2] chore(deps): bump the github-actions group with 4 updates Bumps the github-actions group with 4 updates: [dorny/paths-filter](https://github.com/dorny/paths-filter), [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action), [docker/build-push-action](https://github.com/docker/build-push-action) and [docker/login-action](https://github.com/docker/login-action). Updates `dorny/paths-filter` from 3 to 4 - [Release notes](https://github.com/dorny/paths-filter/releases) - [Changelog](https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md) - [Commits](https://github.com/dorny/paths-filter/compare/v3...v4) Updates `docker/setup-buildx-action` from 3 to 4 - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4) Updates `docker/build-push-action` from 6 to 7 - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v6...v7) Updates `docker/login-action` from 3 to 4 - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/v3...v4) --- updated-dependencies: - dependency-name: dorny/paths-filter dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: docker/setup-buildx-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: docker/build-push-action dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: docker/login-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/ci-dev.yml | 2 +- .github/workflows/ci-docker-cache.yml | 2 +- .github/workflows/deploy-staging.yml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-dev.yml b/.github/workflows/ci-dev.yml index dd3d97a5..c0d7ee9b 100644 --- a/.github/workflows/ci-dev.yml +++ b/.github/workflows/ci-dev.yml @@ -22,7 +22,7 @@ jobs: code: ${{ steps.filter.outputs.code }} steps: - uses: actions/checkout@v6 - - uses: dorny/paths-filter@v3 + - uses: dorny/paths-filter@v4 id: filter with: filters: | diff --git a/.github/workflows/ci-docker-cache.yml b/.github/workflows/ci-docker-cache.yml index cfa0b8dc..1ef8047f 100644 --- a/.github/workflows/ci-docker-cache.yml +++ b/.github/workflows/ci-docker-cache.yml @@ -23,7 +23,7 @@ jobs: code: ${{ steps.filter.outputs.code }} steps: - uses: actions/checkout@v6 - - uses: dorny/paths-filter@v3 + - uses: dorny/paths-filter@v4 id: filter with: filters: | diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 3502ea47..5af84492 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -21,13 +21,13 @@ jobs: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: - uses: actions/checkout@v6 - - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 + - uses: docker/setup-buildx-action@v4 + - uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: docker/build-push-action@v6 + - uses: docker/build-push-action@v7 with: context: . push: true From c39270efec584475fbd5eff51e7a5c37b4882846 Mon Sep 17 00:00:00 2001 From: AkaKwak Date: Mon, 11 May 2026 13:49:11 +0200 Subject: [PATCH 2/2] refactor: update opening hours management to use database instead of cache - Replaced cached opening hours retrieval with a new method to fetch current opening hours from the database across multiple controllers. - Introduced a new OpeningHour model to manage opening hours data, including validations and methods for schedule management. - Updated the admin dashboard and related views to reflect changes in opening hours handling. - Added migration to persist opening hours in the database and ensure proper structure for future updates. --- app/controllers/admin/dashboard_controller.rb | 2 +- .../admin/opening_hours_controller.rb | 48 ++++----- app/controllers/home_controller.rb | 2 +- app/controllers/pages_controller.rb | 2 +- app/helpers/opening_hours_helper.rb | 16 ++- app/models/opening_hour.rb | 97 +++++++++++++++++++ .../opening_hours_updater.rb | 4 +- app/views/pages/become_member.html.erb | 2 +- ...06101500_persist_opening_hours_schedule.rb | 70 +++++++++++++ db/schema.rb | 9 +- spec/factories/opening_hours.rb | 17 ++++ spec/models/opening_hour_spec.rb | 41 ++++++++ spec/requests/admin/dashboard_spec.rb | 6 +- spec/requests/admin/opening_hours_spec.rb | 54 +++++++++++ 14 files changed, 326 insertions(+), 44 deletions(-) create mode 100644 app/models/opening_hour.rb create mode 100644 db/migrate/20260506101500_persist_opening_hours_schedule.rb create mode 100644 spec/factories/opening_hours.rb create mode 100644 spec/models/opening_hour_spec.rb create mode 100644 spec/requests/admin/opening_hours_spec.rb diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index f5956a00..4de1feef 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -7,7 +7,7 @@ class DashboardController < BaseController def index @notepad = Rails.cache.fetch("notepad") || default_notepad - @opening_hours = Rails.cache.fetch("opening_hours") || default_opening_hours + @opening_hours = current_opening_hours @stats = build_dashboard_stats end diff --git a/app/controllers/admin/opening_hours_controller.rb b/app/controllers/admin/opening_hours_controller.rb index 3e46067d..fab63d34 100644 --- a/app/controllers/admin/opening_hours_controller.rb +++ b/app/controllers/admin/opening_hours_controller.rb @@ -16,39 +16,41 @@ def edit end def update - # Reconstruire les horaires à partir des sélecteurs individuels - updated_hours = {} - days = %w[lundi mardi mercredi jeudi vendredi samedi dimanche] - - days.each do |day| - # Vérifier si le jour est fermé - closed_param = params["closed_#{day}"] - if closed_param == "1" - updated_hours[day] = "Fermé" - else - # Reconstruire les horaires à partir des sélecteurs - open_hour = params["open_hour_#{day}"].to_i - open_minute = params["open_minute_#{day}"].to_i - close_hour = params["close_hour_#{day}"].to_i - close_minute = params["close_minute_#{day}"].to_i - - updated_hours[day] = "#{open_hour.to_s.rjust(2, '0')}:#{open_minute.to_s.rjust(2, '0')} - #{close_hour.to_s.rjust(2, '0')}:#{close_minute.to_s.rjust(2, '0')}" - end - end - - # Persist via cache for now (can be moved to a Setting model later) - Rails.cache.write("opening_hours", updated_hours) + updated_hours = build_schedule_params + OpeningHour.replace_schedule!(schedule_hash: updated_hours, updated_by_user: current_user) redirect_to admin_opening_hours_path, notice: t(".success") + rescue ActiveRecord::RecordInvalid, ArgumentError => e + @opening_hours = updated_hours || current_opening_hours + @error_message = e.record&.errors&.full_messages&.to_sentence || e.message + render :edit, status: :unprocessable_content end private def set_opening_hours - @opening_hours = Rails.cache.fetch("opening_hours") || default_opening_hours + @opening_hours = current_opening_hours end def set_breadcrumbs # No need to add dashboard breadcrumb as it's already in the partial end + + def build_schedule_params + OpeningHour::DAYS.keys.each_with_object({}) do |day, updated_hours| + day_name = day.to_s + + updated_hours[day_name] = + if params["closed_#{day_name}"] == "1" + "Fermé" + else + open_hour = params["open_hour_#{day_name}"].to_i + open_minute = params["open_minute_#{day_name}"].to_i + close_hour = params["close_hour_#{day_name}"].to_i + close_minute = params["close_minute_#{day_name}"].to_i + + "#{open_hour.to_s.rjust(2, '0')}:#{open_minute.to_s.rjust(2, '0')} - #{close_hour.to_s.rjust(2, '0')}:#{close_minute.to_s.rjust(2, '0')}" + end + end + end end end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index fd57c5aa..0205a11a 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -7,7 +7,7 @@ class HomeController < ApplicationController def index @upcoming_events = Event.upcoming.by_date.limit(1) - @opening_hours = Rails.cache.fetch("opening_hours") || default_opening_hours + @opening_hours = current_opening_hours end def dashboard; end diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 4b2c0be4..d12447c5 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -23,7 +23,7 @@ def show return redirect_to page_path(target[:page], anchor: target[:anchor]), status: :moved_permanently end - @opening_hours = Rails.cache.fetch("opening_hours") || default_opening_hours + @opening_hours = current_opening_hours @notepad = Rails.cache.fetch("notepad") || default_notepad @blogs = Blog.order(created_at: :desc).limit(3) diff --git a/app/helpers/opening_hours_helper.rb b/app/helpers/opening_hours_helper.rb index 41a5814a..feb2d97c 100644 --- a/app/helpers/opening_hours_helper.rb +++ b/app/helpers/opening_hours_helper.rb @@ -6,14 +6,12 @@ module OpeningHoursHelper # Méthode de module accessible directement def self.default_opening_hours - { - lundi: "Fermé", - mardi: "14:00 - 22:00", - mercredi: "14:00 - 22:00", - jeudi: "14:00 - 22:00", - vendredi: "14:00 - 22:00", - samedi: "14:00 - 22:00", - dimanche: "14:00 - 22:00" - }.freeze + OpeningHour::DEFAULT_SCHEDULE.dup + end + + def current_opening_hours + OpeningHour.schedule_hash + rescue ActiveModel::MissingAttributeError, NoMethodError + default_opening_hours end end diff --git a/app/models/opening_hour.rb b/app/models/opening_hour.rb new file mode 100644 index 00000000..d74d781c --- /dev/null +++ b/app/models/opening_hour.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +class OpeningHour < ApplicationRecord + DAYS = { + lundi: 0, + mardi: 1, + mercredi: 2, + jeudi: 3, + vendredi: 4, + samedi: 5, + dimanche: 6 + }.freeze + + DEFAULT_SCHEDULE = { + "lundi" => "Fermé", + "mardi" => "14:00 - 22:00", + "mercredi" => "14:00 - 22:00", + "jeudi" => "14:00 - 22:00", + "vendredi" => "14:00 - 22:00", + "samedi" => "14:00 - 22:00", + "dimanche" => "14:00 - 22:00" + }.freeze + + belongs_to :updated_by_user, class_name: "User", optional: true + + enum :day, DAYS + + validates :day, presence: true, uniqueness: true + validates :open_at, presence: true, unless: :closed? + validates :close_at, presence: true, unless: :closed? + validate :close_after_open, unless: :closed? + + scope :ordered, -> { order(:day) } + + def self.schedule_hash + schedule = DEFAULT_SCHEDULE.dup + + ordered.each do |record| + schedule[record.day.to_s] = record.formatted_range + end + + schedule + end + + def self.latest_update_entry + ordered.where.not(updated_at: nil).order(updated_at: :desc, id: :desc).first + end + + def self.replace_schedule!(schedule_hash:, updated_by_user:) + transaction do + DAYS.each_key do |day_name| + raw_value = schedule_hash.fetch(day_name.to_s) { schedule_hash.fetch(day_name.to_sym, "Fermé") } + record = find_or_initialize_by(day: day_name) + record.updated_by_user = updated_by_user + + if raw_value.to_s.strip.casecmp("fermé").zero? + record.closed = true + record.open_at = nil + record.close_at = nil + else + open_at, close_at = parse_range!(raw_value) + record.closed = false + record.open_at = open_at + record.close_at = close_at + end + + record.save! + end + end + end + + def self.parse_range!(value) + match = value.to_s.strip.match(/\A(\d{2}:\d{2}) - (\d{2}:\d{2})\z/) + raise ArgumentError, "invalid time range" if match.nil? + + [ parse_time!(match[1]), parse_time!(match[2]) ] + end + + def self.parse_time!(value) + Time.zone.parse(value) || raise(ArgumentError, "invalid time") + end + + def formatted_range + return "Fermé" if closed? + + "#{open_at.strftime("%H:%M")} - #{close_at.strftime("%H:%M")}" + end + + private + + def close_after_open + return if open_at.blank? || close_at.blank? + return if close_at > open_at + + errors.add(:close_at, "doit être après l'heure d'ouverture") + end +end diff --git a/app/services/opening_hours_management/opening_hours_updater.rb b/app/services/opening_hours_management/opening_hours_updater.rb index add90fc5..00a4e12f 100644 --- a/app/services/opening_hours_management/opening_hours_updater.rb +++ b/app/services/opening_hours_management/opening_hours_updater.rb @@ -20,9 +20,7 @@ def call # Valider les horaires return failure("Erreur : l'heure de fermeture doit être après l'heure d'ouverture pour tous les jours ouverts.") unless valid_hours?(opening_hours) - # Sauvegarder dans le cache - Rails.cache.write("opening_hours", opening_hours) - Rails.cache.write("opening_hours_updated_at", Time.current) + OpeningHour.replace_schedule!(schedule_hash: opening_hours, updated_by_user: updated_by) # Instrumentation pour audit ActiveSupport::Notifications.instrument( diff --git a/app/views/pages/become_member.html.erb b/app/views/pages/become_member.html.erb index c0144507..04a58755 100644 --- a/app/views/pages/become_member.html.erb +++ b/app/views/pages/become_member.html.erb @@ -176,7 +176,7 @@ <%# Panneau latéral : Horaires + Accès dans une seule carte %> <% schedule = @opening_hours - updated_at = Rails.cache.fetch("opening_hours_updated_at") + updated_at = OpeningHour.latest_update_entry&.updated_at %>
diff --git a/db/migrate/20260506101500_persist_opening_hours_schedule.rb b/db/migrate/20260506101500_persist_opening_hours_schedule.rb new file mode 100644 index 00000000..fd9fa7eb --- /dev/null +++ b/db/migrate/20260506101500_persist_opening_hours_schedule.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +class PersistOpeningHoursSchedule < ActiveRecord::Migration[8.1] + DAY_MAP = { + "lundi" => 0, + "mardi" => 1, + "mercredi" => 2, + "jeudi" => 3, + "vendredi" => 4, + "samedi" => 5, + "dimanche" => 6 + }.freeze + + DEFAULT_SCHEDULE = { + "lundi" => "Fermé", + "mardi" => "14:00 - 22:00", + "mercredi" => "14:00 - 22:00", + "jeudi" => "14:00 - 22:00", + "vendredi" => "14:00 - 22:00", + "samedi" => "14:00 - 22:00", + "dimanche" => "14:00 - 22:00" + }.freeze + + def up + add_column :opening_hours, :closed, :boolean, default: false, null: false + add_reference :opening_hours, :updated_by_user, foreign_key: { to_table: :users } + change_column_null :opening_hours, :open_at, true + change_column_null :opening_hours, :close_at, true + + say_with_time "Deduplicating opening hours by day" do + OpeningHourRecord.order(:day, updated_at: :desc, id: :desc).group_by(&:day).each_value do |rows| + rows.drop(1).each(&:destroy!) + end + end + + say_with_time "Ensuring one opening hour row per weekday" do + DEFAULT_SCHEDULE.each do |day_name, value| + day_value = DAY_MAP.fetch(day_name) + record = OpeningHourRecord.find_or_initialize_by(day: day_value) + + if value == "Fermé" + record.closed = true + record.open_at = nil + record.close_at = nil + else + open_at, close_at = value.split(" - ") + record.closed = false + record.open_at = open_at + record.close_at = close_at + end + + record.save! + end + end + + add_index :opening_hours, :day, unique: true + end + + def down + remove_index :opening_hours, :day + change_column_null :opening_hours, :open_at, false + change_column_null :opening_hours, :close_at, false + remove_reference :opening_hours, :updated_by_user, foreign_key: { to_table: :users } + remove_column :opening_hours, :closed + end + + class OpeningHourRecord < ApplicationRecord + self.table_name = "opening_hours" + end +end diff --git a/db/schema.rb b/db/schema.rb index 716f727e..89448a5a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -250,11 +250,15 @@ end create_table "opening_hours", force: :cascade do |t| - t.time "close_at", null: false + t.time "close_at" + t.boolean "closed", default: false, null: false t.datetime "created_at", null: false t.integer "day", null: false - t.time "open_at", null: false + t.time "open_at" t.datetime "updated_at", null: false + t.integer "updated_by_user_id" + t.index ["day"], name: "index_opening_hours_on_day", unique: true + t.index ["updated_by_user_id"], name: "index_opening_hours_on_updated_by_user_id" end create_table "payment_audit_logs", force: :cascade do |t| @@ -406,6 +410,7 @@ add_foreign_key "memberships", "membership_types" add_foreign_key "memberships", "people" add_foreign_key "newsletter_subscribers", "people", on_delete: :nullify + add_foreign_key "opening_hours", "users", column: "updated_by_user_id" add_foreign_key "payment_audit_logs", "payments" add_foreign_key "payment_audit_logs", "users" add_foreign_key "payment_lines", "payments" diff --git a/spec/factories/opening_hours.rb b/spec/factories/opening_hours.rb new file mode 100644 index 00000000..eb0733c1 --- /dev/null +++ b/spec/factories/opening_hours.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :opening_hour do + day { :mardi } + open_at { "14:00" } + close_at { "22:00" } + closed { false } + association :updated_by_user, factory: :user + + trait :closed do + closed { true } + open_at { nil } + close_at { nil } + end + end +end diff --git a/spec/models/opening_hour_spec.rb b/spec/models/opening_hour_spec.rb new file mode 100644 index 00000000..6820cabc --- /dev/null +++ b/spec/models/opening_hour_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe OpeningHour, type: :model do + describe ".schedule_hash" do + it "returns all weekdays and marks missing rows as closed" do + create(:opening_hour, day: :mardi, open_at: "14:00", close_at: "22:00") + + schedule = described_class.schedule_hash + + expect(schedule["lundi"]).to eq("Fermé") + expect(schedule["mardi"]).to eq("14:00 - 22:00") + expect(schedule.keys).to eq(%w[lundi mardi mercredi jeudi vendredi samedi dimanche]) + end + end + + describe ".replace_schedule!" do + let(:admin) { create(:user, :admin) } + + it "persists one row per weekday and tracks the updater" do + described_class.replace_schedule!( + schedule_hash: { + "lundi" => "Fermé", + "mardi" => "14:00 - 22:00", + "mercredi" => "14:00 - 22:00", + "jeudi" => "14:00 - 22:00", + "vendredi" => "14:00 - 22:00", + "samedi" => "10:00 - 18:00", + "dimanche" => "Fermé" + }, + updated_by_user: admin + ) + + expect(described_class.count).to eq(7) + expect(described_class.find_by(day: :lundi)).to be_closed + expect(described_class.find_by(day: :samedi).formatted_range).to eq("10:00 - 18:00") + expect(described_class.latest_update_entry.updated_by_user).to eq(admin) + end + end +end diff --git a/spec/requests/admin/dashboard_spec.rb b/spec/requests/admin/dashboard_spec.rb index 3efcc3b9..36317b11 100644 --- a/spec/requests/admin/dashboard_spec.rb +++ b/spec/requests/admin/dashboard_spec.rb @@ -50,12 +50,12 @@ expect(response).to have_http_status(:success) end - it 'loads cached opening_hours' do - hours = { 'monday' => '09:00 - 17:00' } - Rails.cache.write('opening_hours', hours) + it 'loads opening_hours from the database' do + create(:opening_hour, day: :mardi, open_at: '09:00', close_at: '17:00', updated_by_user: admin) get admin_dashboard_index_path expect(response).to have_http_status(:success) + expect(response.body).to include('09:00 - 17:00') end end end diff --git a/spec/requests/admin/opening_hours_spec.rb b/spec/requests/admin/opening_hours_spec.rb new file mode 100644 index 00000000..037ded64 --- /dev/null +++ b/spec/requests/admin/opening_hours_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Admin::OpeningHours", type: :request do + let(:admin) { create(:user, :admin) } + + before { login_as(admin) } + + describe "GET /admin/opening_hours" do + it "renders hours from the database" do + create(:opening_hour, day: :mardi, open_at: "14:00", close_at: "22:00", updated_by_user: admin) + + get admin_opening_hours_path + + expect(response).to have_http_status(:success) + expect(response.body).to include("Horaires d'ouverture") + expect(response.body).to include("14:00 - 22:00") + end + end + + describe "PATCH /admin/opening_hours" do + it "persists the weekly schedule and records the updater" do + patch admin_opening_hours_path, params: { + open_hour_lundi: "9", open_minute_lundi: "0", close_hour_lundi: "17", close_minute_lundi: "0", + open_hour_mardi: "14", open_minute_mardi: "0", close_hour_mardi: "22", close_minute_mardi: "0", + open_hour_mercredi: "14", open_minute_mercredi: "0", close_hour_mercredi: "22", close_minute_mercredi: "0", + open_hour_jeudi: "14", open_minute_jeudi: "0", close_hour_jeudi: "22", close_minute_jeudi: "0", + open_hour_vendredi: "14", open_minute_vendredi: "0", close_hour_vendredi: "22", close_minute_vendredi: "0", + open_hour_samedi: "14", open_minute_samedi: "0", close_hour_samedi: "22", close_minute_samedi: "0", + open_hour_dimanche: "14", open_minute_dimanche: "0", close_hour_dimanche: "22", close_minute_dimanche: "0" + } + + expect(response).to redirect_to(admin_opening_hours_path) + expect(OpeningHour.find_by(day: :lundi).formatted_range).to eq("09:00 - 17:00") + expect(OpeningHour.latest_update_entry.updated_by_user).to eq(admin) + end + + it "supports closed days" do + patch admin_opening_hours_path, params: { + closed_lundi: "1", + open_hour_mardi: "14", open_minute_mardi: "0", close_hour_mardi: "22", close_minute_mardi: "0", + open_hour_mercredi: "14", open_minute_mercredi: "0", close_hour_mercredi: "22", close_minute_mercredi: "0", + open_hour_jeudi: "14", open_minute_jeudi: "0", close_hour_jeudi: "22", close_minute_jeudi: "0", + open_hour_vendredi: "14", open_minute_vendredi: "0", close_hour_vendredi: "22", close_minute_vendredi: "0", + open_hour_samedi: "14", open_minute_samedi: "0", close_hour_samedi: "22", close_minute_samedi: "0", + open_hour_dimanche: "14", open_minute_dimanche: "0", close_hour_dimanche: "22", close_minute_dimanche: "0" + } + + expect(response).to redirect_to(admin_opening_hours_path) + expect(OpeningHour.find_by(day: :lundi)).to be_closed + end + end +end