diff --git a/app/abilities/ability.rb b/app/abilities/ability.rb index be9e6ca4dd..805b68f8e8 100644 --- a/app/abilities/ability.rb +++ b/app/abilities/ability.rb @@ -23,6 +23,7 @@ def initialize(user) can [:confirm, :confirm_resend, :confirm_email], :confirmation can [:index, :rss, :show], DiaryEntry can :index, DiaryComment + can [:index, :show], Event can [:index], Note can [:new, :create, :edit, :update], :password can [:index, :show], Redaction @@ -58,9 +59,12 @@ def initialize(user) } can [:create, :new, :step_up], Community can [:edit, :update], Community, user_is_community_organizer - can [:edit, :create, :destroy, :new, :update], CommunityLink, { :community => user_is_community_organizer } - can [:create], CommunityMember, { :user_id => user.id } - can [:destroy, :edit, :update], CommunityMember, { :community => user_is_community_organizer } + can [:edit, :create, :destroy, :new, :update], CommunityLink, :community => user_is_community_organizer + can [:create, :destroy], CommunityMember, :user_id => user.id + can [:destroy, :edit, :update], CommunityMember, :community => user_is_community_organizer + can [:create, :edit, :new, :update], Event, :community => user_is_community_organizer + can [:create], EventAttendance + can [:update], EventAttendance, :user_id => user.id can [:close, :reopen], Note can [:show, :edit, :update], :preference can [:edit, :update], :profile diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index f3c1ad38ab..8c5cbd6694 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -180,7 +180,7 @@ window.formMapInput = function (id, type) { if (lat_field.value) { map.setView([lat_field.value, lon_field.value], 12); } else { - map.setView([0, 0], 0); + map.setView([0, 0], 2); } L.Control.Watermark = L.Control.extend({ diff --git a/app/assets/javascripts/event.js b/app/assets/javascripts/event.js new file mode 100644 index 0000000000..1989caf2df --- /dev/null +++ b/app/assets/javascripts/event.js @@ -0,0 +1,9 @@ +/*global showMap,formMapInput*/ + +$(document).ready(function () { + if ($("#event_map_form").length) { + formMapInput("event_map_form", "event"); + } else if ($("#event_map_show").length) { + showMap("event_map_show"); + } +}); diff --git a/app/controllers/event_attendances_controller.rb b/app/controllers/event_attendances_controller.rb new file mode 100644 index 0000000000..ed02e0382a --- /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 new file mode 100644 index 0000000000..17971e9714 --- /dev/null +++ b/app/controllers/events_controller.rb @@ -0,0 +1,99 @@ +class EventsController < ApplicationController + layout "site" + before_action :authorize_web + before_action :set_event, :only => [:edit, :show, :update] + # This needs to be one before load_and_authorize_resource, so cancancan will be handed + # an event that contains a community, based on the input parameter community_id. + before_action :set_params_for_new, :only => [:new] + + load_and_authorize_resource + + # GET /events + # GET /events.json + def index + if params[:community_id] + @community = Community.friendly.find(params[:community_id]) + @events = @community.events + else + @community = nil + @events = Event.all + end + rescue ActiveRecord::RecordNotFound + @not_found_community = params[:community_id] + render :template => "communities/no_such_community", :status => :not_found + end + + # GET /events/1 + # 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 + end + + # GET /events/new + def new + @title = t ".new" + @event = Event.new(event_params_new) + end + + # GET /events/1/edit + def edit; end + + # POST /events + # POST /events.json + def create + @event = Event.new(event_params) + @event_organizer = EventOrganizer.new(:event => @event, :user => current_user) + + if @event.save && @event_organizer.save + warn_if_event_in_past + redirect_to @event, :notice => t(".success") + else + flash.now[:alert] = t(".failure") + render :new + end + end + + def update + if @event.update(event_params) + redirect_to @event, :notice => t(".success") + else + flash.now[:alert] = t(".failure") + render :edit + end + end + + private + + def warn_if_event_in_past + flash[:warning] = t "events.show.past" if @event.past? + end + + def set_event + @event = Event.find(params[:id]) + end + + def set_params_for_new + @params = event_params_new + end + + def event_params + params.require(:event).permit( + :title, :moment, :location, :location_url, + :latitude, :longitude, :description, :community_id + ) + end + + def event_params_new + params.require(:event).permit(:community_id) + end +end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb new file mode 100644 index 0000000000..be8f367dff --- /dev/null +++ b/app/helpers/events_helper.rb @@ -0,0 +1,9 @@ +module EventsHelper + def event_location(event) + if event.location_url.present? + link_to event.location, event.location_url + else + event.location + end + end +end diff --git a/app/models/community.rb b/app/models/community.rb index a9b33a5879..6d56daf5ee 100644 --- a/app/models/community.rb +++ b/app/models/community.rb @@ -41,6 +41,7 @@ class Community < ApplicationRecord belongs_to :leader, :class_name => "User" has_many :community_links + has_many :events, -> { order(:moment) }, :inverse_of => :community validates :name, :presence => true, :length => 1..255, :characters => true validates :description, :presence => true, :length => 1..1023, :characters => true diff --git a/app/models/event.rb b/app/models/event.rb new file mode 100644 index 0000000000..4a7e9704cc --- /dev/null +++ b/app/models/event.rb @@ -0,0 +1,83 @@ +# == Schema Information +# +# Table name: events +# +# id :bigint(8) not null, primary key +# title :string not null +# moment :datetime not null +# location :string not null +# location_url :string +# latitude :float +# longitude :float +# description :text not null +# community_id :bigint(8) not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_events_on_community_id (community_id) +# +# Foreign Keys +# +# fk_rails_... (community_id => communities.id) +# + +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) } + + validates :moment, :datetime_format => true + validates :location, :length => { :maximum => 255 }, :presence => true + # While latitude and longitude below will implicitly convert blanks to nil, + # the string/url here will not and I don't know why. + validates( + :location_url, + :allow_nil => true, :length => { :maximum => 255 }, + :url => { :allow_nil => true, :allow_blank => true, :schemes => ["https"] } + ) + validates( + :latitude, + :allow_nil => true, + :numericality => { + :greater_than_or_equal_to => -90, + :less_than_or_equal_to => 90 + } + ) + validates( + :longitude, + :allow_nil => true, + :numericality => { + :greater_than_or_equal_to => -180, + :less_than_or_equal_to => 180 + } + ) + + def organizers + EventOrganizer.where(:event_id => id) + end + + 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 0000000000..030c886871 --- /dev/null +++ b/app/models/event_attendance.rb @@ -0,0 +1,35 @@ +# == Schema Information +# +# Table name: event_attendances +# +# id :bigint(8) not null, primary key +# user_id :bigint(8) not null +# event_id :bigint(8) 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) +# index_event_attendances_on_user_id_and_event_id (user_id,event_id) UNIQUE +# +# Foreign Keys +# +# fk_rails_... (event_id => events.id) +# fk_rails_... (user_id => users.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/models/event_organizer.rb b/app/models/event_organizer.rb new file mode 100644 index 0000000000..00cc250762 --- /dev/null +++ b/app/models/event_organizer.rb @@ -0,0 +1,24 @@ +# == Schema Information +# +# Table name: event_organizers +# +# id :bigint(8) not null, primary key +# event_id :bigint(8) not null +# user_id :bigint(8) not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_event_organizers_on_event_id (event_id) +# index_event_organizers_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (event_id => events.id) +# fk_rails_... (user_id => users.id) +# +class EventOrganizer < ApplicationRecord + belongs_to :event + belongs_to :user +end diff --git a/app/validators/datetime_format_validator.rb b/app/validators/datetime_format_validator.rb new file mode 100644 index 0000000000..ce5850751f --- /dev/null +++ b/app/validators/datetime_format_validator.rb @@ -0,0 +1,16 @@ +require "date" + +class DatetimeFormatValidator < ActiveModel::EachValidator + # No need to pull in validates_timeless for just a simple validation. + def validate_each(record, attribute, _value) + # By this point in time, rails has already converted an invalid _value to + # Nil. With built in rails validation, there's no good way to say the + # input is not a valid date. Validate the user input. + before_value = record.read_attribute_before_type_cast(attribute) + return if before_value.is_a? Time + + Date.iso8601(before_value) + rescue ArgumentError + record.errors.add(attribute, options[:message] || I18n.t("validations.invalid_datetime_range")) + end +end diff --git a/app/views/communities/show.html.erb b/app/views/communities/show.html.erb index 1769b50d58..980ee8567f 100644 --- a/app/views/communities/show.html.erb +++ b/app/views/communities/show.html.erb @@ -82,6 +82,21 @@
+

+ <%= 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 %> +

+

<%= 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 %> +
+
+
+ <%= form.text_field :latitude, :id => "event_latitude" %> +
+
+ <%= form.text_field :longitude, :id => "event_longitude" %> +
+
+
+
+
+ <% 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 %> +

+ + + + + + + + + + + <% events.each do |event| %> + + + + + + + <% end %> + +
<%= t(".moment") %><%= t(".title") %><%= t(".location") %><%= t(".community") %>
<%= l(event.moment, :format => :blog) %><%= link_to event.title, event %><%= event.location %><%= link_to event.community.name, community_path(event.community) %>
+<% 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