From a7a6ad786eeb9f94f7882462bccbdd31e1bb4743 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Tue, 30 Jun 2026 19:15:22 +0200 Subject: Phase 1: standalone events, external_url on nodes - Migration: node_id nullable on events and occurrences, add title/description/is_primary to events, external_url to nodes - Existing events marked is_primary: true (were all 1:1 with nodes) - Node: has_one :event -> has_many :events - Event: belongs_to :node optional, validates title presence for standalone events, is_primary uniqueness scoped to node_id, display_title helper falling back through node title - Occurrence: belongs_to :node optional, summary falls back to event.display_title - nodes_helper: event_information uses events.first (interim; will be replaced in Phase 3 event UI) - Tests: fix node.event -> node.events.first in event_test --- app/models/occurrence.rb | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'app/models/occurrence.rb') diff --git a/app/models/occurrence.rb b/app/models/occurrence.rb index 3baf447..143124f 100644 --- a/app/models/occurrence.rb +++ b/app/models/occurrence.rb @@ -1,13 +1,12 @@ -# TODO Make a gem out of the c wrapper require 'chaos_calendar' class Occurrence < ApplicationRecord # Associations - belongs_to :node + belongs_to :node, optional: true belongs_to :event - + # Class Methods def self.find_in_range start_time, end_time @@ -64,7 +63,7 @@ class Occurrence < ApplicationRecord # Instance Methods def summary - node.head.title + node&.head&.title || event.display_title end - + end -- cgit v1.3 From 51629c5c42270a346885057a441095c964101cc1 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Tue, 30 Jun 2026 03:55:42 +0200 Subject: Fix events CRUD for standalone events and add events to admin menu - event_params now permits title, description, is_primary - event_information helper lists all node.events, not just the first - Occurrence.generate handles nil node (standalone events) - Page.aggregate order_by title uses correlated subquery to avoid GROUP BY conflict with tag-filter path; order_direction whitelisted to ASC/DESC to prevent SQL injection - Events link added to admin menu bar - events/index shows title, is_primary; drops latitude/longitude columns --- app/controllers/events_controller.rb | 2 +- app/helpers/nodes_helper.rb | 20 +++++++++----------- app/models/occurrence.rb | 2 +- app/models/page.rb | 8 ++++---- app/views/admin/_menu.html.erb | 1 + app/views/events/index.html.erb | 8 ++++---- 6 files changed, 20 insertions(+), 21 deletions(-) (limited to 'app/models/occurrence.rb') diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index f50da3e..3a60cf9 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -94,6 +94,6 @@ class EventsController < ApplicationController private def event_params - params.require(:event).permit(:start_time, :end_time, :rrule, :custom_rrule, :allday, :url, :latitude, :longitude, :node_id, :location) + params.require(:event).permit(:title, :description, :is_primary, :start_time, :end_time, :rrule, :custom_rrule, :allday, :url, :latitude, :longitude, :node_id, :location) end end diff --git a/app/helpers/nodes_helper.rb b/app/helpers/nodes_helper.rb index d88d12a..4293628 100644 --- a/app/helpers/nodes_helper.rb +++ b/app/helpers/nodes_helper.rb @@ -30,19 +30,17 @@ module NodesHelper end def event_information - if @node.events.first - event = @node.events.first + events = @node.events.order(:start_time) + items = events.map do |event| safe_join([ - "#{event.start_time.to_fs(:db)} - #{event.end_time.to_fs(:db)} > ", - link_to('show', event_path(event)), - ' > ', - link_to('edit', edit_event_path(event)) - ]) - else - safe_join([ - 'no event attached > ', - link_to('add', new_event_path(:node_id => @node.id)) + "#{event.start_time&.to_fs(:db)} - #{event.end_time&.to_fs(:db)} > ", + link_to('edit', edit_event_path(event)), ]) end + safe_join([ + safe_join(items, ' | '), + ' > ', + link_to('add event', new_event_path(:node_id => @node.id)) + ]) end end diff --git a/app/models/occurrence.rb b/app/models/occurrence.rb index 143124f..777be24 100644 --- a/app/models/occurrence.rb +++ b/app/models/occurrence.rb @@ -35,7 +35,7 @@ class Occurrence < ApplicationRecord self.create( :start_time => occurrence, :end_time => (occurrence + duration), - :node_id => node.id, + :node_id => node&.id, :event_id => event.id ) end diff --git a/app/models/page.rb b/app/models/page.rb index 385b3f6..c982c2e 100644 --- a/app/models/page.rb +++ b/app/models/page.rb @@ -63,15 +63,15 @@ class Page < ApplicationRecord end end + direction = %w[ASC DESC].include?(options[:order_direction]&.upcase) ? options[:order_direction].upcase : "ASC" + if options[:order_by] == "title" return scope - .joins(:translations) - .where(page_translations: { locale: I18n.locale }) - .order("page_translations.title #{options[:order_direction]}") + .order(Arel.sql("(SELECT pt.title FROM page_translations pt WHERE pt.page_id = pages.id AND pt.locale = #{ActiveRecord::Base.connection.quote(I18n.locale.to_s)}) #{direction}")) .paginate(:page => page, :per_page => options[:limit]) end - scope.order("#{options[:order_by]} #{options[:order_direction]}") + scope.order("#{options[:order_by]} #{direction}") .paginate(:page => page, :per_page => options[:limit]) end diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index 6dba085..c87c5f7 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -4,5 +4,6 @@ search <%= link_to 'Nodes', nodes_path, selected?('nodes') %> <%= link_to 'Assets', assets_path, selected?('assets') %> +<%= link_to 'Events', events_path, selected?('events') %> <%= link_to 'User', users_path, selected?('users') %> <%= link_to 'Navigation', menu_items_path, selected?('menu_items') %> >  diff --git a/app/views/events/index.html.erb b/app/views/events/index.html.erb index 19b21ce..064fa86 100644 --- a/app/views/events/index.html.erb +++ b/app/views/events/index.html.erb @@ -2,27 +2,27 @@ + + - - <% @events.each do |event| %> + + - - -- cgit v1.3 From 9a3274345dc83096a9bdea612ec77cb2406fef21 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Thu, 2 Jul 2026 15:13:40 +0200 Subject: Fix DST drift in occurrence generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Occurrence.generate_dates: use ChaosCalendar::occurrences_for_timezone with Time.zone.tzinfo.identifier — occurrences now keep wall-clock time across DST boundaries (19:00 stays 19:00 in summer and winter); requires chaoscalendar gem with occurrences_for_timezone support (Gemfile.lock bumped accordingly) - Occurrence.find_in_range: joins(:event) inner join excludes occurrences whose event no longer exists (107k orphaned rows from the pre-revival calendar era were purged from the test DB); includes(:node) retained for eager loading; column references qualified with table name --- Gemfile.lock | 2 +- app/models/occurrence.rb | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) (limited to 'app/models/occurrence.rb') diff --git a/Gemfile.lock b/Gemfile.lock index b5c5c77..2e9a1ce 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,7 +8,7 @@ GIT GIT remote: https://github.com/erdgeist/chaoscalendar.git - revision: 3c0b34dd260f11d15378a6255d9862d629c8413a + revision: 48fe5c517ae3895cc78d9d2fefbdd35b90d52ba7 branch: erdgeist-ruby1.9 specs: chaos_calendar (0.1.3) diff --git a/app/models/occurrence.rb b/app/models/occurrence.rb index 777be24..41ef4cb 100644 --- a/app/models/occurrence.rb +++ b/app/models/occurrence.rb @@ -10,9 +10,10 @@ class Occurrence < ApplicationRecord # Class Methods def self.find_in_range start_time, end_time - includes(:node) - .where("start_time > ? AND end_time < ?", start_time, end_time) - .order("start_time") + joins(:event) + .includes(:node) + .where("occurrences.start_time > ? AND occurrences.end_time < ?", start_time, end_time) + .order("occurrences.start_time") end def self.find_next @@ -48,10 +49,11 @@ class Occurrence < ApplicationRecord # Return value is always an array of Time objects. def self.generate_dates event if event.rrule && !event.rrule.empty? - ChaosCalendar::occurrences( + ChaosCalendar::occurrences_for_timezone( event.start_time, (Time.now + 5.years), - event.rrule + event.rrule, + Time.zone.tzinfo.identifier ) else [event.start_time] -- cgit v1.3 From bff34fb568f7a83c03e2e51b458a1e44b67b794e Mon Sep 17 00:00:00 2001 From: erdgeist Date: Fri, 3 Jul 2026 02:00:53 +0200 Subject: ascade-destroy occurrences; support events without a fixed start time - Event#occurrences is now dependent: :destroy, so destroying an Event (directly, or via Node's cascade) removes its Occurrence rows instead of orphaning them. - Renamed generate_occurences -> generate_occurrences (typo fix); callback and method updated together. - Occurrence.generate returns early when event.start_time is nil, so a chapter with no sourced time produces zero occurrences instead of a guessed one. - seed_chapter no longer falls back to 19:00 when start_time is absent; base_time/end_time are nil in that case. Database-level foreign key cascade on occurrences.event_id is still pending. --- app/models/event.rb | 6 +++--- app/models/occurrence.rb | 1 + db/seeds/chapters.rb | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) (limited to 'app/models/occurrence.rb') diff --git a/app/models/event.rb b/app/models/event.rb index e2e71c9..b8651a8 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -2,12 +2,12 @@ class Event < ApplicationRecord include RruleHumanizer belongs_to :node, optional: true - has_many :occurrences + has_many :occurrences, dependent: :destroy acts_as_taggable_on :tags validates :title, presence: true, unless: -> { node_id.present? } - after_save :generate_occurences + after_save :generate_occurrences def occurrences_in_range start_time, end_time self.occurrences.where( @@ -21,7 +21,7 @@ class Event < ApplicationRecord end private - def generate_occurences + def generate_occurrences Occurrence.generate self end end diff --git a/app/models/occurrence.rb b/app/models/occurrence.rb index 41ef4cb..c6a2380 100644 --- a/app/models/occurrence.rb +++ b/app/models/occurrence.rb @@ -27,6 +27,7 @@ class Occurrence < ApplicationRecord # event are then calculated and created. def self.generate event self.where(:event_id => event.id).delete_all + return if event.start_time.nil? node = event.node duration = (event.end_time - event.start_time) diff --git a/db/seeds/chapters.rb b/db/seeds/chapters.rb index 0b83987..ad3c9a0 100644 --- a/db/seeds/chapters.rb +++ b/db/seeds/chapters.rb @@ -65,13 +65,13 @@ def seed_chapter(parent_id:, slug:, tag:, title_de:, description_de:, # Create events events.each do |ev| - base_time = Time.parse("#{Date.today.year}-01-01 #{ev[:start_time] || '19:00'}:00") + base_time = ev[:start_time] ? Time.parse("#{Date.today.year}-01-01 #{ev[:start_time]}:00") : nil node.events.create!( title: title_de, location: ev[:location] || location, rrule: ev[:rrule], start_time: base_time, - end_time: base_time + (ev[:duration_hours] || 2).hours, + end_time: base_time && base_time + (ev[:duration_hours] || 2).hours, tag_list: ev[:tag_list] || 'open-day' ) end -- cgit v1.3
TitleIs primary Start time End time Rrule Custom rrule Allday UrlLatitudeLongitude Node
<%=h event.display_title %><%=h event.is_primary %> <%=h event.start_time %> <%=h event.end_time %> <%=h event.rrule %> <%=h event.custom_rrule %> <%=h event.allday %> <%=h event.url %><%=h event.latitude %><%=h event.longitude %> <%=h event.node_id %> <%= link_to 'Show', event %> <%= link_to 'Edit', edit_event_path(event) %>