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
49 changes: 49 additions & 0 deletions app/models/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class Event < ApplicationRecord

# Callbacks
after_commit :build_public_registration_form, if: :public_registration_just_enabled?
before_validation :merge_date_time_fields

# Validations
validates_presence_of :title, :start_date, :end_date
Expand Down Expand Up @@ -155,6 +156,35 @@ def event_details_label
super.presence || "Before you attend"
end

# Virtual attributes for date/time inputs (Firefox datetime-local compat)
attr_writer :start_date_date, :start_date_time,
:end_date_date, :end_date_time,
:registration_close_date_date, :registration_close_date_time

def start_date_date
@start_date_date || start_date&.strftime("%Y-%m-%d")
end

def start_date_time
@start_date_time || start_date&.strftime("%H:%M")
end

def end_date_date
@end_date_date || end_date&.strftime("%Y-%m-%d")
end

def end_date_time
@end_date_time || end_date&.strftime("%H:%M")
end

def registration_close_date_date
@registration_close_date_date || registration_close_date&.strftime("%Y-%m-%d")
end

def registration_close_date_time
@registration_close_date_time || registration_close_date&.strftime("%H:%M")
end

# Virtual attribute for cost in dollars (converts to/from cost_cents)
def cost
return nil if cost_cents.nil?
Expand Down Expand Up @@ -188,6 +218,25 @@ def to_partial_path

private

def merge_date_time_fields
merge_date_time(:start_date)
merge_date_time(:end_date)
merge_date_time(:registration_close_date)
end

def merge_date_time(field)
date_val = send(:"#{field}_date")
time_val = send(:"#{field}_time")
self[field] = build_datetime(date_val, time_val)
end

def build_datetime(date_str, time_str)
return nil if date_str.blank? && time_str.blank?
return Time.zone.parse(date_str) if date_str.present? && time_str.blank?
return Time.zone.parse("2000-01-01 #{time_str}") if date_str.blank? && time_str.present?
Time.zone.parse("#{date_str} #{time_str}")
end

def registration_form_required_when_publicly_registerable
return unless public_registration_enabled?
return if will_save_change_to_public_registration_enabled?
Expand Down
5 changes: 3 additions & 2 deletions app/policies/event_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,9 @@ def google_analytics?
:pre_title,
:pre_date_text,
:featured,
:start_date, :end_date,
:registration_close_date,
:start_date, :start_date_date, :start_date_time,
:end_date, :end_date_date, :end_date_time,
:registration_close_date, :registration_close_date_date, :registration_close_date_time,
:published,
:publicly_visible,
:publicly_featured,
Expand Down
88 changes: 51 additions & 37 deletions app/views/events/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -68,53 +68,67 @@
<%# LEFT: Date/time, cost + pre-date text, videoconference + location %>
<div class="md:col-span-2 space-y-6">
<div class="grid grid-cols-[repeat(auto-fit,minmax(min(100%,13rem),1fr))] gap-3">
<div>
<%= f.label :start_date,
"Start time",
required: true,
class: "block font-medium mb-1" %>

<%= f.text_field :start_date,
type: "datetime-local",
class:
"w-full rounded border-gray-300 shadow-sm px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
value: @event.start_date&.strftime("%Y-%m-%dT%H:%M"),
required: true %>

<div class="bg-white border border-gray-200 rounded-lg p-3">
<h3 class="font-medium mb-2">Start</h3>
<div class="mb-2">
<label class="block text-xs text-gray-600 mb-0.5">Date</label>
<%= f.text_field :start_date_date,
type: "date",
class: "w-full rounded border-gray-300 shadow-sm px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
required: true %>
</div>
<div>
<label class="block text-xs text-gray-600 mb-0.5">Time</label>
<%= f.text_field :start_date_time,
type: "time",
class: "w-full rounded border-gray-300 shadow-sm px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
required: true %>
</div>
<% if f.object.errors[:start_date].any? %>
<p class="text-red-500 text-sm mt-1">Start date
<%= f.object.errors[:start_date].join(", ") %></p>
<% end %>
</div>

<div>
<%= f.label :end_date, "End time", required: true, class: "block font-medium mb-1" %>

<%= f.text_field :end_date,
type: "datetime-local",
class:
"w-full rounded border-gray-300 shadow-sm px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
value: @event.end_date&.strftime("%Y-%m-%dT%H:%M"),
required: true %>

<div class="bg-white border border-gray-200 rounded-lg p-3">
<h3 class="font-medium mb-2">End</h3>
<div class="mb-2">
<label class="block text-xs text-gray-600 mb-0.5">Date</label>
<%= f.text_field :end_date_date,
type: "date",
class: "w-full rounded border-gray-300 shadow-sm px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
required: true %>
</div>
<div>
<label class="block text-xs text-gray-600 mb-0.5">Time</label>
<%= f.text_field :end_date_time,
type: "time",
class: "w-full rounded border-gray-300 shadow-sm px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
required: true %>
</div>
<% if f.object.errors[:end_date].any? %>
<p class="text-red-500 text-sm mt-1">End date
<%= f.object.errors[:end_date].join(", ") %></p>
<% end %>
</div>

<div>
<%= f.label :registration_close_date,
"Registration close time",
class: "block font-medium mb-1" %>

<% registration_close_default = @event.registration_close_date || event_registration_close_default(@event) %>
<%= f.text_field :registration_close_date,
type: "datetime-local",
class:
"w-full rounded border-gray-300 shadow-sm px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
value: registration_close_default.strftime("%Y-%m-%dT%H:%M") %>

