Skip to content
Draft
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
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ This codebase (Rails 8.1)
| `app/models/` | ActiveRecord models | ~80 files |
| `app/services/` | Service objects and POROs (e.g. `MoneyFormatter` for currency display) | ~30 files |
| `app/jobs/` | SolidQueue background jobs | 4 files |
| `app/models/concerns/` | Shared model modules | 16 concerns |
| `app/models/concerns/` | Shared model modules | 17 concerns |

### Presentation

Expand Down Expand Up @@ -143,6 +143,7 @@ This codebase (Rails 8.1)
| `SectorsTaggable` | Enforces a single primary sector for sector-tagged owners |
| `TagFilterable` | Scope-based filtering by tag names |
| `Trendable` | Trending metrics tracking |
| `UserStampable` | Stamps `updated_by_id` from `Current.user` on every write (no-op without the column) |
| `WindowsTypeFilterable` | Filter by WindowsType association |

## Controllers
Expand Down Expand Up @@ -445,7 +446,7 @@ RuboCop linting on PRs and pushes to main.

## Rake Tasks

Located in `lib/tasks/` (8 files):
Located in `lib/tasks/` (9 files):
- `dev.rake` β€” Development database seeding from XML/CSV
- `rhino_migrator.rake` β€” Rich text editor migration
- `attachment_report.rake` β€” Attachment reporting
Expand All @@ -454,3 +455,4 @@ Located in `lib/tasks/` (8 files):
- `legacy_user_permissions_to_comments.rake` β€” Migrate legacy user permissions into comments
- `migrate_sectors.rake` β€” Sector data migration
- `migrate_workshop_logs.rake` β€” Workshop log migration
- `backfill_user_stamps.rake` β€” Backfill `created_by_id`/`updated_by_id` on legacy rows from the Ahoy trail (`data:backfill_user_stamps`)
6 changes: 1 addition & 5 deletions app/controllers/monthly_reports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,12 @@ def index

def show
@monthly_report = MonthlyReport.includes(
:organization, :windows_type, { created_by: :person },
:organization, :windows_type, { created_by: :person }, { updated_by: :person },
{ quotes: :workshop },
{ gallery_assets: { file_attachment: :blob } }
).find(params[:id]).decorate
authorize! @monthly_report
@answers = @monthly_report.report_form_field_answers.includes(:form_field)
@updated_by = Ahoy::Event.where(resource_type: "MonthlyReport", resource_id: @monthly_report.id)
.where("name LIKE 'update.%'")
.order(time: :desc)
.first&.user
end

private
Expand Down
4 changes: 0 additions & 4 deletions app/controllers/story_ideas_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ def index

def show
authorize! @story_idea
@updated_by = Ahoy::Event.where(resource_type: "StoryIdea", resource_id: @story_idea.id)
.where("name LIKE 'update.%'")
.order(time: :desc)
.first&.user
end

def new
Expand Down
6 changes: 1 addition & 5 deletions app/controllers/workshop_logs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,13 @@ def create

def show
@workshop_log = WorkshopLog.includes(
:organization, :windows_type, { created_by: :person },
:organization, :windows_type, { created_by: :person }, { updated_by: :person },
{ quotes: :workshop },
{ gallery_assets: { file_attachment: :blob } }
).find(params[:id]).decorate
authorize! @workshop_log
@workshop = @workshop_log.workshop&.decorate
@answers = @workshop_log.report_form_field_answers.includes(:form_field)
@updated_by = Ahoy::Event.where(resource_type: "WorkshopLog", resource_id: @workshop_log.id)
.where("name LIKE 'update.%'")
.order(time: :desc)
.first&.user
end

def edit
Expand Down
5 changes: 0 additions & 5 deletions app/controllers/workshop_variation_ideas_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,6 @@ def index
def show
authorize! @workshop_variation_idea
track_view(@workshop_variation_idea)
@updated_by = Ahoy::Event.where(resource_type: "WorkshopVariationIdea", resource_id: @workshop_variation_idea.id)
.where("name LIKE 'update.%'")
.order(time: :desc)
.first&.user

@workshop = (@workshop_variation_idea.workshop || Workshop.where(id: params[:workshop_id]).last)&.decorate
@bookmark = current_user&.bookmarks&.find_by(bookmarkable: @workshop)
@new_bookmark = @workshop.bookmarks.build
Expand Down
1 change: 1 addition & 0 deletions app/models/application_record.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
include AhoyTrackable
include UserStampable

