From 95955abaa339098755a214cfcadf87c90211fe64 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Wed, 1 Jul 2026 00:24:10 +0200
Subject: Add RRULE humanizer and wire events into nodes#show
- app/models/concerns/rrule_humanizer.rb: new concern included into
Event, renders recurring schedule as natural-language German or
English from RRULE string; handles WEEKLY/MONTHLY, biweekly
(INTERVAL=2), ordinal weekday positions (1TU, -1TH, -2WE),
BYMONTH single-month exclusions (December pause convention);
gracefully returns nil for COUNT/UNTIL/unrecognized shapes
- test/models/concerns/rrule_humanizer_test.rb: 15 tests covering
all distinct RRULE shapes found in production data
- app/helpers/nodes_helper.rb: add event_schedule_text helper
combining humanize_rrule with start_time formatting
- app/views/nodes/show.html.erb: add events row, conditionally
rendered when node has associated events
- config/locales/de.yml, en.yml: add event_schedule_time,
event_schedule_unrecognized, event_schedule_none keys
---
app/views/nodes/show.html.erb | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
(limited to 'app/views/nodes')
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb
index de4d8a2..7223219 100644
--- a/app/views/nodes/show.html.erb
+++ b/app/views/nodes/show.html.erb
@@ -42,6 +42,18 @@
| Tagged with: |
<%= @page.tag_list %> |
+ <% if @node.events.any? %>
+
+ | Events |
+
+
+ <% @node.events.order(:start_time).each do |event| %>
+ - <%= event_schedule_text(event) %><%= " (primary)" if event.is_primary? %>
+ <% end %>
+
+ |
+
+ <% end %>
| Title |
<%= sanitize( @page.title ) %> |
@@ -59,4 +71,4 @@
|
-
\ No newline at end of file
+
--
cgit v1.3
From b6416e86c9c58b8e886c14de55b01aeb863b8676 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Thu, 2 Jul 2026 00:12:02 +0200
Subject: Clean up events views: return_to, subnav, remove custom_rrule
- events_controller: wire return_to through show and new actions;
create respects return_to with fallback to node/event path;
new pre-populates node_id and tag_list from params
- events/edit: remove custom_rrule checkbox; node link removed from
subnav (use show for node navigation); destroy button added
- events/new: remove custom_rrule checkbox; add return_to hidden
field and back link; tag_list field added
- events/show: fix back link via safe_return_to; add node link to
subnav; add destroy button; remove custom_rrule display; show
humanized rrule below raw string
- events/index: destructive class on destroy button; node_id column
replaced with node link; show/edit links carry return_to
- nodes/show: add show/edit links to event entries with return_to
---
app/controllers/events_controller.rb | 10 +++++++---
app/views/events/edit.html.erb | 9 +++------
app/views/events/index.html.erb | 10 ++++------
app/views/events/new.html.erb | 7 ++++---
app/views/events/show.html.erb | 16 +++++++++++-----
app/views/nodes/show.html.erb | 6 +++++-
6 files changed, 34 insertions(+), 24 deletions(-)
(limited to 'app/views/nodes')
diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb
index 3a60cf9..9bed5dd 100644
--- a/app/controllers/events_controller.rb
+++ b/app/controllers/events_controller.rb
@@ -21,6 +21,7 @@ class EventsController < ApplicationController
# GET /events/1.xml
def show
@event = Event.find(params[:id])
+ @return_to = params[:return_to] || events_path
respond_to do |format|
format.html # show.html.erb
@@ -31,7 +32,10 @@ class EventsController < ApplicationController
# GET /events/new
# GET /events/new.xml
def new
- @event = Event.new(:node_id => params[:node_id])
+ @event = Event.new(
+ node_id: params[:node_id],
+ tag_list: params[:tag_list]
+ )
respond_to do |format|
format.html # new.html.erb
@@ -53,7 +57,7 @@ class EventsController < ApplicationController
respond_to do |format|
if @event.save
flash[:notice] = 'Event was successfully created.'
- format.html { redirect_to(@event.node ? edit_node_path(@event.node) : edit_event_path(@event)) }
+ format.html { redirect_to(safe_return_to(params[:return_to] || (@event.node ? edit_node_path(@event.node) : edit_event_path(@event)))) }
format.xml { render :xml => @event, :status => :created, :location => @event }
else
format.html { render :action => "new" }
@@ -94,6 +98,6 @@ class EventsController < ApplicationController
private
def event_params
- params.require(:event).permit(:title, :description, :is_primary, :start_time, :end_time, :rrule, :custom_rrule, :allday, :url, :latitude, :longitude, :node_id, :location)
+ params.require(:event).permit(:title, :description, :start_time, :end_time, :rrule, :allday, :url, :latitude, :longitude, :node_id, :location, :tag_list)
end
end
diff --git a/app/views/events/edit.html.erb b/app/views/events/edit.html.erb
index 17457df..5aee501 100644
--- a/app/views/events/edit.html.erb
+++ b/app/views/events/edit.html.erb
@@ -1,9 +1,6 @@
<% content_for :subnavigation do %>
<%= link_to 'back', safe_return_to(params[:return_to] || events_path) %>
- <% if @event.node %>
- <%= link_to 'node', edit_node_path(@event.node) %>
- <% end %>
- <%= link_to 'show', @event %>
+ <%= button_to 'destroy', event_path(@event), method: :delete, form: { data: { confirm: 'Delete this event?' }, class: 'button_to destructive' } %>
<% end %>
Editing event
@@ -25,8 +22,8 @@
<%= f.text_field :rrule %>
<%= f.hidden_field :node_id %>
+ <%= hidden_field_tag :return_to, params[:return_to] %>
Date: Thu, 2 Jul 2026 00:53:40 +0200
Subject: Add direct edit link to nodes#index
First application of subnav visual conventions to nodes: actions
column now shows show | edit | revisions as plain informational
links. edit intentionally uses edit_node_path (locking is expected
behaviour for nodes, unlike event views where node_path is used to
avoid inadvertent locking).
---
app/views/nodes/index.html.erb | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
(limited to 'app/views/nodes')
diff --git a/app/views/nodes/index.html.erb b/app/views/nodes/index.html.erb
index e5a55d4..2399105 100644
--- a/app/views/nodes/index.html.erb
+++ b/app/views/nodes/index.html.erb
@@ -21,7 +21,8 @@
<%= link_to 'show', node_path(node) %>
- <%= link_to 'Revisions', node_revisions_path(node) %>
+ <%= link_to 'edit', edit_node_path(node) %>
+ <%= link_to 'revisions', node_revisions_path(node) %>
|
<%= node.lock_owner.login if node.lock_owner %>
--
cgit v1.3
From 206dc5c50a73c5402b90d7fdc8945d3ba9356758 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Sat, 4 Jul 2026 01:24:55 +0200
Subject: Add self-service event creation from nodes#show
nodes#show's events table now renders unconditionally (previously
hidden entirely for a zero-event node) and gains an "add event" link.
The suggested tag_list is derived from the page's own category tags
via NodesHelper::DEFAULT_EVENT_TAG_BY_PAGE_TAG (erfa-detail/
chaostreff-detail -> open-day) rather than hardcoded, and degrades to
a blank field for any node whose tags don't match - deliberately
universal, not chapter-specific, since Updates have historically
carried event dates the same way.
events#new surfaces *why* the tag was pre-filled via flash.now (not
flash - this is a same-request render, not a redirect), using an
explicit auto_tag_source param passed alongside tag_list rather than
having the controller re-derive the reason from node_id.
No destroy link added to nodes#show's events list - deliberate, per
existing subnav-semantics convention (destructive actions live on the
resource's own views). Covered directly by test.
Adds real coverage for EventsController, previously 100% commented-out
scaffold, plus unit tests for the new tag-mapping helper and the
weekday-abbreviation helpers from the prior commit.
---
app/controllers/events_controller.rb | 4 +
app/helpers/nodes_helper.rb | 14 +++
app/views/nodes/show.html.erb | 4 +-
test/controllers/events_controller_test.rb | 167 ++++++++++++++++++++-------
test/controllers/nodes_controller_test.rb | 33 ++++++
test/models/concerns/rrule_humanizer_test.rb | 15 +++
test/models/helpers/content_helper_test.rb | 4 +
test/models/helpers/nodes_helper_test.rb | 21 ++++
8 files changed, 219 insertions(+), 43 deletions(-)
(limited to 'app/views/nodes')
diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb
index 9bed5dd..b98a38e 100644
--- a/app/controllers/events_controller.rb
+++ b/app/controllers/events_controller.rb
@@ -37,6 +37,10 @@ class EventsController < ApplicationController
tag_list: params[:tag_list]
)
+ if params[:tag_list].present? && params[:auto_tag_source].present?
+ flash.now[:notice] = "Tag '#{params[:tag_list]}' was pre-filled because this page is tagged '#{params[:auto_tag_source]}'. You can remove it below."
+ end
+
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => @event }
diff --git a/app/helpers/nodes_helper.rb b/app/helpers/nodes_helper.rb
index 329bcc5..093bfc6 100644
--- a/app/helpers/nodes_helper.rb
+++ b/app/helpers/nodes_helper.rb
@@ -44,6 +44,20 @@ module NodesHelper
])
end
+ DEFAULT_EVENT_TAG_BY_PAGE_TAG = {
+ 'erfa-detail' => 'open-day',
+ 'chaostreff-detail' => 'open-day'
+ }.freeze
+
+ def default_event_tag_mapping(page)
+ page_tags = page.tag_list
+ DEFAULT_EVENT_TAG_BY_PAGE_TAG.find { |page_tag, _| page_tags.include?(page_tag) }
+ end
+
+ def default_event_tag_list(page)
+ default_event_tag_mapping(page)&.last
+ end
+
def event_schedule_text(event)
if event.rrule.present?
recurrence = event.humanize_rrule(I18n.locale)
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb
index 2ce3853..c533a55 100644
--- a/app/views/nodes/show.html.erb
+++ b/app/views/nodes/show.html.erb
@@ -42,7 +42,6 @@
| Tagged with: |
<%= @page.tag_list %> |
- <% if @node.events.any? %>
| Events |
@@ -55,9 +54,10 @@
<% end %>
+ <% mapping = default_event_tag_mapping(@page) %>
+ <%= link_to 'add event', new_event_path(node_id: @node.id, tag_list: mapping&.last, auto_tag_source: mapping&.first, return_to: request.path) %>
|
- <% end %>
| Title |
<%= sanitize( @page.title ) %> |
diff --git a/test/controllers/events_controller_test.rb b/test/controllers/events_controller_test.rb
index 14e534e..9371ca7 100644
--- a/test/controllers/events_controller_test.rb
+++ b/test/controllers/events_controller_test.rb
@@ -1,45 +1,130 @@
require 'test_helper'
class EventsControllerTest < ActionController::TestCase
- # test "should get index" do
- # get :index
- # assert_response :success
- # assert_not_nil assigns(:events)
- # end
- #
- # test "should get new" do
- # get :new
- # assert_response :success
- # end
- #
- # test "should create event" do
- # assert_difference('Event.count') do
- # post :create, params: { :event => { } }
- # end
- #
- # assert_redirected_to event_path(assigns(:event))
- # end
- #
- # test "should show event" do
- # get :show, params: { :id => events(:one).to_param }
- # assert_response :success
- # end
- #
- # test "should get edit" do
- # get :edit, params: { :id => events(:one).to_param }
- # assert_response :success
- # end
- #
- # test "should update event" do
- # put :update, params: { :id => events(:one).to_param, :event => { } }
- # assert_redirected_to event_path(assigns(:event))
- # end
- #
- # test "should destroy event" do
- # assert_difference('Event.count', -1) do
- # delete :destroy, params: { :id => events(:one).to_param }
- # end
- #
- # assert_redirected_to events_path
- # end
+
+ test "should get index" do
+ login_as :quentin
+ get :index
+ assert_response :success
+ assert_not_nil assigns(:events)
+ end
+
+ test "should get new" do
+ login_as :quentin
+ get :new
+ assert_response :success
+ end
+
+ test "new pre-fills tag_list and explains it via flash when auto_tag_source is given" do
+ login_as :quentin
+ node = create_node_with_published_page
+
+ get :new, params: { node_id: node.id, tag_list: "open-day", auto_tag_source: "erfa-detail" }
+
+ assert_response :success
+ assert_equal "open-day", assigns(:event).tag_list.to_s
+ assert_match "open-day", flash[:notice]
+ assert_match "erfa-detail", flash[:notice]
+ end
+
+ test "new does not flash without an auto_tag_source" do
+ login_as :quentin
+
+ get :new, params: { tag_list: "open-day" }
+
+ assert_response :success
+ assert_nil flash[:notice]
+ end
+
+ test "new with no params renders a blank, unflashed form" do
+ login_as :quentin
+
+ get :new
+
+ assert_response :success
+ assert_nil assigns(:event).tag_list.presence
+ assert_nil flash[:notice]
+ end
+
+ test "should show event" do
+ login_as :quentin
+ node = create_node_with_published_page
+ event = Event.create!(node_id: node.id, start_time: Time.now, end_time: Time.now + 1.hour)
+
+ get :show, params: { id: event.id }
+ assert_response :success
+ end
+
+ test "should get edit" do
+ login_as :quentin
+ node = create_node_with_published_page
+ event = Event.create!(node_id: node.id, start_time: Time.now, end_time: Time.now + 1.hour)
+
+ get :edit, params: { id: event.id }
+ assert_response :success
+ end
+
+ test "should create event attached to a node" do
+ login_as :quentin
+ node = create_node_with_published_page
+
+ assert_difference('Event.count') do
+ post :create, params: {
+ event: {
+ node_id: node.id,
+ start_time: Time.now,
+ end_time: Time.now + 1.hour,
+ tag_list: "open-day"
+ }
+ }
+ end
+
+ assert_redirected_to edit_node_path(node)
+ assert_equal 'Event was successfully created.', flash[:notice]
+ end
+
+ test "should not create an event without a title or a node_id" do
+ login_as :quentin
+
+ assert_no_difference('Event.count') do
+ post :create, params: { event: { start_time: Time.now, end_time: Time.now + 1.hour } }
+ end
+
+ assert_response :success # re-renders :new, not a redirect
+ end
+
+ test "should honour return_to on create" do
+ login_as :quentin
+ node = create_node_with_published_page
+
+ post :create, params: {
+ event: { node_id: node.id, start_time: Time.now, end_time: Time.now + 1.hour },
+ return_to: node_path(node)
+ }
+
+ assert_redirected_to node_path(node)
+ end
+
+ test "should update event" do
+ login_as :quentin
+ node = create_node_with_published_page
+ event = Event.create!(node_id: node.id, start_time: Time.now, end_time: Time.now + 1.hour)
+
+ put :update, params: { id: event.id, event: { title: "Updated title" } }
+
+ assert_redirected_to events_path
+ assert_equal "Updated title", event.reload.title
+ end
+
+ test "should destroy event" do
+ login_as :quentin
+ node = create_node_with_published_page
+ event = Event.create!(node_id: node.id, start_time: Time.now, end_time: Time.now + 1.hour)
+
+ assert_difference('Event.count', -1) do
+ delete :destroy, params: { id: event.id }
+ end
+
+ assert_redirected_to events_url
+ end
end
diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb
index 53799f1..f14e27c 100644
--- a/test/controllers/nodes_controller_test.rb
+++ b/test/controllers/nodes_controller_test.rb
@@ -379,4 +379,37 @@ class NodesControllerTest < ActionController::TestCase
get :index
end
+ test "show renders events row and add-link for zero-event chapter node" do
+ login_as :quentin
+ node = create_node_with_published_page
+ node.head.tag_list = "erfa-detail"
+ node.head.save!
+
+ get :show, params: { id: node.id }
+ assert_response :success
+ assert_select "a", text: "add event"
+ assert_select "a[href*='tag_list=open-day']"
+ assert_select "a[href*='auto_tag_source=erfa-detail']"
+ end
+
+ test "show renders events row without a tag default for untagged node" do
+ login_as :quentin
+ node = create_node_with_published_page
+
+ get :show, params: { id: node.id }
+ assert_response :success
+ assert_select "a", text: "add event"
+ assert_select "a[href*='tag_list=']", count: 0
+ end
+
+ test "show never renders a destroy link for events" do
+ login_as :quentin
+ node = create_node_with_published_page
+ Event.create!(node_id: node.id, start_time: Time.now, end_time: Time.now + 1.hour)
+
+ get :show, params: { id: node.id }
+ assert_response :success
+ assert_select "form.button_to.destructive", count: 0
+ end
+
end
diff --git a/test/models/concerns/rrule_humanizer_test.rb b/test/models/concerns/rrule_humanizer_test.rb
index 500dbc7..279ff73 100644
--- a/test/models/concerns/rrule_humanizer_test.rb
+++ b/test/models/concerns/rrule_humanizer_test.rb
@@ -81,4 +81,19 @@ class RruleHumanizerTest < ActiveSupport::TestCase
test "falls back to english for unknown locale" do
assert_equal "Every Tuesday", humanize("FREQ=WEEKLY;BYDAY=TU", :fr)
end
+
+ test "wday_abbr returns the correct German abbreviation for each day" do
+ monday = Time.parse("2026-07-06") # confirmed Monday
+ assert_equal "Mo", RruleHumanizer.wday_abbr(monday, :de)
+ assert_equal "Di", RruleHumanizer.wday_abbr(monday + 1.day, :de)
+ assert_equal "Mi", RruleHumanizer.wday_abbr(monday + 2.days, :de)
+ assert_equal "Do", RruleHumanizer.wday_abbr(monday + 3.days, :de)
+ assert_equal "Fr", RruleHumanizer.wday_abbr(monday + 4.days, :de)
+ assert_equal "Sa", RruleHumanizer.wday_abbr(monday + 5.days, :de)
+ assert_equal "So", RruleHumanizer.wday_abbr(monday + 6.days, :de)
+ end
+
+ test "wday_abbr falls back to :de for an unrecognized locale" do
+ assert_equal "Mo", RruleHumanizer.wday_abbr(Time.parse("2026-07-06"), :fr)
+ end
end
diff --git a/test/models/helpers/content_helper_test.rb b/test/models/helpers/content_helper_test.rb
index 2da82d7..a7ed478 100644
--- a/test/models/helpers/content_helper_test.rb
+++ b/test/models/helpers/content_helper_test.rb
@@ -1,4 +1,8 @@
require 'test_helper'
class ContentHelperTest < ActionView::TestCase
+ test "weekday_abbr delegates through the current I18n locale" do
+ I18n.locale = :de
+ assert_equal "Mo", weekday_abbr(Time.parse("2026-07-06"))
+ end
end
diff --git a/test/models/helpers/nodes_helper_test.rb b/test/models/helpers/nodes_helper_test.rb
index 13011de..5d91a88 100644
--- a/test/models/helpers/nodes_helper_test.rb
+++ b/test/models/helpers/nodes_helper_test.rb
@@ -1,4 +1,25 @@
require 'test_helper'
class NodesHelperTest < ActionView::TestCase
+ FakePage = Struct.new(:tag_list)
+
+ test "default_event_tag_mapping matches erfa-detail" do
+ page = FakePage.new(["erfa-detail"])
+ assert_equal ["erfa-detail", "open-day"], default_event_tag_mapping(page)
+ end
+
+ test "default_event_tag_mapping matches chaostreff-detail" do
+ page = FakePage.new(["chaostreff-detail"])
+ assert_equal ["chaostreff-detail", "open-day"], default_event_tag_mapping(page)
+ end
+
+ test "default_event_tag_mapping returns nil for unrelated tags" do
+ page = FakePage.new(["update"])
+ assert_nil default_event_tag_mapping(page)
+ end
+
+ test "default_event_tag_list is nil without a matching tag" do
+ page = FakePage.new([])
+ assert_nil default_event_tag_list(page)
+ end
end
--
cgit v1.3
From 9c630d186003a47563be2e3547485060dac7dd98 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Sat, 4 Jul 2026 01:34:42 +0200
Subject: Retire event_information, superseded by nodes#show's event manager
The metadata-panel "Event" field on nodes#edit called event_information,
a second, tag-less implementation of "list events + add one" - now
redundant now that nodes#show has the tag-aware version with the
auto-tag flash explanation. Confirmed via grep this was the only call
site before removing both it and the method definition.
---
app/helpers/nodes_helper.rb | 15 ---------------
app/views/nodes/edit.html.erb | 3 ---
2 files changed, 18 deletions(-)
(limited to 'app/views/nodes')
diff --git a/app/helpers/nodes_helper.rb b/app/helpers/nodes_helper.rb
index 093bfc6..2baf813 100644
--- a/app/helpers/nodes_helper.rb
+++ b/app/helpers/nodes_helper.rb
@@ -29,21 +29,6 @@ module NodesHelper
User.all.map {|u| [u.login, u.id]}
end
- def event_information
- 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('edit', edit_event_path(event)),
- ])
- end
- safe_join([
- safe_join(items, ' | '),
- ' > ',
- link_to('add event', new_event_path(:node_id => @node.id))
- ])
- end
-
DEFAULT_EVENT_TAG_BY_PAGE_TAG = {
'erfa-detail' => 'open-day',
'chaostreff-detail' => 'open-day'
diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb
index 498fa1f..31f585c 100644
--- a/app/views/nodes/edit.html.erb
+++ b/app/views/nodes/edit.html.erb
@@ -15,9 +15,6 @@
<% end %>
|