Skip to content

Commit de3c632

Browse files
committed
Add event reminder functionality with associated tests and mailer
- Implement EventReminderNotifier to handle sending event reminder emails and creating notification records. - Create EventReminderJob to enqueue notifications for event attendees. - Develop EventReminderSchedulerJob to schedule reminders at specified intervals. - Add EventMailer for sending reminder and update emails with appropriate content. - Enhance Event model with validations, scopes, and callbacks for reminder scheduling. - Introduce tests for Event, EventReminderNotifier, EventReminderJob, EventReminderSchedulerJob, and EventMailer. - Implement validations and methods in LocatableLocation for handling location data.
1 parent d921f33 commit de3c632

File tree

22 files changed

+2225
-70
lines changed

22 files changed

+2225
-70
lines changed

.github/copilot-instructions.md

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,15 @@
1-
# Be## Core Principles
2-
3-
- **Security first**: Run `bundle exec brakeman -q` before generating code; fix high-confidence vulnerabilities
4-
- **Accessibility first** (WCAG AA/AAA): semantic HTML, ARIA roles, keyboard nav, proper contrast.
5-
- **Hotwire everywhere**: Turbo for navigation/updates; Stimulus controllers for interactivity.
6-
- **Keep controllers thin**; move business logic to POROs/service objects or concerns.
7-
- **Prefer explicit join models** over polymorphic associations when validation matters.
8-
- **Avoid the term "STI"** in code/comments; use "single-table inheritance" or alternate designs.
9-
- **Use `ENV.fetch`** rather than `ENV[]`.
10-
- **Always add policy/authorization checks** on links/buttons to controller actions.
11-
- **i18n & Mobility**: every user-facing string must be translatable; include missing keys.
12-
- Provide translations for all available locales (e.g., en, es, fr) when adding new strings.er Community Engine – Rails App & Engine Guidelines
1+
# Better Together Community Engine – Rails App & Engine Guidelines
132

143
This repository contains the **Better Together Community Engine** (an isolated Rails engine under the `BetterTogether` namespace) and/or a host Rails app that mounts it. Use these instructions for all code generation.
154

165
## Core Principles
176

