+ <% @event.yes_attendees.each do |attendance| %>
+ <%= render :partial => "users/user_card", :locals => { :user => attendance.user } %>
+ <% end %>
+
+
+ <% @event.maybe_attendees.each do |attendance| %>
+ <%= render :partial => "users/user_card", :locals => { :user => attendance.user } %>
+ <% end %>
+
+
+ <% @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 @@
<%= link_to t("layouts.communities"), communities_path, :class => "nav-link" %>
+
+ <%= link_to t("layouts.events"), events_path, :class => "nav-link" %>
+
<%= link_to t("layouts.gps_traces"), traces_path, :class => "nav-link" %>
@@ -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" %>
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