<div class="bg-white border border-gray-200 rounded-lg p-3">
<h3 class="font-medium mb-2">Registration closed</h3>
<% rc_default = @event.registration_close_date || event_registration_close_default(@event) %>
<div class="mb-2">
<label class="block text-xs text-gray-600 mb-0.5">Date</label>
<%= f.text_field :registration_close_date_date,
type: "date",
class: "w-full rounded border-gray-300 shadow-sm px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
value: rc_default.strftime("%Y-%m-%d") %>
</div>
<div>
<label class="block text-xs text-gray-600 mb-0.5">Time</label>
<%= f.text_field :registration_close_date_time,
type: "time",
class: "w-full rounded border-gray-300 shadow-sm px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
value: rc_default.strftime("%H:%M") %>
</div>
<% if f.object.errors[:registration_close_date].any? %>
<p class="text-red-500 text-sm mt-1">
Registration close date
Expand Down Expand Up @@ -485,7 +499,7 @@
<label
for="<%= id %>"
class="
flex items-center gap-2 p-3 cursor-pointer w-auto min-w-[180px] bg-white border border-gray-200
flex items-center gap-2 p-3 cursor-pointer w-auto min-w-[180px] bg-blue-50 border border-gray-200
rounded-lg shadow-sm hover:bg-gray-100 transition">
<%= hidden_field_tag "event[sector_ids][]", "" %>
<!-- # To ensure unchecked boxes are submitted -->
Expand Down Expand Up @@ -515,7 +529,7 @@
<label
for="<%= id %>"
class="
flex items-center gap-2 p-3 cursor-pointer w-auto min-w-[180px] bg-white border
flex items-center gap-2 p-3 cursor-pointer w-auto min-w-[180px] bg-blue-50 border
border-gray-200 rounded-lg shadow-sm hover:bg-gray-100 transition">
<%= hidden_field_tag "event[category_ids][]", "" %>
<!-- # To ensure unchecked boxes are submitted -->
Expand Down
26 changes: 17 additions & 9 deletions spec/requests/events_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -176,13 +176,15 @@
it "stores start_date/end_date in UTC when created by user in Pacific time zone" do
admin_pt = create(:user, :admin, time_zone: "Pacific Time (US & Canada)")
sign_in admin_pt
# datetime-local sends "YYYY-MM-DDTHH:MM" — interpreted in request's Time.zone (PT)
# date+time inputs send separate values — interpreted in request's Time.zone (PT)
# 12:00–13:00 PT (PDT) on 2025-06-15 = 19:00–20:00 UTC
post events_url, params: { event: {
title: "PT event",
description: "desc",
start_date: "2025-06-15T12:00",
end_date: "2025-06-15T13:00",
start_date_date: "2025-06-15",
start_date_time: "12:00",
end_date_date: "2025-06-15",
end_date_time: "13:00",
registration_close_date: 1.day.ago,
public: true
} }
Expand All @@ -200,8 +202,10 @@
post events_url, params: { event: {
title: "ET event",
description: "desc",
start_date: "2025-06-15T15:00",
end_date: "2025-06-15T16:00",
start_date_date: "2025-06-15",
start_date_time: "15:00",
end_date_date: "2025-06-15",
end_date_time: "16:00",
registration_close_date: 1.day.ago,
public: true
} }
Expand Down Expand Up @@ -238,8 +242,10 @@

post events_path, params: { event: {
title: "Missing dates",
start_date: "",
end_date: "",
start_date_date: "",
start_date_time: "",
end_date_date: "",
end_date_time: "",
registration_form_id: reg_form.id,
scholarship_enabled: "1",
bulk_payment_enabled: "1"
Expand All @@ -258,8 +264,10 @@

post events_path, params: { event: {
title: "Missing dates",
start_date: "",
end_date: "",
start_date_date: "",
start_date_time: "",
end_date_date: "",
end_date_time: "",
sector_ids: [ "", sector.id.to_s ],
category_ids: [ "", category.id.to_s ]
} }
Expand Down
15 changes: 9 additions & 6 deletions spec/views/events/_form.html.erb_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@
expect(rendered).to have_selector("form")
expect(rendered).to have_field("event[title]", with: "Original Title")
expect(rendered).to have_selector("#event_rhino_description[type='hidden']", visible: :all)
expect(rendered).to have_selector("input[type='datetime-local'][name='event[start_date]']")
expect(rendered).to have_selector("input[type='datetime-local'][name='event[end_date]']")
expect(rendered).to have_selector("input[type='datetime-local'][name='event[registration_close_date]']")
expect(rendered).to have_selector("input[type='date'][name='event[start_date_date]']")
expect(rendered).to have_selector("input[type='time'][name='event[start_date_time]']")
expect(rendered).to have_selector("input[type='date'][name='event[end_date_date]']")
expect(rendered).to have_selector("input[type='time'][name='event[end_date_time]']")
expect(rendered).to have_selector("input[type='date'][name='event[registration_close_date_date]']")
expect(rendered).to have_selector("input[type='time'][name='event[registration_close_date_time]']")
expect(rendered).to have_selector("input[type='checkbox'][name='event[published]']")
end

Expand All @@ -31,9 +34,9 @@

expect(rendered).to have_selector("label", text: "Event title")
expect(rendered).to have_selector("label", text: "Event Cost")
expect(rendered).to have_selector("label", text: "Start time")
expect(rendered).to have_selector("label", text: "End time")
expect(rendered).to have_selector("label", text: "Registration close time")
expect(rendered).to have_selector("h3", text: "Start")
expect(rendered).to have_selector("h3", text: "End")
expect(rendered).to have_selector("h3", text: "Registration closed")
expect(rendered).to have_selector("label", text: "Published")
end

Expand Down