Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-docker-cache.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/deploy-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/admin/dashboard_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
48 changes: 25 additions & 23 deletions app/controllers/admin/opening_hours_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle ArgumentError without calling record

When OpeningHour.replace_schedule! raises an ArgumentError here, for example from a crafted time parameter like open_hour_lundi=100 that produces an invalid range, this rescue path raises a new NoMethodError because ArgumentError does not respond to record. That turns the intended validation response into a 500 instead of rendering the edit form with e.message.

Useful? React with 👍 / 👎.

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
2 changes: 1 addition & 1 deletion app/controllers/home_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/pages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
16 changes: 7 additions & 9 deletions app/helpers/opening_hours_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
97 changes: 97 additions & 0 deletions app/models/opening_hour.rb
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion app/views/pages/become_member.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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
%>
<div class="rounded-2xl border border-gray-100 bg-white shadow-sm overflow-hidden">

Expand Down
70 changes: 70 additions & 0 deletions db/migrate/20260506101500_persist_opening_hours_schedule.rb
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve existing opening-hour rows during migration

When this migration runs on a database that already has opening_hours rows, find_or_initialize_by returns the existing row and the following default-assignment block overwrites its stored open_at/close_at values with DEFAULT_SCHEDULE. The preceding deduplication step keeps the latest row per day, so this migration appears intended to retain existing data and only fill missing weekdays; as written it discards any non-default schedule during deploy.

Useful? React with 👍 / 👎.


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
Comment on lines +61 to +62
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make the rollback tolerate seeded closed days

Rolling back immediately after this migration will fail because up seeds Monday as closed with open_at/close_at set to NULL, but down changes those columns back to null: false before replacing or deleting the null values. Any environment that needs to rollback after deploying this migration will hit a NOT NULL violation here.

Useful? React with 👍 / 👎.

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
9 changes: 7 additions & 2 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions spec/factories/opening_hours.rb
Original file line number Diff line number Diff line change
@@ -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
Loading