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
3 changes: 0 additions & 3 deletions app/controllers/admin/events_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
module Admin
class EventsController < BaseController
TZ_STRING = 'Eastern Time (US & Canada)'
before_action :set_event, only: %i[show edit update destroy]
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found

Expand All @@ -21,7 +20,6 @@ def show; end
# POST /events or /events.json
def create
@event = Event.new(event_params)
@event.start_at = @event.start_at.change(zone: TZ_STRING)

respond_to do |format|
if @event.save
Expand All @@ -37,7 +35,6 @@ def create
# PATCH/PUT /events/1 or /events/1.json
def update
@event.assign_attributes(event_params)
@event.start_at = @event.start_at.change(zone: TZ_STRING)

respond_to do |format|
if @event.save
Expand Down
2 changes: 1 addition & 1 deletion app/models/calendar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def publish
@events.each do |event|
calendar.event do |e|
e.dtstart = ical_time(event.start_at)
e.dtend = ical_time(event.start_at + 3.hours)
e.dtend = ical_time(event.end_at)
e.summary = "Toronto Ruby - #{event.name}"
e.location = event.city
e.url = event.rsvp_link || event_url(event)
Expand Down
37 changes: 34 additions & 3 deletions app/models/event.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
class Event < ApplicationRecord
# Meetups run about three hours; we don't track an explicit end time.
DURATION = 3.hours

validates :start_at, :name, :location, :description, presence: true

enum :status, { draft: 0, published: 1 }, default: :draft

scope :upcoming, -> { published.where(start_at: Time.zone.now...).order(start_at: :asc) }
scope :past, -> { published.where(start_at: ...Time.zone.now).order(start_at: :desc) }
# An event counts as upcoming until it's over, not until it starts.
scope :upcoming, -> { published.where(start_at: (Time.zone.now - DURATION)...).order(start_at: :asc) }
scope :past, -> { published.where(start_at: ...(Time.zone.now - DURATION)).order(start_at: :desc) }

has_rich_text :description
has_rich_text :location

def start_time
start_at.in_time_zone('Eastern Time (US & Canada)').to_fs(:long_at)
start_at.to_fs(:long_at)
end

def end_at
start_at + DURATION
end

def over?
Time.zone.now >= end_at
end

def map_url
return if map_query.blank?

"https://www.google.com/maps/search/?#{{ api: 1, query: map_query }.to_query}"
end

def self.statuses_for_select
Expand All @@ -20,4 +38,17 @@ def self.statuses_for_select
def to_param
slug
end

private

# Locations are written as a venue name, then a street address, then optional
# arrival instructions. Only the first two lines help a map search, so the
# rest is dropped and the city is appended when it isn't already there.
def map_query
lines = location&.to_plain_text.to_s.split("\n").map(&:strip).reject(&:blank?).first(2)
return '' if lines.blank?

lines << city if city.present? && lines.none? { |line| line.include?(city.split(',').first.strip) }
lines.join(', ')
end
end
2 changes: 1 addition & 1 deletion app/views/admin/events/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
<%= form.label :start_at, class: "block text-sm font-medium text-gray-700 mb-1" %>
<%= form.datetime_field :start_at,
include_seconds: false,
value: event&.start_at&.in_time_zone("Eastern Time (US & Canada)").presence || Time.zone.now.in_time_zone("Eastern Time (US & Canada)"),
value: event&.start_at || Time.zone.now,
class: "block w-full px-4 py-3 rounded-lg border border-gray-300 shadow-sm focus:border-ruby-500 focus:ring-1 focus:ring-ruby-500 transition-colors" %>
<p class="mt-1 text-sm text-gray-500">Eastern Time (US & Canada)</p>
</div>
Expand Down
9 changes: 8 additions & 1 deletion app/views/events/_event.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div id="<%= dom_id event %>" class="card group">
<% future = Time.zone.now < event.start_at %>
<% future = !event.over? %>

<div class="p-8">
<!-- Event Header -->
Expand Down Expand Up @@ -94,6 +94,13 @@
<div class="prose prose-sm text-gray-600">
<%= event.location %>
</div>
<% if event.map_url %>
<%= link_to "Open in Google Maps",
event.map_url,
class: "external-link mt-2 text-sm",
target: "_blank",
rel: "noopener" %>
<% end %>
</div>
<!-- Agenda -->
<div>
Expand Down
3 changes: 2 additions & 1 deletion config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ class Application < Rails::Application
# These settings can be overridden in specific environments using the files
# in config/environments, which are processed later.
#
# config.time_zone = "Central Time (US & Canada)"
# Every meetup is in Toronto, so render times there. Storage stays UTC.
config.time_zone = 'Eastern Time (US & Canada)'
# config.eager_load_paths << Rails.root.join("extras")

