From 506495a76ecc5cc709d50c243cadf13be12d9c1c Mon Sep 17 00:00:00 2001 From: Brian DeRocher Date: Mon, 10 Oct 2022 23:50:34 -0400 Subject: [PATCH] User can rsvp 0021, 0217, 0220 Allow user to cancel their intention to attend an event. 0022, 0217, 0222? Show event attendees. Add attendees of an event, reformat some dates 0024 Translate all the things. 0055 Use .user_cards for event attendances too. 0092 Disable 1 button 0176 Add events to header. 0179 Add yes and no traits to event attendance factory. 0189 Add tests for routing. 0217 Not going to force user to belong to community in order to attend event. When using constants instead of strings, use them everywhere. 0219 Create update_params for update and add another test. Finish writing tests for EventAttendancesController. 0220 Handle plurals better 0228 If the user is not signed in, provide a link to login before having them RSVP. 0230 No need for :login_link. We just display a sentence with no params. 0231 Add maybe constant and convenience functions. Convert event attendance intention to enum. 0273 Display RSVPs as cards using a partial. User can only update their own RSVP. Use "could not be" phrasing. --- app/abilities/ability.rb | 2 + .../event_attendances_controller.rb | 40 +++++++ app/controllers/events_controller.rb | 7 ++ app/models/event.rb | 17 +++ app/models/event_attendance.rb | 29 +++++ app/views/events/show.html.erb | 33 +++++ app/views/layouts/_header.html.erb | 4 + config/locales/en.yml | 21 ++++ config/routes.rb | 1 + ...20221010234421_create_event_attendances.rb | 18 +++ db/structure.sql | 99 ++++++++++++++- .../event_attendances_controller_test.rb | 113 ++++++++++++++++++ test/factories/event_attendances.rb | 15 +++ test/models/event_attendance_test.rb | 7 ++ test/models/event_test.rb | 56 +++++++++ 15 files changed, 460 insertions(+), 2 deletions(-) create mode 100644 app/controllers/event_attendances_controller.rb create mode 100644 app/models/event_attendance.rb create mode 100644 db/migrate/20221010234421_create_event_attendances.rb create mode 100644 test/controllers/event_attendances_controller_test.rb create mode 100644 test/factories/event_attendances.rb create mode 100644 test/models/event_attendance_test.rb diff --git a/app/abilities/ability.rb b/app/abilities/ability.rb index 313e604e064..4f0baf90e4a 100644 --- a/app/abilities/ability.rb +++ b/app/abilities/ability.rb @@ -72,6 +72,8 @@ def initialize(user) can [:destroy, :edit, :update], CommunityMember, :community => user_is_community_organizer can [:destroy], CommunityMember, :user_id => user.id can [:create, :edit, :new, :update], Event, :community => user_is_community_organizer + can [:create], EventAttendance + can [:update], EventAttendance, :user_id => user.id if user.moderator? can [:hide, :hidecomment], DiaryEntry diff --git a/app/controllers/event_attendances_controller.rb b/app/controllers/event_attendances_controller.rb new file mode 100644 index 00000000000..ed02e0382aa --- /dev/null +++ b/app/controllers/event_attendances_controller.rb @@ -0,0 +1,40 @@ +class EventAttendancesController < ApplicationController + layout "site" + before_action :authorize_web + before_action :set_event_attendance, :only => [:update] + + authorize_resource + + def create + attendance = EventAttendance.new(event_attendance_params) + if attendance.save + redirect_to event_path(attendance.event), :notice => t(".success") + else + redirect_to event_path(attendance.event), :alert => t(".failure") + end + end + + def update + respond_to do |format| + if @event_attendance.update(update_params) + format.html { redirect_to @event_attendance.event, :notice => t(".success") } + else + format.html { redirect_to :edit, :alert => t(".failure") } + end + end + end + + private + + def set_event_attendance + @event_attendance = EventAttendance.find(params[:id]) + end + + def event_attendance_params + params.require(:event_attendance).permit(:event_id, :user_id, :intention) + end + + def update_params + params.require(:event_attendance).permit(:intention) + end +end diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index b0604836c5a..e5cf6a535de 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -60,6 +60,13 @@ def update # GET /events/1.json def show @community = Community.friendly.find(params[:community_id]) if params[:community_id] + @my_attendance = EventAttendance.find_or_initialize_by(:event_id => @event.id, :user_id => current_user&.id) + @yes_check = @my_attendance.intention == EventAttendance::Intentions::YES ? "✓" : "" + @no_check = @my_attendance.intention == EventAttendance::Intentions::NO ? "✓" : "" + @maybe_check = @my_attendance.intention == EventAttendance::Intentions::MAYBE ? "✓" : "" + @yes_disabled = @my_attendance.intention == EventAttendance::Intentions::YES + @no_disabled = @my_attendance.intention == EventAttendance::Intentions::NO + @maybe_disabled = @my_attendance.intention == EventAttendance::Intentions::MAYBE rescue ActiveRecord::RecordNotFound @not_found_community = params[:community_id] render :template => "communities/no_such_community", :status => :not_found diff --git a/app/models/event.rb b/app/models/event.rb index 72ed286967e..68a367bac44 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -18,6 +18,7 @@ class Event < ApplicationRecord belongs_to :community has_many :event_organizers + has_many :event_attendances scope :future, -> { where("moment >= ?", Time.now.utc) } scope :past, -> { where("moment < ?", Time.now.utc) } @@ -55,4 +56,20 @@ def organizers def past? moment < Time.now.utc end + + def attendees(intention) + EventAttendance.where(:event_id => id, :intention => intention) + end + + def yes_attendees + attendees(EventAttendance::Intentions::YES) + end + + def no_attendees + attendees(EventAttendance::Intentions::NO) + end + + def maybe_attendees + attendees(EventAttendance::Intentions::MAYBE) + end end diff --git a/app/models/event_attendance.rb b/app/models/event_attendance.rb new file mode 100644 index 00000000000..aa98e1f8d02 --- /dev/null +++ b/app/models/event_attendance.rb @@ -0,0 +1,29 @@ +# == Schema Information +# +# Table name: event_attendances +# +# id :bigint(8) not null, primary key +# user_id :integer not null +# event_id :integer not null +# intention :enum not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_event_attendances_on_event_id (event_id) +# index_event_attendances_on_user_id (user_id) +# + +class EventAttendance < ApplicationRecord + module Intentions + YES = "Yes".freeze + NO = "No".freeze + MAYBE = "Maybe".freeze + ALL_INTENTIONS = [YES, NO, MAYBE].freeze + end + validates :intention, :inclusion => { :in => Intentions::ALL_INTENTIONS } + + belongs_to :event + belongs_to :user +end diff --git a/app/views/events/show.html.erb b/app/views/events/show.html.erb index 2d89e2c312e..f0bf0dd5663 100644 --- a/app/views/events/show.html.erb +++ b/app/views/events/show.html.erb @@ -23,7 +23,22 @@

