Skip to content

Commit a4966af

Browse files
committed
Implement RSVP restrictions for draft events and enhance user feedback
- Added validation to prevent RSVPs for draft events in EventAttendance model. - Updated EventAttendancePolicy to restrict RSVP creation and updates for draft events. - Modified EventsController to redirect with an alert when attempting to RSVP to a draft event. - Enhanced show view to display appropriate messages for draft events. - Added internationalization support for new alert messages. - Created tests to ensure proper behavior for draft event restrictions.
1 parent 8acb8db commit a4966af

File tree

11 files changed

+362
-11
lines changed

11 files changed

+362
-11
lines changed

app/controllers/better_together/events_controller.rb

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,21 @@ def resource_class
7474

7575
private
7676

77-
def rsvp_update(status)
77+
# rubocop:todo Metrics/MethodLength
78+
def rsvp_update(status) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
7879
@event = set_resource_instance
7980
authorize @event, :show?
80-
attendance = BetterTogether::EventAttendance.find_or_initialize_by(event: @event, person: helpers.current_person)
81+
82+
# Check if event allows RSVP
83+
unless @event.scheduled?
84+
redirect_to @event,
85+
alert: t('better_together.events.rsvp_not_available',
86+
default: 'RSVP is not available for this event.')
87+
return
88+
end
89+
90+
attendance = BetterTogether::EventAttendance.find_or_initialize_by(event: @event,
91+
person: helpers.current_person)
8192
attendance.status = status
8293
authorize attendance
8394
if attendance.save
@@ -86,5 +97,6 @@ def rsvp_update(status)
8697
redirect_to @event, alert: attendance.errors.full_messages.to_sentence
8798
end
8899
end
100+
# rubocop:enable Metrics/MethodLength
89101
end
90102
end

app/models/better_together/event_attendance.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,16 @@ class EventAttendance < ApplicationRecord
1313

1414
validates :status, inclusion: { in: STATUS.values }
1515
validates :event_id, uniqueness: { scope: :person_id }
16+
validate :event_must_be_scheduled
17+
18+
private
19+
20+
def event_must_be_scheduled
21+
return unless event
22+
23+
return if event.scheduled?
24+
25+
errors.add(:event, 'must be scheduled to allow RSVPs')
26+
end
1627
end
1728
end

app/policies/better_together/event_attendance_policy.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ module BetterTogether
44
# Access control for event attendance (RSVPs)
55
class EventAttendancePolicy < ApplicationPolicy
66
def create?
7-
user.present?
7+
user.present? && event_allows_rsvp?
88
end
99

1010
def update?
11-
user.present? && record.person_id == agent&.id
11+
user.present? && record.person_id == agent&.id && event_allows_rsvp?
1212
end
1313

1414
alias rsvp_interested? update?
@@ -17,5 +17,15 @@ def update?
1717
def destroy?
1818
update?
1919
end
20+
21+
private
22+
23+
def event_allows_rsvp?
24+
event = record&.event || record
25+
return false unless event
26+
27+
# Don't allow RSVP for draft events (no start date)
28+
event.scheduled?
29+
end
2030
end
2131
end

app/views/better_together/events/show.html.erb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
<hr aria-hidden="true">
4747

4848
<!-- RSVP Actions -->
49-
<% if current_person %>
49+
<% if current_person && @event.scheduled? %>
5050
<% attendance = BetterTogether::EventAttendance.find_by(event: @event, person: current_person) %>
5151
<div class="container my-3">
5252
<div class="d-flex gap-2">
@@ -64,6 +64,13 @@
6464
</small>
6565
</div>
6666
</div>
67+
<% elsif current_person && @event.draft? %>
68+
<div class="my-3">
69+
<div class="alert alert-info">
70+
<i class="fas fa-info-circle me-2"></i>
71+
<%= t('better_together.events.rsvp_unavailable_draft', default: 'RSVP will be available once this event is scheduled.') %>
72+
</div>
73+
</div>
6774
<% end %>
6875

6976
<!-- Event tabbed section with members -->

config/locales/en.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,7 +915,9 @@ en:
915915
rsvp_counts: 'Going: %{going} · Interested: %{interested}'
916916
rsvp_going: Going
917917
rsvp_interested: Interested
918+
rsvp_not_available: RSVP is not available for this event.
918919
rsvp_saved: RSVP saved
920+
rsvp_unavailable_draft: RSVP will be available once this event is scheduled.
919921
save_event: Save Event
920922
tabs:
921923
details: Details