7+
- **Security first**: Run `bundle exec brakeman --quiet --no-pager` before generating code; fix high-confidence vulnerabilities
188
- **Accessibility first** (WCAG AA/AAA): semantic HTML, ARIA roles, keyboard nav, proper contrast.
199
- **Hotwire everywhere**: Turbo for navigation/updates; Stimulus controllers for interactivity.
2010
- **Keep controllers thin**; move business logic to POROs/service objects or concerns.
2111
- **Prefer explicit join models** over polymorphic associations when validation matters.
22-
- **Avoid the term STI** in code/comments; use single-table inheritance or alternate designs.
12+
- **Avoid the term "STI"** in code/comments; use "single-table inheritance" or alternate designs.
2313
- **Use `ENV.fetch`** rather than `ENV[]`.
2414
- **Always add policy/authorization checks** on links/buttons to controller actions.
2515
- **i18n & Mobility**: every user-facing string must be translatable; include missing keys.
@@ -59,7 +49,7 @@ This repository contains the **Better Together Community Engine** (an isolated R
5949
## Coding Guidelines
6050

6151
### Security Requirements
62-
- **Run Brakeman before generating code**: `bundle exec brakeman -q`
52+
- **Run Brakeman before generating code**: `bundle exec brakeman --quiet --no-pager`
6353
- **Fix high-confidence vulnerabilities immediately** - never ignore security warnings with "High" confidence
6454
- **Review and address medium-confidence warnings** that are security-relevant
6555
- **Safe coding practices when generating code:**
@@ -71,7 +61,7 @@ This repository contains the **Better Together Community Engine** (an isolated R
7161
- **SQL injection prevention**: Use parameterized queries, avoid string interpolation in SQL
7262
- **XSS prevention**: Use Rails auto-escaping, sanitize HTML inputs with allowlists
7363
- **For reflection-based features**: Create concerns with `included_in_models` class methods for safe dynamic class resolution
74-
- **Post-generation security check**: Run `bundle exec brakeman -c UnsafeReflection,SQL,CrossSiteScripting` after major code changes
64+
- **Post-generation security check**: Run `bundle exec brakeman --quiet --no-pager -c UnsafeReflection,SQL,CrossSiteScripting` after major code changes
7565

7666
## Test Environment Setup
7767
- Configure the host Platform in a before block for controller/request/feature tests.
@@ -105,10 +95,74 @@ This repository contains the **Better Together Community Engine** (an isolated R
10595
- Ensure blobs are encrypted at rest
10696
- **Testing**
10797
- RSpec (if present) or Minitest – follow existing test framework
98+
- **Generate comprehensive test coverage for all changes**: Every modification must include RSpec tests covering the new functionality
10899
- All RSpec specs **must use FactoryBot factories** for model instances (do not use `Model.create` or `Model.new` directly in specs).
109100
- **A FactoryBot factory must exist for every model**. When generating a new model, also generate a factory for it.
110101
- **Factories must use the Faker gem** to provide realistic, varied test data for all attributes (e.g., names, emails, addresses, etc.).
102+
- **Test all layers**: models, controllers, mailers, jobs, JavaScript/Stimulus controllers, and integration workflows
111103
- System tests for Turbo flows where possible
104+
- **Session-based testing**: When working on existing code modifications, generate tests that cover all unstaged changes and related functionality
105+
106+
## Test Generation Strategy
107+
108+
### Mandatory Test Creation
109+
When modifying existing code or adding new features, always generate RSpec tests that provide comprehensive coverage:
110+
111+
1. **Model Tests**:
112+
- Validations, associations, scopes, callbacks
113+
- Instance methods, class methods, delegations
114+
- Business logic and calculated attributes
115+
- Security-related functionality (encryption, authorization)
116+
117+
2. **Controller Tests**:
118+
- All CRUD actions and custom endpoints
119+
- Authorization policy checks (Pundit/equivalent)
120+
- Parameter handling and strong params
121+
- Response formats (HTML, JSON, Turbo Stream)
122+
- Error handling and edge cases
123+
124+
3. **Background Job Tests**:
125+
- Job execution and success scenarios
126+
- Retry logic and error handling
127+
- Side effects and state changes
128+
- Queue assignment and timing
129+
130+
4. **Mailer Tests**:
131+
- Email content and formatting
132+
- Recipient handling and localization
133+
- Attachment and delivery configurations
134+
- Multi-locale support
135+
136+
5. **JavaScript/Stimulus Tests**:
137+
- Controller initialization and teardown
138+
- User interaction handlers
139+
- Form state management and dynamic updates
140+
- Target and action mappings
141+
142+
6. **Integration Tests**:
143+
- Complete user workflows
144+
- Cross-model interactions
145+
- End-to-end feature functionality
146+
- Authentication and authorization flows
147+
148+
### Session-Specific Test Coverage
149+
For this codebase, ensure tests cover all recent changes including:
150+
- Enhanced LocatableLocation model with polymorphic associations
151+
- Event model with notification callbacks and location integration
152+
- Calendar and CalendarEntry associations
153+
- Event notification system (EventReminderNotifier, EventUpdateNotifier)
154+
- Background jobs for event reminders and scheduling
155+
- EventMailer with localized content
156+
- Dynamic location selector JavaScript controller
157+
- Form enhancements with location type selection
158+
159+
### Test Quality Standards
160+
- Use descriptive test names that explain the expected behavior
161+
- Follow AAA pattern (Arrange, Act, Assert) in test structure
162+
- Mock external dependencies and network calls
163+
- Test both success and failure scenarios
164+
- Use shared examples for common behavior patterns
165+
- Ensure tests are deterministic and can run independently
112166

113167
## Project Architecture Notes
114168

AGENTS.md

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,28 @@ Instructions for GitHub Copilot and other automated contributors working in this
2121
- **Tests:** `bin/ci`
2222
(Equivalent: `cd spec/dummy && bundle exec rspec`)
2323
- **Lint:** `bundle exec rubocop`
24-
- **Security:** `bundle exec brakeman -q -w2` and `bundle exec bundler-audit --update`
24+
- **Security:** `bundle exec brakeman --quiet --no-pager` and `bundle exec bundler-audit --update`
2525
- **Style:** `bin/codex_style_guard`
2626

2727
## Security Requirements
28-
- **Always run Brakeman** before generating/committing code: `bundle exec brakeman -q`
29-
- **Address high-confidence vulnerabilities immediately** - anything with "High" confidence must be fixed
30-
- **Review medium-confidence warnings** - evaluate and fix security-relevant issues
31-
- **Use safe coding practices:**
28+
## Security Requirements
29+
- **Run Brakeman before generating code**: `bundle exec brakeman --quiet --no-pager`
30+
- **Fix high-confidence vulnerabilities immediately** - never ignore security warnings with "High" confidence
31+
- **Review and address medium-confidence warnings** that are security-relevant
32+
- **Safe coding practices when generating code:**
3233
- Never use `constantize`, `safe_constantize`, or `eval` on user input
3334
- Use allow-lists for dynamic class resolution (see `joatu_source_class` pattern)
3435
- Sanitize and validate all user inputs
3536
- Use strong parameters in controllers
3637
- Implement proper authorization checks (Pundit policies)
37-
- **For reflection-based code**: Create concern-based allow-lists using `included_in_models` pattern
38-
- **Run security scan after major changes**: `bundle exec brakeman -c UnsafeReflection,SQL,CrossSiteScripting`
38+
- **For reflection-based features**: Create concerns with `included_in_models` class methods for safe dynamic class resolution
39+
- **Post-generation security check**: Run `bundle exec brakeman --quiet --no-pager -c UnsafeReflection,SQL,CrossSiteScripting` after major code changes
3940

4041
## Conventions
4142
- Make incremental changes with passing tests.
42-
- **Security first**: Run `bundle exec brakeman -q` before committing code changes.
43+
- **Security first**: Run `bundle exec brakeman --quiet --no-pager` before committing code changes.
44+
- **Test every change**: Generate RSpec tests for all code modifications, including models, controllers, mailers, jobs, and JavaScript.
45+
- **Test coverage requirements**: All new features, bug fixes, and refactors must include comprehensive test coverage.
4346
- Avoid introducing new external services in tests; stub where possible.
4447
- If RuboCop reports offenses after autocorrect, update and rerun until clean.
4548
- Keep commit messages and PR descriptions concise and informative.
@@ -103,3 +106,35 @@ i18n-tasks health
103106
```
104107

105108
See `.github/instructions/i18n-mobility.instructions.md` for additional translation rules.
109+
110+
# Testing Requirements
111+
112+
## Mandatory Test Generation
113+
- **Every code change must include RSpec tests** covering the new or modified functionality.
114+
- **Generate factories for new models** using FactoryBot with realistic Faker-generated test data.
115+
- **Test all layers**: models (validations, associations, methods), controllers (actions, authorization), services, mailers, jobs, and view components.
116+
- **JavaScript/Stimulus testing**: Include feature specs that exercise dynamic behaviors like form interactions and AJAX updates.
117+
118+
## Test Coverage Standards
119+
- **Models**: Test validations, associations, scopes, instance methods, class methods, and callbacks.
120+
- **Controllers**: Test all actions, authorization policies, parameter handling, and response formats.
121+
- **Mailers**: Test email content, recipients, localization, and delivery configurations.
122+
- **Jobs**: Test job execution, retry behavior, error handling, and side effects.
123+
- **JavaScript**: Test Stimulus controller behavior, form interactions, and dynamic content updates.
124+
- **Integration**: Test complete user workflows and cross-model interactions.
125+
126+
## Session Coverage Requirements
127+
When making changes to existing code, generate tests that cover:
128+
- All modified models and their new/changed methods, associations, and validations
129+
- Any new background jobs, mailers, and notification systems
130+
- Controller actions that handle the new functionality
131+
- JavaScript controllers and dynamic form behaviors
132+
- Integration tests for complete user workflows
133+
- Edge cases and error conditions
134+
135+
## Test Organization
136+
- Follow the existing RSpec structure and naming conventions.
137+
- Use FactoryBot factories instead of direct model creation.
138+
- Group related tests with descriptive context blocks.
139+
- Use shared examples for common behavior patterns.
140+
- Mock external dependencies and network calls.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Stimulus controller for dynamic location selection in event forms
2+
import { Controller } from "@hotwired/stimulus"
3+
4+
export default class extends Controller {
5+
static targets = [
6+
"typeSelector",
7+
"simpleLocation",
8+
"addressLocation",
9+
"buildingLocation",
10+
"addressTypeField",
11+
"buildingTypeField"
12+
]
13+
14+
connect() {
15+
// Initialize form state based on existing data
16+
this.updateVisibility()
17+
}
18+
19+
toggleLocationType(event) {
20+
const selectedType = event.target.value
21+
this.hideAllLocationTypes()
22+
23+
switch(selectedType) {
24+
case 'simple':
25+
this.showSimpleLocation()
26+
break
27+
case 'address':
28+
this.showAddressLocation()
29+
break
30+
case 'building':
31+
this.showBuildingLocation()
32+
break
33+
}
34+
}
35+
36+
hideAllLocationTypes() {
37+
if (this.hasSimpleLocationTarget) {
38+
this.simpleLocationTarget.style.display = 'none'
39+
}
40+
if (this.hasAddressLocationTarget) {
41+
this.addressLocationTarget.style.display = 'none'
42+
}
43+
if (this.hasBuildingLocationTarget) {
44+
this.buildingLocationTarget.style.display = 'none'
45+
}
46+
}
47+
48+
showSimpleLocation() {
49+
if (this.hasSimpleLocationTarget) {
50+
this.simpleLocationTarget.style.display = 'block'
51+
}
52+
// Clear structured location fields
53+
this.clearStructuredLocationFields()
54+
}
55+
56+
showAddressLocation() {
57+
if (this.hasAddressLocationTarget) {
58+
this.addressLocationTarget.style.display = 'block'
59+
}
60+
// Clear simple name field
61+
this.clearSimpleLocationFields()
62+
}
63+
64+
showBuildingLocation() {
65+
if (this.hasBuildingLocationTarget) {
66+
this.buildingLocationTarget.style.display = 'block'
67+
}
68+
// Clear simple name field
69+
this.clearSimpleLocationFields()
70+
}
71+
72+
updateAddressType(event) {
73+
if (event.target.value && this.hasAddressTypeFieldTarget) {
74+
// Type field should already be set in the hidden field
75+
}
76+
}
77+
78+
updateBuildingType(event) {
79+
if (event.target.value && this.hasBuildingTypeFieldTarget) {
80+
// Type field should already be set in the hidden field
81+
}
82+
}
83+
84+
updateVisibility() {
85+
// Show the appropriate section based on current data
86+
const checkedRadio = this.element.querySelector('input[name="location_type_selector"]:checked')
87+
if (checkedRadio) {
88+
this.toggleLocationType({ target: { value: checkedRadio.value } })
89+
} else {
90+
// Default to simple location if nothing is selected
91+
this.hideAllLocationTypes()
92+
this.showSimpleLocation()
93+
const simpleRadio = this.element.querySelector('#simple_location')
94+
if (simpleRadio) {
95+
simpleRadio.checked = true
96+
}
97+
}
98+
}
99+
100+
clearSimpleLocationFields() {
101+
const nameField = this.element.querySelector('input[name*="[name]"]')
102+
if (nameField) {
103+
nameField.value = ''
104+
}
105+
}
106+
107+
clearStructuredLocationFields() {
108+
// Clear location_id and location_type for structured locations
109+
const locationIdFields = this.element.querySelectorAll('select[name*="[location_id]"]')
110+
locationIdFields.forEach(field => {
111+
field.selectedIndex = 0
112+
})
113+
}
114+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# frozen_string_literal: true
2+
3+
module BetterTogether
4+
# Job to send event reminders to attendees
5+
class EventReminderJob < ApplicationJob
6+
queue_as :notifications
7+
8+
def perform(event, reminder_type = '24_hours')
9+
return unless event_valid?(event)
10+
11+
attendees = going_attendees(event)
12+
send_reminders_to_attendees(event, attendees, reminder_type)
13+
log_completion(event, attendees, reminder_type)
14+
end
15+
16+
private
17+
18+
def event_valid?(event)
19+
event.present? && event.starts_at.present?
20+
end
21+
22+
def going_attendees(event)
23+
event.attendees.joins(:event_attendances)
24+
.where(better_together_event_attendances: { status: 'going' })
25+
end
26+
27+
def send_reminders_to_attendees(event, attendees, reminder_type)
28+
attendees.find_each do |attendee|
29+
send_reminder_to_attendee(event, attendee, reminder_type)
30+
end
31+
end
32+
33+
def send_reminder_to_attendee(event, attendee, reminder_type)
34+
BetterTogether::EventReminderNotifier.with(
35+
event: event,
36+
reminder_type: reminder_type
37+
).deliver(attendee)
38+
rescue StandardError => e
39+
Rails.logger.error "Failed to send event reminder to #{attendee.identifier}: #{e.message}"
40+
end
41+
42+
def log_completion(event, attendees, reminder_type)
43+
Rails.logger.info "Sent #{reminder_type} reminders for event #{event.identifier} to #{attendees.count} attendees"
44+
end
45+
end
46+
end

0 commit comments

Comments
 (0)