+
+ <%= link_to t(".events"), community_community_events_path(@community) %>
+
+
+ <% if current_user && @community.organizer?(current_user) %>
+ <%= link_to t(".new_event"), new_event_path(:community_id => @community.id) %>
+ <% end %>
+
+
+ <% @community.events.future.each do |event| %>
+ -
+ <%= link_to "#{l(event.moment, :format => '%e %B')} - #{event.title}", community_event_path(@community, event) %>
+
+ <% end %>
+
<%= t(".recent_changes") %>
diff --git a/app/views/events/_event_property.html.erb b/app/views/events/_event_property.html.erb
new file mode 100644
index 0000000000..1162451dfa
--- /dev/null
+++ b/app/views/events/_event_property.html.erb
@@ -0,0 +1,4 @@
+
+ <%= name %> |
+ <%= value %> |
+
diff --git a/app/views/events/_form.html.erb b/app/views/events/_form.html.erb
new file mode 100644
index 0000000000..c3a09ff2c7
--- /dev/null
+++ b/app/views/events/_form.html.erb
@@ -0,0 +1,48 @@
+<%= javascript_include_tag "event" %>
+
+<%= bootstrap_form_for(@event) do |form| %>
+ <% if event.errors.any? %>
+
+
<%= pluralize(event.errors.count, "error") %> prohibited this event from being saved:
+
+ <% event.errors.full_messages.each do |message| %>
+ - <%= message %>
+ <% end %>
+
+
+ <% end %>
+
+ <%= form.text_field :title, :id => :event_title %>
+
+
+ <%= form.datetime_local_field :moment, :pattern => "[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}" %>
+
+
+ <%= form.text_field :location, :id => :event_location %>
+
+
+ <%= form.text_field :location_url, :id => :event_location_url %>
+
+
+ <%= form.text_area :description, :id => :event_description %>
+
+
+ <% if @event&.community_id %>
+ <%= form.hidden_field(:community_id, :value => @event.community_id) %>
+ <% else %>
+
+ <%= collection_select(:event, :community_id, Community.all, :id, :name, :prompt => true) %>
+
+ <% end %>
+ <%= form.primary %>
+<% end %>
diff --git a/app/views/events/_index_list.html.erb b/app/views/events/_index_list.html.erb
new file mode 100644
index 0000000000..6dd3d3055d
--- /dev/null
+++ b/app/views/events/_index_list.html.erb
@@ -0,0 +1,25 @@
+<% if !events.empty? %>
+
+ <%= header %>
+
+
+
+
+ <%= t(".moment") %> |
+ <%= t(".title") %> |
+ <%= t(".location") %> |
+ <%= t(".community") %> |
+
+
+
+ <% events.each do |event| %>
+
+ <%= l(event.moment, :format => :blog) %> |
+ <%= link_to event.title, event %> |
+ <%= event.location %> |
+ <%= link_to event.community.name, community_path(event.community) %> |
+
+ <% end %>
+
+
+<% end %>
diff --git a/app/views/events/edit.html.erb b/app/views/events/edit.html.erb
new file mode 100644
index 0000000000..82a4785900
--- /dev/null
+++ b/app/views/events/edit.html.erb
@@ -0,0 +1,3 @@
+
<%= t(".edit_event") %>
+
+<%= render "form", :event => @event %>
diff --git a/app/views/events/index.html.erb b/app/views/events/index.html.erb
new file mode 100644
index 0000000000..9a1aa804ab
--- /dev/null
+++ b/app/views/events/index.html.erb
@@ -0,0 +1,20 @@
+<% content_for :heading do %>
+
<%= @community&.name %> <%= t(".events") %>
+
+<% end %>
+
+
<%= notice %>
+
+<%= render :partial => "index_list", :locals => { :events => @events.future, :header => t(".upcoming_events") } %>
+<%= render :partial => "index_list", :locals => { :events => @events.past, :header => t(".past_events") } %>
diff --git a/app/views/events/new.html.erb b/app/views/events/new.html.erb
new file mode 100644
index 0000000000..e7c961aff0
--- /dev/null
+++ b/app/views/events/new.html.erb
@@ -0,0 +1,7 @@
+<% content_for :heading do %>
+
+ <%= @title %>
+
+<% end %>
+
+<%= render "form", :event => @event %>
diff --git a/app/views/events/show.html.erb b/app/views/events/show.html.erb
new file mode 100644
index 0000000000..dbc9533e2b
--- /dev/null
+++ b/app/views/events/show.html.erb
@@ -0,0 +1,81 @@
+<%= javascript_include_tag "event" %>
+
+<% content_for :heading do %>
+
+ <%= @event.title %>
+
+ <% if current_user && @community&.organizer?(current_user) %>
+ <%= link_to t(".edit"), edit_event_path(@event) %>
+ <% end %>
+<% end %>
+
+
+
+
+
+ <%= t(".hosted_by") %>: <%= link_to @event.community.name, community_path(@event.community) %>
+
+
+ <%= t(".organized_by") %>: <% @event.organizers.each do |organizer| %>
+ <%= link_to organizer.user.display_name, user_path(organizer.user) %>
+ <% end %>
+
+
+
+ <%= render :partial => "events/event_property", :locals => { :name => t(".when"), :value => l(@event.moment, :format => :friendly) } %>
+ <%= render :partial => "events/event_property", :locals => { :name => t(".location"), :value => event_location(@event) } %>
+ <%= render :partial => "events/event_property", :locals => { :name => t(".description"), :value => @event.description } %>
+
+
+
+
<%= 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 %>
+
+
+
+ <%# TODO: replace these attributes @map_coords %>
+ <%= tag.div :class => "content_map", :id => "event_map_show", :data => { :lat => @event.latitude, :lon => @event.longitude, :zoom => 11 } %>
+
+ <%= t(".directions_to") %>
+
+
+
+
+
+
+
+
<%= 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 1abcb8e329..dfa51829a4 100644
--- a/app/views/layouts/_header.html.erb
+++ b/app/views/layouts/_header.html.erb
@@ -72,7 +72,7 @@
<% end %>
<%= 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"), communities_path, :class => "dropdown-item" %>
+
<%= link_to t("layouts.communities"), communities_index_path, :class => "dropdown-item" %>
<%= link_to t("layouts.copyright"), copyright_path, :class => "dropdown-item" %>
<%= link_to t("layouts.help"), help_path, :class => "dropdown-item" %>
<%= link_to t("layouts.about"), about_path, :class => "dropdown-item" %>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 2240c93879..445e9a91ec 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -25,12 +25,22 @@ en:
community_id: "Community"
user_id: "User"
role: "Role"
+ event:
+ description: "Description"
+ location: "Location"
+ location_url: "Location URL"
+ community_id: "Community"
+ moment: "When"
+ title: "Title"
submit:
diary_comment:
create: Comment
diary_entry:
create: "Publish"
update: "Update"
+ event:
+ create: "Create Event"
+ update: "Update Event"
issue_comment:
create: Add Comment
message:
@@ -568,6 +578,7 @@ en:
show:
diary_entries: "Diary Entries of Members"
edit: "Edit"
+ events: "Events"
header_title: "Community"
join:
action: "Join"
@@ -578,6 +589,7 @@ en:
links: "Links"
login_to_join: "Please login to join the community."
members: "Members"
+ new_event: "new event"
organizers: "Organizers"
recent_changes: "Recent Changes"
report: "Report"
@@ -762,6 +774,58 @@ 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.
+ failure: Event was not created.
+ edit:
+ edit_event: Edit Event
+ index:
+ description: "Description"
+ events: "Events"
+ new_event: "New Event"
+ past_events: "Past Events"
+ show: "Show"
+ upcoming_events: "Upcoming Events"
+ index_list:
+ community: "Community"
+ location: "Location"
+ moment: "When"
+ title: "Title"
+ new:
+ new: "New Event"
+ show:
+ are_you_going: "Are you going?"
+ 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:
+ 0: "" # TODO: Not working, but can't use "zero" due to lint.
+ 1: "1 person is going." # TODO: Not working.
+ one: "1 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."
friendships:
make_friend:
heading: "Add %{user} as a friend?"
@@ -1673,6 +1737,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
@@ -3426,4 +3493,6 @@ en:
leading_whitespace: "has leading whitespace"
trailing_whitespace: "has trailing whitespace"
invalid_characters: "contains invalid characters"
+ invalid_datetime_format: "contains invalid datetime format, use yyyy-mm-ddThh:mm"
+ invalid_datetime_range: "contains invalid datetime range"
url_characters: "contains special URL characters (%{characters})"
diff --git a/config/routes.rb b/config/routes.rb
index b5d58de721..acfe0e2983 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -347,12 +347,17 @@
# communities
resources :communities do
resources :community_links, :only => [:create, :index, :new]
+ # TODO: Shorten these path names, like :event.
get :community_members, :to => "community_members#index"
+ get :community_events, :to => "events#index"
+ resources :events, :only => [:show]
end
post "/communities/:id/step_up" => "communities#step_up", :as => :step_up, :id => /\d+/
resources :community_links, :only => [:destroy, :edit, :update]
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 "/400", :to => "errors#bad_request", :via => :all
diff --git a/db/migrate/20240728144036_create_events.rb b/db/migrate/20240728144036_create_events.rb
new file mode 100644
index 0000000000..20beffea09
--- /dev/null
+++ b/db/migrate/20240728144036_create_events.rb
@@ -0,0 +1,16 @@
+class CreateEvents < ActiveRecord::Migration[7.0]
+ def change
+ create_table :events do |t|
+ t.string :title, :null => false
+ t.datetime :moment, :null => false
+ t.string :location, :null => false
+ t.string :location_url
+ t.float :latitude
+ t.float :longitude
+ t.text :description, :null => false
+ t.references :community, :null => false, :foreign_key => true, :index => true
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20240728224134_create_event_organizers.rb b/db/migrate/20240728224134_create_event_organizers.rb
new file mode 100644
index 0000000000..3d37351631
--- /dev/null
+++ b/db/migrate/20240728224134_create_event_organizers.rb
@@ -0,0 +1,10 @@
+class CreateEventOrganizers < ActiveRecord::Migration[7.0]
+ def change
+ create_table :event_organizers do |t|
+ t.references :event, :foreign_key => true, :null => false, :index => true
+ t.references :user, :foreign_key => true, :null => false, :index => true
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20240730234421_create_event_attendances.rb b/db/migrate/20240730234421_create_event_attendances.rb
new file mode 100644
index 0000000000..c21a584ea7
--- /dev/null
+++ b/db/migrate/20240730234421_create_event_attendances.rb
@@ -0,0 +1,18 @@
+class CreateEventAttendances < ActiveRecord::Migration[7.0]
+ def up
+ create_enum :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_enum :event_attendances_intention_enum
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 70daae3a32..6393e58c32 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -33,6 +33,17 @@ CREATE TYPE public.community_member_role_enum AS ENUM (
);
+--
+-- 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: -
--
@@ -912,6 +923,109 @@ 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: -
+--
+
+CREATE TABLE public.event_organizers (
+ id bigint NOT NULL,
+ event_id bigint NOT NULL,
+ user_id bigint NOT NULL,
+ created_at timestamp(6) without time zone NOT NULL,
+ updated_at timestamp(6) without time zone NOT NULL
+);
+
+
+--
+-- Name: event_organizers_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE public.event_organizers_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: event_organizers_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE public.event_organizers_id_seq OWNED BY public.event_organizers.id;
+
+
+--
+-- Name: events; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.events (
+ id bigint NOT NULL,
+ title character varying NOT NULL,
+ moment timestamp(6) without time zone NOT NULL,
+ location character varying NOT NULL,
+ location_url character varying,
+ latitude double precision,
+ longitude double precision,
+ description text NOT NULL,
+ community_id bigint NOT NULL,
+ created_at timestamp(6) without time zone NOT NULL,
+ updated_at timestamp(6) without time zone NOT NULL
+);
+
+
+--
+-- Name: events_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE public.events_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: events_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE public.events_id_seq OWNED BY public.events.id;
+
+
--
-- Name: friendly_id_slugs; Type: TABLE; Schema: public; Owner: -
--
@@ -1947,6 +2061,27 @@ 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: -
+--
+
+ALTER TABLE ONLY public.event_organizers ALTER COLUMN id SET DEFAULT nextval('public.event_organizers_id_seq'::regclass);
+
+
+--
+-- Name: events id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.events ALTER COLUMN id SET DEFAULT nextval('public.events_id_seq'::regclass);
+
+
--
-- Name: friendly_id_slugs id; Type: DEFAULT; Schema: public; Owner: -
--
@@ -2286,6 +2421,30 @@ 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: -
+--
+
+ALTER TABLE ONLY public.event_organizers
+ ADD CONSTRAINT event_organizers_pkey PRIMARY KEY (id);
+
+
+--
+-- Name: events events_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.events
+ ADD CONSTRAINT events_pkey PRIMARY KEY (id);
+
+
--
-- Name: friendly_id_slugs friendly_id_slugs_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -2857,6 +3016,48 @@ 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: -
+--
+
+CREATE INDEX index_event_organizers_on_event_id ON public.event_organizers USING btree (event_id);
+
+
+--
+-- Name: index_event_organizers_on_user_id; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_event_organizers_on_user_id ON public.event_organizers USING btree (user_id);
+
+
+--
+-- Name: index_events_on_community_id; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_events_on_community_id ON public.events USING btree (community_id);
+
+
--
-- Name: index_friendly_id_slugs_on_slug_and_sluggable_type; Type: INDEX; Schema: public; Owner: -
--
@@ -3456,6 +3657,14 @@ ALTER TABLE ONLY public.oauth_access_grants
ADD CONSTRAINT fk_rails_330c32d8d9 FOREIGN KEY (resource_owner_id) REFERENCES public.users(id) NOT VALID;
+--
+-- Name: events fk_rails_3451eeb877; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.events
+ ADD CONSTRAINT fk_rails_3451eeb877 FOREIGN KEY (community_id) REFERENCES public.communities(id);
+
+
--
-- Name: user_mutes fk_rails_591dad3359; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -3464,6 +3673,14 @@ ALTER TABLE ONLY public.user_mutes
ADD CONSTRAINT fk_rails_591dad3359 FOREIGN KEY (owner_id) REFERENCES public.users(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: -
--
@@ -3496,6 +3713,14 @@ ALTER TABLE ONLY public.active_storage_variant_records
ADD CONSTRAINT fk_rails_993965df05 FOREIGN KEY (blob_id) REFERENCES public.active_storage_blobs(id);
+--
+-- Name: event_organizers fk_rails_b1c2c61554; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.event_organizers
+ ADD CONSTRAINT fk_rails_b1c2c61554 FOREIGN KEY (user_id) REFERENCES public.users(id);
+
+
--
-- Name: oauth_access_grants fk_rails_b4b53e07b8; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -3504,6 +3729,14 @@ ALTER TABLE ONLY public.oauth_access_grants
ADD CONSTRAINT fk_rails_b4b53e07b8 FOREIGN KEY (application_id) REFERENCES public.oauth_applications(id) NOT VALID;
+--
+-- Name: event_organizers fk_rails_c1e082c91e; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.event_organizers
+ ADD CONSTRAINT fk_rails_c1e082c91e FOREIGN KEY (event_id) REFERENCES public.events(id);
+
+
--
-- Name: active_storage_attachments fk_rails_c3b3935057; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -3520,6 +3753,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: user_mutes fk_rails_e9dd4fb6c3; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -3883,6 +4124,9 @@ INSERT INTO "schema_migrations" (version) VALUES
('23'),
('22'),
('21'),
+('20240730234421'),
+('20240728224134'),
+('20240728144036'),
('20240618193051'),
('20240605134916'),
('20240605043305'),
diff --git a/test/controllers/event_attendances_controller_test.rb b/test/controllers/event_attendances_controller_test.rb
new file mode 100644
index 0000000000..e9f6c8f222
--- /dev/null
+++ b/test/controllers/event_attendances_controller_test.rb
@@ -0,0 +1,98 @@
+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
+ 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)
+
+ assert_difference "EventAttendance.count", 1 do
+ post event_attendances_url, :params => { :event_attendance => form.as_json }, :xhr => true
+ end
+
+ 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
+ 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)
+
+ # Update ea1 with the values from ea2.
+ put event_attendance_url(ea1), :params => { :event_attendance => form }, :xhr => true
+
+ assert_redirected_to :controller => :errors, :action => :forbidden
+ end
+
+ def test_create_when_save_fails
+ 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")
+
+ assert_no_difference "EventAttendance.count", 0 do
+ post event_attendances_path, :params => { :event_attendance => form }
+ end
+ end
+
+ def test_update_success
+ 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)
+
+ # Update m1 with the values from m2.
+ put event_attendance_url(ea1), :params => { :event_attendance => form.as_json }, :xhr => true
+
+ 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
+ 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.
+
+ assert_difference "EventAttendance.count", 0 do
+ put event_attendance_url(ea), :params => { :event_attendance => form.as_json }, :xhr => true
+ end
+
+ follow_redirect!
+ assert_equal I18n.t("event_attendances.update.failure"), flash[:alert]
+ end
+end
diff --git a/test/controllers/events_controller_test.rb b/test/controllers/events_controller_test.rb
new file mode 100644
index 0000000000..4b33e77472
--- /dev/null
+++ b/test/controllers/events_controller_test.rb
@@ -0,0 +1,260 @@
+require "test_helper"
+require "minitest/mock"
+
+class EventsControllerTest < ActionDispatch::IntegrationTest
+ def test_routes
+ assert_routing(
+ { :path => "/events", :method => :get },
+ { :controller => "events", :action => "index" }
+ )
+ assert_routing(
+ { :path => "/events/new", :method => :get },
+ { :controller => "events", :action => "new" }
+ )
+ assert_routing(
+ { :path => "/events", :method => :post },
+ { :controller => "events", :action => "create" }
+ )
+ assert_routing(
+ { :path => "/events/1", :method => :get },
+ { :controller => "events", :action => "show", :id => "1" }
+ )
+ assert_routing(
+ { :path => "/events/1/edit", :method => :get },
+ { :controller => "events", :action => "edit", :id => "1" }
+ )
+ assert_routing(
+ { :path => "/events/1", :method => :patch },
+ { :controller => "events", :action => "update", :id => "1" }
+ )
+ # No ability in cancancan yet.
+ # assert_routing(
+ # { :path => "/events/1", :method => :delete },
+ # { :controller => "events", :action => "destroy", :id => "1" }
+ # )
+ end
+
+ def test_index_get_future
+ e = create(:event)
+
+ get events_path
+
+ assert_response :success
+ assert_template "index"
+ assert_match e.title, response.body
+ end
+
+ def test_index_get_past
+ e = create(:event, :moment => Time.now.utc - 2000)
+
+ get events_path
+
+ assert_response :success
+ assert_template "index"
+ assert_match e.title, response.body
+ end
+
+ def test_index_of_community
+ c = create(:community)
+ e = create(:event, :community => c)
+
+ get community_community_events_path(c)
+
+ assert_response :success
+ assert_template "index"
+ assert_match e.title, response.body
+ end
+
+ def test_index_community_does_not_exist
+ get community_community_events_path("dne")
+
+ assert_response :not_found
+ assert_template "communities/no_such_community"
+ end
+
+ def test_show_get
+ e = create(:event)
+
+ get event_path(e)
+
+ assert_response :success
+ # assert_template("show")
+ assert_match e.title, response.body
+ assert_match e.description, response.body
+ assert_match e.location, response.body
+ end
+
+ def test_new_no_login
+ # Make sure that you are redirected to the login page when you
+ # are not logged in.
+
+ # There must be community to build an event against.
+ c = create(:community)
+ params = { :event => { :community_id => c.id } }
+ get new_event_path(params)
+
+ assert_response :redirect
+ assert_redirected_to login_path(:referer => new_event_path(:params => params))
+ end
+
+ def test_new_form
+ # Now try again when logged in.
+ # There must be community to build an event against.
+ cm = create(:community_member, :organizer)
+ session_for(cm.user)
+
+ get new_event_path(:event => { :community_id => cm.community_id })
+
+ assert_response :success
+ assert_select "div.content-heading", :count => 1 do
+ assert_select "h1", :text => /New Event/, :count => 1
+ end
+ assert_select "div#content", :count => 1 do
+ assert_select "form[action='/events'][method=post]", :count => 1 do
+ assert_select "input#event_title[name='event[title]']", :count => 1
+ assert_select "input#event_moment[name='event[moment]']", :count => 1
+ assert_select "input#event_location[name='event[location]']", :count => 1
+ assert_select "input#event_location_url[name='event[location_url]']", :count => 1
+ assert_select "textarea#event_description[name='event[description]']", :count => 1
+ assert_select "input#event_latitude[name='event[latitude]']", :count => 1
+ assert_select "input#event_longitude[name='event[longitude]']", :count => 1
+ assert_select "input", :count => 8
+ end
+ end
+ end
+
+ def test_new_form_non_organizer
+ # Now try again when logged in. There must be community to build an event against.
+ cm = create(:community_member)
+ session_for(cm.user)
+
+ get new_event_path(:event => { :community_id => cm.community_id })
+
+ assert_redirected_to :controller => :errors, :action => :forbidden
+ end
+
+ # also tests application_controller::nilify
+ def test_create_when_save_works
+ cm = create(:community_member, :organizer)
+ e_orig = build(:event, :community => cm.community)
+ session_for(cm.user)
+
+ e_new_id = nil
+ assert_difference "Event.count", 1 do
+ post events_url, :params => { :event => e_orig.as_json }, :xhr => true
+ e_new_id = @response.headers["Location"].split("/")[-1]
+ end
+
+ e_new = Event.find(e_new_id)
+ # Assign the id e_new to e_orig, so we can do an equality test easily.
+ e_orig.id = e_new.id
+ assert_equal(e_orig, e_new)
+ end
+
+ def test_create_as_non_organizer
+ cm = create(:community_member)
+ ev = build(:event, :community => cm.community)
+ session_for(cm.user)
+
+ assert_difference "Event.count", 0 do
+ post events_url, :params => { :event => ev.as_json }, :xhr => true
+ end
+
+ assert_redirected_to :controller => :errors, :action => :forbidden
+ end
+
+ def test_create_when_save_fails
+ cm = create(:community_member, :organizer)
+ session_for(cm.user)
+
+ ev = create(:event, :community => cm.community)
+ # Customize this instance.
+ def ev.save
+ false
+ end
+
+ controller_mock = EventsController.new
+ def controller_mock.render(_partial)
+ # TODO: Would be nice to verify :new was rendered.
+ end
+
+ EventsController.stub :new, controller_mock do
+ Event.stub :new, ev do
+ assert_difference "Event.count", 0 do
+ post events_url, :params => { :event => ev.as_json }, :xhr => true
+ end
+ end
+ end
+
+ assert_equal I18n.t("events.create.failure"), flash[:alert]
+ end
+
+ def test_update_as_organizer
+ cm = create(:community_member, :organizer)
+ session_for(cm.user)
+ e1 = create(:event, :community => cm.community) # original object
+ e2 = build(:event, :community => cm.community) # new data
+
+ put event_url(e1), :params => { :event => e2.as_json }, :xhr => true
+
+ assert_redirected_to event_path(e1)
+ # TODO: Is it better to use t() to translate?
+ assert_equal "The event was successfully updated.", flash[:notice]
+ e1.reload
+ # Assign the id of e1 to e2, so we can do an equality test easily.
+ e2.id = e1.id
+ assert_equal(e2, e1)
+ end
+
+ def test_update_as_non_organizer
+ cm = create(:community_member)
+ session_for(cm.user)
+ e1 = create(:event, :community => cm.community) # original object
+ e2 = build(:event, :community => cm.community) # new data
+
+ put event_url(e1), :params => { :event => e2.as_json }, :xhr => true
+
+ assert_redirected_to :controller => :errors, :action => :forbidden
+ end
+
+ def test_update_put_failure
+ cm = create(:community_member, :organizer)
+ session_for(cm.user)
+ ev = create(:event, :community => cm.community)
+ def ev.update(_params)
+ false
+ end
+
+ controller_mock = EventsController.new
+ def controller_mock.set_event
+ @event = Event.new
+ end
+
+ def controller_mock.render(_partial)
+ # Can't do assert_equal here.
+ # assert_equal :edit, partial
+ end
+
+ EventsController.stub :new, controller_mock do
+ Event.stub :new, ev do
+ assert_difference "Event.count", 0 do
+ put event_url(ev), :params => { :event => ev.as_json }, :xhr => true
+ end
+ end
+ end
+
+ assert_equal I18n.t("events.update.failure"), flash[:alert]
+ end
+
+ def test_in_past_warns
+ cm = create(:community_member, :organizer)
+ session_for(cm.user)
+ form = create(:event, :community => cm.community).attributes
+ form["moment"] = "1000-01-01T01:01"
+
+ post events_url, :params => { :event => form }
+
+ follow_redirect!
+ assert_equal I18n.t("events.show.past"), flash[:warning]
+ end
+end
diff --git a/test/factories/event_attendances.rb b/test/factories/event_attendances.rb
new file mode 100644
index 0000000000..89c1336c91
--- /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/factories/event_organizers.rb b/test/factories/event_organizers.rb
new file mode 100644
index 0000000000..175c60eaf5
--- /dev/null
+++ b/test/factories/event_organizers.rb
@@ -0,0 +1,6 @@
+FactoryBot.define do
+ factory :event_organizer do
+ event
+ user
+ end
+end
diff --git a/test/factories/events.rb b/test/factories/events.rb
new file mode 100644
index 0000000000..77171a48e1
--- /dev/null
+++ b/test/factories/events.rb
@@ -0,0 +1,14 @@
+FactoryBot.define do
+ factory :event do
+ sequence(:title) { |n| "Title #{n}" }
+ moment { Time.now.utc + 1000 }
+ sequence(:location) { |n| "Location #{n}" }
+ sequence(:location_url) { |n| "https://example.com/app/#{n}" }
+ sequence(:description) { |n| "Description #{n}" }
+ community
+ latitude { rand(-90.0...90.0) }
+ longitude { rand(-180.0...180.0) }
+
+ # TODO: trait for event with attendees
+ end
+end
diff --git a/test/helpers/events_helper_test.rb b/test/helpers/events_helper_test.rb
new file mode 100644
index 0000000000..9a89dbcadd
--- /dev/null
+++ b/test/helpers/events_helper_test.rb
@@ -0,0 +1,15 @@
+require "test_helper"
+
+class EventsHelperTest < ActionView::TestCase
+ def test_event_location_url
+ event = create(:event)
+ location = event_location(event)
+ assert_match %r{^
#{event.location}$}, location
+ end
+
+ def test_event_location_no_url
+ event = create(:event, :location_url => nil)
+ location = event_location(event)
+ assert_match(/^#{event.location}$/, location)
+ end
+end
diff --git a/test/models/event_attendance_test.rb b/test/models/event_attendance_test.rb
new file mode 100644
index 0000000000..dd8c492497
--- /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_organizer_test.rb b/test/models/event_organizer_test.rb
new file mode 100644
index 0000000000..0cd6db89b7
--- /dev/null
+++ b/test/models/event_organizer_test.rb
@@ -0,0 +1,7 @@
+require "test_helper"
+
+class EventOrganizerTest < ActiveSupport::TestCase
+ def test_eventorganizer_validations
+ validate({}, true)
+ end
+end
diff --git a/test/models/event_test.rb b/test/models/event_test.rb
new file mode 100644
index 0000000000..bb63a9ae68
--- /dev/null
+++ b/test/models/event_test.rb
@@ -0,0 +1,92 @@
+require "test_helper"
+
+class EventTest < ActiveSupport::TestCase
+ def test_validations
+ validate({}, true)
+
+ validate({ :moment => nil }, false)
+ validate({ :moment => "" }, false)
+ validate({ :moment => "not a timestamp" }, false)
+ validate({ :moment => "3030-30-30T30:30" }, false)
+
+ validate({ :location => nil }, false)
+ validate({ :location => "" }, false)
+
+ validate({ :location => "a" * 255 }, true)
+ validate({ :location => "a" * 256 }, false)
+
+ validate({ :location_url => nil }, true)
+ validate({ :location_url => "" }, true)
+
+ validate({ :location_url => "foo" }, false)
+ scheme = "https://"
+ validate({ :location_url => scheme + ("a" * (255 - scheme.length)) }, true)
+ validate({ :location_url => scheme + ("a" * (256 - scheme.length)) }, false)
+
+ validate({ :latitude => 90 }, true)
+ validate({ :latitude => 90.00001 }, false)
+ validate({ :latitude => -90 }, true)
+ validate({ :latitude => -90.00001 }, false)
+
+ validate({ :longitude => 180 }, true)
+ validate({ :longitude => 180.00001 }, false)
+ 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