def bookmarks_count
if self.respond_to?(:bookmarks)
Expand Down
2 changes: 1 addition & 1 deletion app/models/concerns/ahoy_trackable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def track_create_event
def track_update_event
return if previously_new_record? # Skip the fake "update" that happens right after create

changes = previous_changes.except("updated_at", "created_at")
changes = previous_changes.except("updated_at", "created_at", "created_by_id", "updated_by_id")
assoc_changes = collect_association_changes

return if changes.empty? && assoc_changes.empty?
Expand Down
28 changes: 28 additions & 0 deletions app/models/concerns/user_stampable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module UserStampable
extend ActiveSupport::Concern

# Stamps updated_by_id from Current.user on every write, so the last editor is
# recorded on the record itself. (created_by_id is set at create time by the
# controllers; only updated_by_id was going unset on updates.) Included on
# ApplicationRecord; the guard makes it a no-op for tables without the column. Runs
# on before_validation so it satisfies a required belongs_to :updated_by before the
# presence check.
included do
before_validation :stamp_updated_by
end

private

def stamp_updated_by
user = Current.user
return unless user
return unless has_attribute?(:updated_by_id)

# Skip when the caller set updated_by_id explicitly (respect it), and when nothing
# else changed (don't turn a no-op save into a write / spurious update event).
return if updated_by_id_changed?
return unless new_record? || changed?

self.updated_by_id = user.id
end
end
1 change: 1 addition & 0 deletions app/models/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Event < ApplicationRecord
has_rich_text :rhino_description

belongs_to :created_by, class_name: "User", optional: true
belongs_to :updated_by, class_name: "User", optional: true
belongs_to :location, optional: true
has_many :bookmarks, as: :bookmarkable, dependent: :destroy
has_many :event_registrations, dependent: :destroy
Expand Down
1 change: 1 addition & 0 deletions app/models/report.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
class Report < ApplicationRecord
belongs_to :owner, polymorphic: true, optional: true
belongs_to :created_by, class_name: "User"
belongs_to :updated_by, class_name: "User", optional: true
belongs_to :organization
belongs_to :windows_type
belongs_to :workshop, optional: true
Expand Down
1 change: 1 addition & 0 deletions app/models/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def self.mentionable_rich_text_fields
has_rich_text :rhino_body

belongs_to :created_by, class_name: "User"
belongs_to :updated_by, class_name: "User", optional: true
belongs_to :author, class_name: "Person", optional: true
belongs_to :workshop, optional: true
belongs_to :windows_type, optional: true
Expand Down
1 change: 1 addition & 0 deletions app/models/workshop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def self.mentionable_rich_text_fields

belongs_to :windows_type, optional: true
belongs_to :created_by, class_name: "User", optional: true
belongs_to :updated_by, class_name: "User", optional: true
belongs_to :author, class_name: "Person", optional: true
belongs_to :workshop_idea, optional: true

Expand Down
1 change: 1 addition & 0 deletions app/models/workshop_log.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class WorkshopLog < ApplicationRecord
belongs_to :created_by, class_name: "User", optional: true
belongs_to :updated_by, class_name: "User", optional: true
belongs_to :organization, optional: true
belongs_to :windows_type, optional: true
belongs_to :workshop, optional: true
Expand Down
1 change: 1 addition & 0 deletions app/models/workshop_variation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def self.search_by_params(params)
belongs_to :organization, optional: true
belongs_to :windows_type, optional: true
belongs_to :created_by, class_name: "User", optional: true
belongs_to :updated_by, class_name: "User", optional: true
belongs_to :author, class_name: "Person", optional: true
belongs_to :workshop_variation_idea, optional: true
has_many :bookmarks, as: :bookmarkable, dependent: :destroy
Expand Down
12 changes: 7 additions & 5 deletions app/views/monthly_reports/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,13 @@
<%= @monthly_report.created_by&.name %>
<br><span class="text-gray-400"><%= @monthly_report.created_at.in_time_zone.strftime("%Y-%m-%d %I:%M %P") %></span>
</div>
<div>
<span class="font-semibold text-gray-700">Updated by:</span>
<%= @updated_by&.name || "β€”" %>
<br><span class="text-gray-400"><%= @monthly_report.updated_at.in_time_zone.strftime("%Y-%m-%d %I:%M %P") %></span>
</div>
<% if @monthly_report.updated_at != @monthly_report.created_at %>
<div>
<span class="font-semibold text-gray-700">Updated by:</span>
<%= @monthly_report.updated_by&.name || "β€”" %>
<br><span class="text-gray-400"><%= @monthly_report.updated_at.in_time_zone.strftime("%Y-%m-%d %I:%M %P") %></span>
</div>
<% end %>
</div>
</div>