config/locales/es.yml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -914,12 +914,14 @@ es:
914914
select_building: Seleccionar edificio
915915
register: Register
916916
required: debe existir
917-
rsvp_cancel: Cancel RSVP
918-
rsvp_cancelled: RSVP cancelled
919-
rsvp_counts: 'Going: %{going} · Interested: %{interested}'
920-
rsvp_going: Going
921-
rsvp_interested: Interested
922-
rsvp_saved: RSVP saved
917+
rsvp_cancel: Cancelar RSVP
918+
rsvp_cancelled: RSVP cancelado
919+
rsvp_counts: 'Asistirán: %{going} · Interesados: %{interested}'
920+
rsvp_going: Asistiré
921+
rsvp_interested: Me interesa
922+
rsvp_not_available: RSVP no está disponible para este evento.
923+
rsvp_saved: RSVP guardado
924+
rsvp_unavailable_draft: RSVP estará disponible una vez que este evento esté programado.
923925
save_event: Save Event
924926
tabs:
925927
details: Details

config/locales/fr.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,7 +925,9 @@ fr:
925925
rsvp_counts: 'Allant : %{going} · Intéressé : %{interested}'
926926
rsvp_going: Participe
927927
rsvp_interested: Intéressé
928+
rsvp_not_available: RSVP n'est pas disponible pour cet événement.
928929
rsvp_saved: RSVP enregistré
930+
rsvp_unavailable_draft: RSVP sera disponible une fois que cet événement sera programmé.
929931
save_event: Enregistrer l'événement
930932
tabs:
931933
details: Détails
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
# Event Attendance Assessment & Improvements
2+
3+
## Overview
4+
5+
This document outlines the assessment and improvements made to the Better Together Community Engine's event attendance (RSVP) functionality, addressing under which conditions users should be able to indicate or change their attendance.
6+
7+
## Assessment of Current Implementation
8+
9+
### Original Behavior
10+
11+
The original implementation allowed users to RSVP to events with minimal restrictions:
12+
13+
- **Authentication Required**: Users must be logged in
14+
- **No Date Restrictions**: Users could RSVP to any event regardless of:
15+
- Whether the event had a start date (draft events)
16+
- Whether the event was in the past
17+
- Event scheduling status
18+
19+
### Identified Issues
20+
21+
1. **Draft Events**: Users could RSVP to unscheduled draft events, which creates confusion
22+
2. **Incomplete UX**: No clear messaging about why RSVP might not be available
23+
3. **Business Logic Gap**: Missing validation for appropriate RSVP timing
24+
25+
## Implemented Improvements
26+
27+
### 1. Draft Event Restrictions
28+
29+
**Problem**: Users could RSVP to draft events (events without `starts_at` date)
30+
31+
**Solution**:
32+
- Added UI logic to hide RSVP buttons for draft events
33+
- Added informational message explaining RSVP will be available once scheduled
34+
- Implemented server-side validation in policy and model layers
35+
36+
### 2. Enhanced User Experience
37+
38+
**Problem**: No clear feedback about RSVP availability
39+
40+
**Solution**:
41+
- Added informational alert for draft events explaining when RSVP becomes available
42+
- Added proper error messages with internationalization support
43+
- Maintained existing RSVP functionality for scheduled events
44+
45+
### 3. Multi-Layer Validation
46+
47+
**Problem**: Client-side only restrictions could be bypassed
48+
49+
**Solution**:
50+
- **View Layer**: Conditional display based on event state
51+
- **Policy Layer**: Authorization checks prevent unauthorized access
52+
- **Controller Layer**: Graceful error handling with user feedback
53+
- **Model Layer**: Data validation ensures consistency
54+
55+
## Code Changes Made
56+
57+
### 1. View Template Updates (`show.html.erb`)
58+
59+
```erb
60+
<!-- Before: Always showed RSVP if user logged in -->
61+
<% if current_person %>
62+
<!-- RSVP buttons -->
63+
<% end %>
64+
65+
<!-- After: Check if event is scheduled -->
66+
<% if current_person && @event.scheduled? %>
67+
<!-- RSVP buttons -->
68+
<% elsif current_person && @event.draft? %>
69+
<div class="alert alert-info">
70+
RSVP will be available once this event is scheduled.
71+
</div>
72+
<% end %>
73+
```
74+
75+
### 2. Policy Updates (`EventAttendancePolicy`)
76+
77+
```ruby
78+
# Added event scheduling validation
79+
def create?
80+
user.present? && event_allows_rsvp?
81+
end
82+
83+
def update?
84+
user.present? && record.person_id == agent&.id && event_allows_rsvp?
85+
end
86+
87+
private
88+
89+
def event_allows_rsvp?
90+
event = record&.event || record
91+
return false unless event
92+
event.scheduled? # Don't allow RSVP for draft events
93+
end
94+
```
95+
96+
### 3. Controller Updates (`EventsController`)
97+
98+
```ruby
99+
# Added draft event check in RSVP methods
100+
def rsvp_update(status)
101+
@event = set_resource_instance
102+
authorize @event, :show?
103+
104+
unless @event.scheduled?
105+
redirect_to @event, alert: t('better_together.events.rsvp_not_available')
106+
return
107+
end
108+
109+
# ... existing RSVP logic
110+
end
111+
```
112+
113+
### 4. Model Validation (`EventAttendance`)
114+
115+
```ruby
116+
# Added validation to prevent draft event attendance
117+
validates :event_id, uniqueness: { scope: :person_id }
118+
validate :event_must_be_scheduled
119+
120+
private
121+
122+
def event_must_be_scheduled
123+
return unless event
124+
unless event.scheduled?
125+
errors.add(:event, 'must be scheduled to allow RSVPs')
126+
end
127+
end
128+
```
129+
130+
## Recommendations for Event Attendance
131+
132+
### ✅ When Users SHOULD Be Able to RSVP:
133+
134+
1. **Scheduled Future Events**: Events with `starts_at` date in the future
135+
2. **Scheduled Current Events**: Events happening now (if still accepting RSVPs)
136+
3. **Any Scheduled Event**: As long as the event has a confirmed date/time
137+
138+
### ❌ When Users SHOULD NOT Be Able to RSVP:
139+
140+
1. **Draft Events**: Events without `starts_at` date (unscheduled)
141+
2. **Potentially Past Events**: Depending on business requirements
142+
143+
### 🤔 Considerations for Past Events:
144+
145+
The current implementation still allows RSVPs to past events. Consider these options:
146+
147+
1. **Allow RSVPs**: For record-keeping and "I attended" functionality
148+
2. **Restrict RSVPs**: To prevent confusion about future attendance
149+
3. **Different Status**: Add "attended" status for post-event interactions
150+
151+
### Future Enhancements
152+
153+
1. **Time-Based Restrictions**:
154+
- Stop RSVPs X hours before event starts
155+
- Different cutoff times for different event types
156+
157+
2. **Capacity Limits**:
158+
- Maximum attendee validation
159+
- Waitlist functionality for full events
160+
161+
3. **Event Status Integration**:
162+
- Cancelled events should block new RSVPs
163+
- Postponed events might temporarily disable RSVPs
164+
165+
4. **Enhanced UX**:
166+
- More granular status messages
167+
- Visual indicators for RSVP availability
168+
- Countdown timers for RSVP deadlines
169+
170+
## Testing Coverage
171+
172+
### New Tests Added:
173+
174+
1. **Model Validation Tests**: Verify draft events reject RSVPs
175+
2. **Policy Tests**: Ensure authorization prevents draft event RSVPs
176+
3. **Controller Tests**: Confirm proper error handling and redirects
177+
4. **Integration Tests**: End-to-end RSVP workflow validation
178+
179+
### Test Results:
180+
- All existing tests continue to pass
181+
- New restrictions properly implemented
182+
- Graceful degradation for existing data
183+
184+
## Internationalization
185+
186+
Added new translation keys:
187+
188+
```yaml
189+
better_together:
190+
events:
191+
rsvp_not_available: "RSVP is not available for this event."
192+
rsvp_unavailable_draft: "RSVP will be available once this event is scheduled."
193+
```
194+
195+
## Security Considerations
196+
197+
- All changes maintain existing authorization patterns
198+
- Multiple validation layers prevent bypass attempts
199+
- No sensitive information exposed in error messages
200+
- Existing Brakeman security scan passes without new issues
201+
202+
## Conclusion
203+
204+
The implemented changes provide a more robust and user-friendly event attendance system that:
205+
206+
1. **Prevents Confusion**: Clear restrictions on when RSVPs are available
207+
2. **Maintains Flexibility**: Existing functionality preserved for valid use cases
208+
3. **Improves UX**: Better messaging and feedback for users
209+
4. **Ensures Data Integrity**: Multi-layer validation prevents inconsistent states
210+
211+
The system now properly handles the distinction between draft and scheduled events, providing appropriate user feedback while maintaining the flexibility needed for various event management workflows.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
module BetterTogether
6+
RSpec.describe EventAttendance, 'draft event validation' do
7+
let(:person) { create(:better_together_person) }
8+
let(:draft_event) { create(:event, :draft) }
9+
let(:scheduled_event) { create(:event, :upcoming) }
10+
11+
describe 'validation for scheduled events' do
12+
it 'allows RSVP for scheduled events' do
13+
attendance = described_class.new(event: scheduled_event, person: person, status: 'interested')
14+
15+
expect(attendance).to be_valid
16+
end
17+
18+
it 'prevents RSVP for draft events' do # rubocop:todo RSpec/MultipleExpectations
19+
attendance = described_class.new(event: draft_event, person: person, status: 'interested')
20+
21+
expect(attendance).not_to be_valid
22+
expect(attendance.errors[:event]).to include('must be scheduled to allow RSVPs')
23+
end
24+
end
25+
end
26+
end

0 commit comments

Comments
 (0)