Add DayExclusion model and remove-from-list button #245
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "236-day-exclusion-model"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
DayExclusionmodel that lets admins permanently remove a property from a specific weekday's Previously accordionexcludeendpoint, which creates the exclusion idempotently and removes the item via Turbo Streamload_previouslyby wday, so exclusion on Tuesday does not affect WednesdayChanges
db/migrate/20260617000000_create_day_exclusions.rb: newday_exclusionstable with property_id (FK NOT NULL), wday (integer NOT NULL), unique index on [property_id, wday]app/models/day_exclusion.rb: model with belongs_to :property, validates presence of property_id and wday, validates uniqueness of wday scoped to property_idapp/models/property.rb: addedhas_many :day_exclusions, dependent: :destroyconfig/routes.rb: addedPOST /days/:date/excluderouteapp/controllers/days_controller.rb: addedexcludeaction usingfind_or_create_byfor idempotency; updatedload_previouslyto filter out excluded property_ids for the given wdayapp/views/days/_previously_section.html.erb: added DOM IDs (previously_item_<id>,previously-count) and X button per itemapp/views/days/exclude.turbo_stream.erb: removes the previously item and updates the count badgespec/models/day_exclusion_spec.rb: 6 specs covering presence, uniqueness, associationsspec/requests/days_spec.rb: 12 new specs covering create, idempotency, filtering, cross-day isolation, turbo stream response, and role accessTest Plan
Review Checklist
Related Notes
ldraney/landscaping-assistant #236— the Forgejo issue this PR implementsproject_landscaping_assistant— the project this work belongs toCloses #236
PR #245 Review
Parent issue: #236 -- Day detail page: DayExclusion model and remove-from-list button
DOMAIN REVIEW
Stack: Ruby on Rails 8.1, Hotwire (Turbo Streams), PostgreSQL, RSpec
Model (
DayExclusion)belongs_to :property, validates presence of both fields, uniqueness ofwdayscoped toproperty_id. Good.has_many :day_exclusions, dependent: :destroyonProperty-- correct lifecycle management.Migration
t.references :property, null: false, foreign_key: true-- proper FK with NOT NULL.add_index :day_exclusions, [:property_id, :wday], unique: true-- matches the model uniqueness validation. DB-level enforcement is correct.Controller (
excludeaction)add_to_queueexactly:parse_date->find_property_or_reject-> error handling -> business logic -> respond_to with turbo_stream and HTML fallback. Consistent.find_or_create_by(property_id: property.id, wday: @date.wday)-- idempotent by design. No race condition risk because the unique index will catch concurrent inserts andfind_or_create_byhandles that gracefully.load_previouslymodification: adds 2 lines to reject excluded property IDs by wday. Scoped correctly -- exclusion on Tuesday (wday 2) does not affect Wednesday (wday 3).Authorization
require_role :admin, :super_adminon line 2 covers all actions includingexclude. No separate auth logic needed. No DRY violation.View changes
previously_item_<id>on each<li>,previously-counton the badge,previously-liston the<ul>. All needed for Turbo Stream targeting.button_to(generates a form with CSRF token) -- correct for a POST action. Inline SVG keeps it dependency-free.exclude.turbo_stream.erb): removes the item by DOM ID, replaces the count badge. Clean.Accessibility note (nit): The X button has a
titleattribute ("Remove from Tuesdays") but noaria-label. Screen readers may not announce thetitle. See nits below.BLOCKERS
None.
property_idgoes throughfind_property_or_reject(returns nil for bogus IDs),dategoes throughparse_date(returns nil for unparseable input). Both are handled gracefully.require_rolemechanism.NITS
wdayrange validation (low priority): The model validates presence but not range (0-6). In practice this is safe because@date.wdayalways produces 0-6, and the controller is the only writer. But a model-levelinclusion: { in: 0..6 }validation would be a defensive-in-depth measure that documents the domain constraint. Not blocking.excluded_ids.include?(pid)uses Array: Inload_previously,excluded_idsis an Array and.include?is O(n). For the expected dataset size (handful of exclusions) this is fine. If the exclusion list ever grew large, converting to a Set would be trivial. Not blocking.Accessibility --
aria-labelon X button: The remove button hastitle: "Remove from #{day_name}s"but noaria-label. Addingaria-label: "Remove #{property.client_name} from #{day_name}s"would improve screen reader experience by identifying which property is being removed. Minor.Empty state after all items excluded: If a user excludes every item in the Previously accordion, the list becomes empty but no empty-state message appears (the
<ul>is simply empty). The original partial has an<% else %>branch for the empty state, but Turbo Stream removal does not trigger it. The count badge correctly shows 0, so this is cosmetic. Could be addressed in a follow-up.SOP COMPLIANCE
Closes #236present -- issue will auto-close on mergePROCESS OBSERVATIONS
load_previouslymodification is additive (filter step). Rollback path is clean -- drop the table, revert the code.VERDICT: APPROVED