Expand Down
12 changes: 7 additions & 5 deletions app/views/story_ideas/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,13 @@
<%= @story_idea.created_by&.name %>
<br><span class="text-gray-400"><%= @story_idea.created_at.in_time_zone.strftime("%Y-%m-%d %I:%M %P") %></span>
</div>
<div>
<span class="font-semibold text-gray-700">Updated by:</span>
<%= @updated_by&.name || "β€”" %>
<br><span class="text-gray-400"><%= @story_idea.updated_at.in_time_zone.strftime("%Y-%m-%d %I:%M %P") %></span>
</div>
<% if @story_idea.updated_at != @story_idea.created_at %>
<div>
<span class="font-semibold text-gray-700">Updated by:</span>
<%= @story_idea.updated_by&.name || "β€”" %>
<br><span class="text-gray-400"><%= @story_idea.updated_at.in_time_zone.strftime("%Y-%m-%d %I:%M %P") %></span>
</div>
<% end %>
</div>

<% if @story_idea.sectors.any? %>
Expand Down
12 changes: 7 additions & 5 deletions app/views/workshop_logs/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,13 @@
<%= @workshop_log.created_by&.name %>
<br><span class="text-gray-400"><%= @workshop_log.created_at.in_time_zone.strftime("%Y-%m-%d %I:%M %P") %></span>
</div>
<div>
<span class="font-semibold text-gray-700">Updated by:</span>
<%= @updated_by&.name || "β€”" %>
<br><span class="text-gray-400"><%= @workshop_log.updated_at.in_time_zone.strftime("%Y-%m-%d %I:%M %P") %></span>
</div>
<% if @workshop_log.updated_at != @workshop_log.created_at %>
<div>
<span class="font-semibold text-gray-700">Updated by:</span>
<%= @workshop_log.updated_by&.name || "β€”" %>
<br><span class="text-gray-400"><%= @workshop_log.updated_at.in_time_zone.strftime("%Y-%m-%d %I:%M %P") %></span>
</div>
<% end %>
</div>
</div>

Expand Down
12 changes: 7 additions & 5 deletions app/views/workshop_variation_ideas/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,13 @@
<%= @workshop_variation_idea.created_by&.name %>
<br><span class="text-gray-400"><%= @workshop_variation_idea.created_at.in_time_zone.strftime("%Y-%m-%d %I:%M %P") %></span>
</div>
<div>
<span class="font-semibold text-gray-700">Updated by:</span>
<%= @updated_by&.name || "β€”" %>
<br><span class="text-gray-400"><%= @workshop_variation_idea.updated_at.in_time_zone.strftime("%Y-%m-%d %I:%M %P") %></span>
</div>
<% if @workshop_variation_idea.updated_at != @workshop_variation_idea.created_at %>
<div>
<span class="font-semibold text-gray-700">Updated by:</span>
<%= @workshop_variation_idea.updated_by&.name || "β€”" %>
<br><span class="text-gray-400"><%= @workshop_variation_idea.updated_at.in_time_zone.strftime("%Y-%m-%d %I:%M %P") %></span>
</div>
<% end %>
</div>
</div>

Expand Down
22 changes: 22 additions & 0 deletions db/migrate/20260729123942_add_updated_by_id_to_audited_tables.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class AddUpdatedByIdToAuditedTables < ActiveRecord::Migration[8.1]
# Tables that already stamp created_by_id but have no matching updated_by_id. With
# these columns the UserStampable concern records the last editor on every update.
# reports covers MonthlyReport (STI on reports).
TABLES = %i[reports workshop_logs workshop_variations resources workshops events].freeze

def up
TABLES.each do |table|
add_column table, :updated_by_id, :integer, null: true unless column_exists?(table, :updated_by_id)
add_index table, :updated_by_id unless index_exists?(table, :updated_by_id)
add_foreign_key table, :users, column: :updated_by_id unless foreign_key_exists?(table, :users, column: :updated_by_id)
end
end

def down
TABLES.each do |table|
remove_foreign_key table, :users, column: :updated_by_id if foreign_key_exists?(table, :users, column: :updated_by_id)
remove_index table, :updated_by_id if index_exists?(table, :updated_by_id)
remove_column table, :updated_by_id if column_exists?(table, :updated_by_id)
end
end
end
Loading