# Don't generate system test files.
Expand Down
21 changes: 21 additions & 0 deletions test/controllers/admin/events_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,27 @@ def setup
assert response.body.include?(Event.last.name)
end

test 'index shows event times in Toronto time, not UTC' do
Event.destroy_all
# 00:30 UTC is the previous evening in Toronto.
Event.create!(
start_at: Time.utc(2024, 11, 26, 0, 30),
name: 'Late Night Edition',
location: 'Some Office',
description: 'A talk',
status: :published,
sponsor: 'Some Sponsor',
sponsor_link: 'https://example.com'
)

get admin_events_path

assert_response :success
assert_match 'November 25, 2024', response.body
assert_match '7:30 PM', response.body
assert_no_match(/November 26, 2024/, response.body)
end

test 'should show a single event' do
get admin_event_path(Event.first.slug)

Expand Down
54 changes: 54 additions & 0 deletions test/controllers/events_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,58 @@ def setup
assert_response :found
assert_redirected_to all_events_path
end

test 'links the location to Google Maps' do
event = @events.first
event.update!(location: "Workplace One\n51 Wolseley St, Toronto ON")

get :show, params: { slug: event.slug }

assert_response :success
assert_match 'https://www.google.com/maps/search/?api=1&amp;query=Workplace+One%2C+51+Wolseley+St%2C+Toronto+ON',
response.body
assert_match 'Open in Google Maps', response.body
end

test 'an event still in progress stays on the home page' do
create_only_event('In Progress Event', Time.zone.now - 1.hour)

get :index
assert_response :success
assert_match 'In Progress Event', response.body
assert_match 'Upcoming', response.body

get :past
assert_response :success
assert_match 'No past events', response.body
end

test 'an event that has ended moves to past events' do
create_only_event('Finished Event', Time.zone.now - (Event::DURATION + 1.minute))

get :index
assert_response :success
assert_match "We're planning our next outing", response.body

get :past
assert_response :success
assert_match 'Finished Event', response.body
assert_match 'Past Event', response.body
end

private

def create_only_event(name, start_at)
Event.destroy_all
Event.create!(
start_at: start_at,
name: name,
location: 'Some Office',
description: 'A talk',
status: :published,
rsvp_link: 'https://example.com/rsvp',
sponsor: 'Some Sponsor',
sponsor_link: 'https://example.com'
)
end
end
64 changes: 64 additions & 0 deletions test/models/event_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
require 'test_helper'

class EventTest < ActiveSupport::TestCase
def build_event(attributes = {})
Event.new({
name: 'Witty Event Name',
location: "Workplace One\n51 Wolseley St, Toronto ON\nLower level, enter through doors on Wolseley St.",
description: 'A talk',
rsvp_link: 'https://test.com',
status: :published,
start_at: Time.zone.parse('2024-11-26T00:30Z')
}.merge(attributes))
end

test 'end_at is three hours after start_at' do
event = build_event(start_at: Time.zone.parse('2024-11-25T19:30-05:00'))

assert_equal Time.zone.parse('2024-11-25T22:30-05:00'), event.end_at
end

test 'over? flips at the end time, not the start time' do
event = build_event(start_at: Time.zone.now - 2.hours)
assert_not event.over?

event.start_at = Time.zone.now - 4.hours
assert event.over?
end

test 'map_url builds a Google Maps search from the venue and street address' do
event = build_event

assert_equal 'https://www.google.com/maps/search/?api=1&query=Workplace+One%2C+51+Wolseley+St%2C+Toronto+ON',
event.map_url
end

test 'map_url appends the city when the address does not name it' do
event = build_event(location: "FinanceIt @ The Well\n8 Spadina Ave\nSuite 2400", city: 'Toronto, Canada')

assert_equal 'https://www.google.com/maps/search/?api=1&query=FinanceIt+%40+The+Well%2C+8+Spadina+Ave%2C+Toronto%2C+Canada',
event.map_url
end

test 'map_url is nil without a location' do
assert_nil build_event(location: '').map_url
end

test 'an in-progress event is upcoming, not past' do
Event.destroy_all
event = build_event(start_at: Time.zone.now - 1.hour)
event.save!

assert_includes Event.upcoming, event
assert_not_includes Event.past, event
end

test 'a finished event is past, not upcoming' do
Event.destroy_all
event = build_event(start_at: Time.zone.now - (Event::DURATION + 1.minute))
event.save!

assert_includes Event.past, event
assert_not_includes Event.upcoming, event
end
end
Loading