+

<%= t(".people_are_going", :count => @event.yes_attendees.size) %>

<%= t(".are_you_going") %>

+ <% if current_user %> + <%= form_with :model => @my_attendance do |form| %> + <%= form.hidden_field(:event_id, :value => @event.id) %> + <%= form.hidden_field(:user_id, :value => current_user&.id) %> + <%= form.submit :name => "event_attendance[intention]", :value => @yes_check + t(".going_yes"), :disabled => @yes_disabled %> + <%= form.submit :name => "event_attendance[intention]", :value => @no_check + t(".going_no"), :disabled => @no_disabled %> + <%= form.submit :name => "event_attendance[intention]", :value => @maybe_check + t(".going_maybe"), :disabled => @maybe_disabled %> + <% end %> + <% else %> +

+ <%# TODO: Use login_path %> + <%= t(".login_to_rsvp") %> +

+ <% end %>

<%= @event.title %> @@ -56,6 +71,24 @@ <%= t(".description") %>: <%= @event.description %>

+ <%= t(".who_yes") %> +
+ <% @event.yes_attendees.each do |attendance| %> + <%= render :partial => "users/user_card", :locals => { :user => attendance.user } %> + <% end %> +
+ <%= t(".who_maybe") %> +
+ <% @event.maybe_attendees.each do |attendance| %> + <%= render :partial => "users/user_card", :locals => { :user => attendance.user } %> + <% end %> +
+ <%= t(".who_no") %> +
+ <% @event.no_attendees.each do |attendance| %> + <%= render :partial => "users/user_card", :locals => { :user => attendance.user } %> + <% end %> +

diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb index 8b741b28c1d..75999b1696b 100644 --- a/app/views/layouts/_header.html.erb +++ b/app/views/layouts/_header.html.erb @@ -47,6 +47,9 @@

+ @@ -77,6 +80,7 @@ <% end %>
  • <%= link_to t("layouts.communities"), communities_path, :class => "dropdown-item" %>
  • +
  • <%= link_to t("layouts.events"), events_path, :class => "dropdown-item" %>
  • <%= link_to t("layouts.gps_traces"), traces_path, :class => "dropdown-item" %>
  • <%= link_to t("layouts.user_diaries"), diary_entries_path, :class => "dropdown-item" %>
  • <%= link_to t("layouts.communities"), community_index_path, :class => "dropdown-item" %>
  • diff --git a/config/locales/en.yml b/config/locales/en.yml index 5a3a7ea5fca..b41d2379d5c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -699,6 +699,13 @@ en: not_found: title: File not found description: Couldn't find a file/directory/API operation by that name on the OpenStreetMap server (HTTP 404) + event_attendances: + create: + success: Attendance was successfully saved. + failure: Attendance could not be saved. + update: + success: Attendance was successfully updated. + failure: Attendance could not be updated. events: create: success: Event was created successfully. @@ -724,11 +731,22 @@ en: description: "Description" directions_to: "Directions to this location." edit: "Edit" + going_maybe: "Maybe" + going_no: "No" + going_yes: "Yes" hosted_by: "Hosted by" location: "Location" + login_to_rsvp: "Login to RSVP." organized_by: "Organized by" past: "Event is in the past." + people_are_going: + zero: "" + one: "One person is going." + other: "%{count} people are going" when: "When" + who_yes: "Going" + who_no: "Not Going" + who_maybe: "Might Go" update: success: "The event was successfully updated." failure: "The event was not updated." @@ -1644,6 +1662,9 @@ en: home: Go to Home Location logout: Log Out log_in: Log In + log_in_tooltip: Log in with an existing account + communities: Communities + events: Events sign_up: Sign Up start_mapping: Start Mapping edit: Edit diff --git a/config/routes.rb b/config/routes.rb index 053009ba6aa..992feec49d9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -339,6 +339,7 @@ resources :community_members, :only => [:create, :destroy, :edit, :new, :update] get "/community_members" => "community_members#create", :as => "login_to_join" resources :events + resources :event_attendances # errors match "/403", :to => "errors#forbidden", :via => :all diff --git a/db/migrate/20221010234421_create_event_attendances.rb b/db/migrate/20221010234421_create_event_attendances.rb new file mode 100644 index 00000000000..213199034bf --- /dev/null +++ b/db/migrate/20221010234421_create_event_attendances.rb @@ -0,0 +1,18 @@ +class CreateEventAttendances < ActiveRecord::Migration[7.0] + def up + create_enumeration :event_attendances_intention_enum, %w[Maybe No Yes] + create_table :event_attendances do |t| + t.references :user, :foreign_key => true, :null => false, :index => true + t.references :event, :foreign_key => true, :null => false, :index => true + t.column :intention, :event_attendances_intention_enum, :null => false + + t.timestamps + end + add_index :event_attendances, [:user_id, :event_id], :unique => true + end + + def down + drop_table :event_attendances + drop_enumeration :event_attendances_intention_enum + end +end diff --git a/db/structure.sql b/db/structure.sql index c8d207ccd1f..adb0cbfe2f9 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -16,6 +16,17 @@ SET row_security = off; CREATE EXTENSION IF NOT EXISTS btree_gist WITH SCHEMA public; +-- +-- Name: event_attendances_intention_enum; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.event_attendances_intention_enum AS ENUM ( + 'Maybe', + 'No', + 'Yes' + ); + + -- -- Name: format_enum; Type: TYPE; Schema: public; Owner: - -- @@ -110,8 +121,6 @@ CREATE TYPE public.user_status_enum AS ENUM ( SET default_tablespace = ''; -SET default_table_access_method = heap; - -- -- Name: acls; Type: TABLE; Schema: public; Owner: - -- @@ -777,6 +786,39 @@ CREATE TABLE public.diary_entry_subscriptions ( ); +-- +-- Name: event_attendances; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.event_attendances ( + id bigint NOT NULL, + user_id bigint NOT NULL, + event_id bigint NOT NULL, + intention public.event_attendances_intention_enum NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: event_attendances_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.event_attendances_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: event_attendances_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.event_attendances_id_seq OWNED BY public.event_attendances.id; + + -- -- Name: event_organizers; Type: TABLE; Schema: public; Owner: - -- @@ -1852,6 +1894,13 @@ ALTER TABLE ONLY public.diary_comments ALTER COLUMN id SET DEFAULT nextval('publ ALTER TABLE ONLY public.diary_entries ALTER COLUMN id SET DEFAULT nextval('public.diary_entries_id_seq'::regclass); +-- +-- Name: event_attendances id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.event_attendances ALTER COLUMN id SET DEFAULT nextval('public.event_attendances_id_seq'::regclass); + + -- -- Name: event_organizers id; Type: DEFAULT; Schema: public; Owner: - -- @@ -2190,6 +2239,14 @@ ALTER TABLE ONLY public.diary_entry_subscriptions ADD CONSTRAINT diary_entry_subscriptions_pkey PRIMARY KEY (user_id, diary_entry_id); +-- +-- Name: event_attendances event_attendances_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.event_attendances + ADD CONSTRAINT event_attendances_pkey PRIMARY KEY (id); + + -- -- Name: event_organizers event_organizers_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2762,6 +2819,27 @@ CREATE INDEX index_community_members_on_user_id ON public.community_members USIN CREATE INDEX index_diary_entry_subscriptions_on_diary_entry_id ON public.diary_entry_subscriptions USING btree (diary_entry_id); +-- +-- Name: index_event_attendances_on_event_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_event_attendances_on_event_id ON public.event_attendances USING btree (event_id); + + +-- +-- Name: index_event_attendances_on_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_event_attendances_on_user_id ON public.event_attendances USING btree (user_id); + + +-- +-- Name: index_event_attendances_on_user_id_and_event_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_event_attendances_on_user_id_and_event_id ON public.event_attendances USING btree (user_id, event_id); + + -- -- Name: index_event_organizers_on_event_id; Type: INDEX; Schema: public; Owner: - -- @@ -3383,6 +3461,14 @@ ALTER TABLE ONLY public.events ADD CONSTRAINT fk_rails_3451eeb877 FOREIGN KEY (community_id) REFERENCES public.communities(id); +-- +-- Name: event_attendances fk_rails_64ad6920ae; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.event_attendances + ADD CONSTRAINT fk_rails_64ad6920ae FOREIGN KEY (user_id) REFERENCES public.users(id); + + -- -- Name: oauth_access_tokens fk_rails_732cb83ab7; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -3447,6 +3533,14 @@ ALTER TABLE ONLY public.oauth_applications ADD CONSTRAINT fk_rails_cc886e315a FOREIGN KEY (owner_id) REFERENCES public.users(id) NOT VALID; +-- +-- Name: event_attendances fk_rails_d082d0d206; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.event_attendances + ADD CONSTRAINT fk_rails_d082d0d206 FOREIGN KEY (event_id) REFERENCES public.events(id); + + -- -- Name: oauth_access_tokens fk_rails_ee63f25419; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -3842,6 +3936,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20220925043305'), ('20221008144036'), ('20221008224134'), +('20221010234421'), ('21'), ('22'), ('23'), diff --git a/test/controllers/event_attendances_controller_test.rb b/test/controllers/event_attendances_controller_test.rb new file mode 100644 index 00000000000..13956517616 --- /dev/null +++ b/test/controllers/event_attendances_controller_test.rb @@ -0,0 +1,113 @@ +require "test_helper" + +class EventAttendancesControllerTest < ActionDispatch::IntegrationTest + def test_routes + assert_routing( + { :path => "/event_attendances/1", :method => :put }, + { :controller => "event_attendances", :action => "update", :id => "1" } + ) + assert_routing( + { :path => "/event_attendances", :method => :post }, + { :controller => "event_attendances", :action => "create" } + ) + end + + def test_create_when_save_works + # arrange + cm = create(:community_member) + ev = create(:event, :community => cm.community) + ea_orig = build(:event_attendance, :user => cm.user, :event => ev) + form = ea_orig.attributes.except("id", "created_at", "updated_at") + session_for(cm.user) + + # act + assert_difference "EventAttendance.count", 1 do + post event_attendances_url, :params => { :event_attendance => form.as_json }, :xhr => true + end + + # assert + assert_redirected_to event_path(ev) + ea_new_id = EventAttendance.maximum(:id) + assert_equal I18n.t("event_attendances.create.success"), flash[:notice] + ea_new = EventAttendance.find(ea_new_id) + # Assign the new id to the original object, so we can do an equality test easily. + ea_orig.id = ea_new.id + assert_equal(ea_orig, ea_new) + end + + def test_update_as_wrong_user + # arrange + cm = create(:community_member) + ev = create(:event, :community => cm.community) + ea1 = create(:event_attendance, :user => cm.user, :event => ev) # original object + u2 = create(:user) + ea2 = build(:event_attendance, :user => u2, :event => ev) # new data + form = ea2.attributes.except("id", "created_at", "updated_at") + session_for(u2) + + # act + # Update ea1 with the values from ea2. + put event_attendance_url(ea1), :params => { :event_attendance => form }, :xhr => true + + # assert + follow_redirect! + assert_response :forbidden + end + + def test_create_when_save_fails + # arrange + cm = create(:community_member, :organizer) + session_for(cm.user) + + e = create(:event, :community => cm.community) + ea = build(:event_attendance, :event => e, :user => cm.user, :intention => "Invalid") + form = ea.attributes.except("id", "created_at", "updated_at") + + # act and assert + assert_no_difference "EventAttendance.count", 0 do + post event_attendances_path, :params => { :event_attendance => form } + end + end + + def test_update_success + # arrange + cm = create(:community_member) + ev = create(:event, :community => cm.community) + ea1 = create(:event_attendance, :user => cm.user, :event => ev, :intention => "Yes") # original object + ea2 = build(:event_attendance, :user => cm.user, :event => ev, :intention => "No") # new data + form = ea2.attributes.except("id", "created_at", "updated_at") + session_for(cm.user) + + # act + # Update m1 with the values from m2. + put event_attendance_url(ea1), :params => { :event_attendance => form.as_json }, :xhr => true + + # assert + assert_redirected_to event_path(ev) + assert_equal I18n.t("event_attendances.update.success"), flash[:notice] + ea1.reload + # Assign the id of object 1 to object 2, so we can do an equality test easily. + ea2.id = ea1.id + assert_equal(ea2, ea1) + end + + def test_update_when_save_fails + # arrange + cm = create(:community_member) + ev = create(:event, :community => cm.community) + session_for(cm.user) + + ea = create(:event_attendance, :user => cm.user, :event => ev) # original object + form = ea.attributes.except("id", "created_at", "updated_at") + form["intention"] = "Invalid" # Force "save" to fail. + + # act + assert_difference "EventAttendance.count", 0 do + put event_attendance_url(ea), :params => { :event_attendance => form.as_json }, :xhr => true + end + + # assert + follow_redirect! + assert_equal I18n.t("event_attendances.update.failure"), flash[:alert] + end +end diff --git a/test/factories/event_attendances.rb b/test/factories/event_attendances.rb new file mode 100644 index 00000000000..89c1336c911 --- /dev/null +++ b/test/factories/event_attendances.rb @@ -0,0 +1,15 @@ +FactoryBot.define do + factory :event_attendance do + user + event + intention { "Maybe" } + + trait :no do + intention { "No" } + end + + trait :yes do + intention { "Yes" } + end + end +end diff --git a/test/models/event_attendance_test.rb b/test/models/event_attendance_test.rb new file mode 100644 index 00000000000..dd8c492497c --- /dev/null +++ b/test/models/event_attendance_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class EventAttendanceTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/event_test.rb b/test/models/event_test.rb index 537ec6dc1fd..bb63a9ae68a 100644 --- a/test/models/event_test.rb +++ b/test/models/event_test.rb @@ -33,4 +33,60 @@ def test_validations validate({ :longitude => -180 }, true) validate({ :longitude => -180.00001 }, false) end + + def test_attendees + # Zero + event = create(:event) + assert_equal(0, event.maybe_attendees.length) + assert_equal(0, event.no_attendees.length) + assert_equal(0, event.yes_attendees.length) + + # 1 Maybe + ea = create(:event_attendance) + assert_equal(1, ea.event.maybe_attendees.length) + assert_equal(0, ea.event.no_attendees.length) + assert_equal(0, ea.event.yes_attendees.length) + assert_equal("Maybe", ea.event.event_attendances[0].intention) + assert_equal(ea.user.display_name, ea.event.event_attendances[0].user.display_name) + + # 1 No + ea = create(:event_attendance, :no) + assert_equal(0, ea.event.maybe_attendees.length) + assert_equal(1, ea.event.no_attendees.length) + assert_equal(0, ea.event.yes_attendees.length) + assert_equal("No", ea.event.event_attendances[0].intention) + assert_equal(ea.user.display_name, ea.event.event_attendances[0].user.display_name) + + # 1 Yes + ea = create(:event_attendance, :yes) + assert_equal(0, ea.event.maybe_attendees.length) + assert_equal(0, ea.event.no_attendees.length) + assert_equal(1, ea.event.yes_attendees.length) + assert_equal("Yes", ea.event.event_attendances[0].intention) + assert_equal(ea.user.display_name, ea.event.event_attendances[0].user.display_name) + + # 2 Yes + event = create(:event) + u1 = create(:user) + ea1 = EventAttendance.new(:event => event, :user => u1, :intention => EventAttendance::Intentions::YES) + ea1.save + u2 = create(:user) + ea2 = EventAttendance.new(:event => event, :user => u2, :intention => EventAttendance::Intentions::YES) + ea2.save + assert_equal(2, event.yes_attendees.length) + assert_equal(u1.display_name, event.yes_attendees[0].user.display_name) + assert_equal(u2.display_name, event.yes_attendees[1].user.display_name) + + # 1 Yes and 1 No + event = create(:event) + u1 = create(:user) + ea1 = EventAttendance.new(:event => event, :user => u1, :intention => EventAttendance::Intentions::NO) + ea1.save + u2 = create(:user) + ea2 = EventAttendance.new(:event => event, :user => u2, :intention => EventAttendance::Intentions::YES) + ea2.save + assert_equal(1, event.yes_attendees.length) + assert_equal(1, event.no_attendees.length) + assert_equal(u2.display_name, event.yes_attendees[0].user.display_name) + end end