diff --git a/Gemfile b/Gemfile index 5bc497e8efc..3d5d861bb34 100644 --- a/Gemfile +++ b/Gemfile @@ -67,6 +67,7 @@ gem "rails_param" gem "rinku", ">= 2.0.6", :require => "rails_rinku" gem "strong_migrations", "< 2.0.0" gem "validates_email_format_of", ">= 1.5.1" +gem "validate_url" # Native OSM extensions gem "quad_tile", "~> 1.0.1" diff --git a/Gemfile.lock b/Gemfile.lock index 0fd0b75d88c..ae45f2f1e8b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -583,6 +583,9 @@ GEM concurrent-ruby (~> 1.0) unicode-display_width (2.5.0) uri (0.13.0) + validate_url (1.0.15) + activemodel (>= 3.0.0) + public_suffix validates_email_format_of (1.8.2) i18n (>= 0.8.0) simpleidn @@ -703,6 +706,7 @@ DEPENDENCIES terser turbo-rails unicode-display_width + validate_url validates_email_format_of (>= 1.5.1) vendorer webmock diff --git a/app/abilities/ability.rb b/app/abilities/ability.rb index e72d5094b78..82101d16622 100644 --- a/app/abilities/ability.rb +++ b/app/abilities/ability.rb @@ -18,6 +18,7 @@ def initialize(user) can [:index, :feed, :show], Changeset can :index, ChangesetComment can [:index, :show], Community + can [:index], CommunityLink can [:confirm, :confirm_resend, :confirm_email], :confirmation can [:index, :rss, :show], DiaryEntry can :index, DiaryComment @@ -50,6 +51,7 @@ def initialize(user) can [:new, :create, :reply, :show, :inbox, :outbox, :muted, :mark, :unmute, :destroy], Message can [:create, :new], Community can [:edit, :update], Community, { :organizer_id => user.id } + can [:edit, :create, :destroy, :new, :update], CommunityLink, { :community => { :organizer_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 f3c1ad38abc..d5fd050241b 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -10,6 +10,7 @@ //= require leaflet.osm //= require leaflet.map //= require leaflet.zoom +//= require leaflet.locatecontrol/src/L.Control.Locate //= require leaflet.locationfilter //= require i18n //= require oauth @@ -148,70 +149,121 @@ $(document).ready(function () { .attr("title", I18n.t("javascripts.site.edit_disabled_tooltip")); }); +window.addLocateControl = function (map, position) { + var locate = L.control.locate({ + position: position, + icon: "icon geolocate", + iconLoading: "icon geolocate", + strings: { + title: I18n.t("javascripts.map.locate.title"), + popup: function (options) { + return I18n.t("javascripts.map.locate." + options.unit + "Popup", { count: options.distance }); + } + } + }).addTo(map); + $(locate.getContainer()) + .removeClass("leaflet-control-locate leaflet-bar") + .addClass("control-locate") + .children("a") + .attr("href", "#") + .removeClass("leaflet-bar-part leaflet-bar-part-single") + .addClass("control-button"); +}; + +/* + * Create a map on the page for the given `id`. Adhere to rtl pages. Parameters to + * the map are provided as data attributes. + * * zoom + * * latitude, longitude + * * min-lat, max-lat, min-lon, max-lon + * If the map div has a class `has_marker` there will be a marker added to the map. + */ window.showMap = function (id) { - var params = $("#" + id).data(); - var map = L.map(id, { - attributionControl: false, - zoomControl: false - }); - map.addLayer(new L.OSM.Mapnik()); - var show_marker = true; - if (!params.lon || !params.lat) { - params.lon = 0; - params.lat = 0; - params.zoom = 1; - show_marker = false; + const div = $("#" + id); + // Defaults + const position = $("html").attr("dir") === "rtl" ? "topleft" : "topright"; + let zoom = 2; + let [latitude, longitude] = [0, 0]; + let bounds = null; + let marker = null; + + // Extract params + const params = div.data(); + if (params.zoom) { + zoom = params.zoom; } - map.setView([params.lat, params.lon], params.zoom); - if (show_marker) { - L.marker([params.lat, params.lon], { icon: OSM.getUserIcon() }).addTo(map); + if (params.latitude && params.longitude) { + [latitude, longitude] = [params.latitude, params.longitude]; + } + if (params.minLat && params.maxLat && params.minLon && params.maxLon) { + bounds = [ + [params.minLat, params.minLon], + [params.maxLat, params.maxLon] + ]; } -}; -window.formMapInput = function (id, type) { - var map = L.map(id, { - attributionControl: false + // Create map. + const map = L.map(id, { + attributionControl: false, + center: [latitude, longitude], + zoom: zoom, + zoomControl: false }); + window.addLocateControl(map, position); + L.OSM.zoom({ position: position }).addTo(map); map.addLayer(new L.OSM.Mapnik()); + if (bounds) { + map.fitBounds(bounds); + } - var lat_field = document.getElementById(type + "_latitude"); - var lon_field = document.getElementById(type + "_longitude"); - - if (lat_field.value) { - map.setView([lat_field.value, lon_field.value], 12); - } else { - map.setView([0, 0], 0); + if (div.hasClass("has_marker")) { + marker = L.marker([latitude, longitude], { + icon: OSM.getUserIcon(), + keyboard: false, + interactive: false + }).addTo(map); } - L.Control.Watermark = L.Control.extend({ - onAdd: function () { - var container = map.getContainer(); - var img = L.DomUtil.create("img"); - img.src = "/assets/marker-blue.png"; // 25x41 px - // img.style.width = '200px'; - img.style["margin-left"] = ((container.offsetWidth / 2) - 12) + "px"; - img.style["margin-bottom"] = ((container.offsetHeight / 2) - 20) + "px"; - return img; - }, - onRemove: function () { - // Nothing to do here + return { map, marker }; +}; + +/* + * Create a basic map using showMap above, and connect it to the form that + * contains it. If the map div has the set_location class, the map will + * set values in appropriate fields if they exist as the user clicks and + * moves the map. These fields are: + * * field_latitude + * * field_longitude + * * field_min_lat + * * field_max_lat + * * field_min_lon + * * field_max_lon + */ +window.formMapInit = function (id) { + const formDiv = $("#" + id); + const mapDiv = $("#" + id + "_map"); + const { map, marker } = window.showMap(id + "_map"); + + if (mapDiv.hasClass("set_location")) { + if ($(".field_latitude", formDiv) && $(".field_longitude", formDiv)) { + map.on("click", function (e) { + const location = e.latlng.wrap(); + marker.setLatLng(location); + // If the page has these elements, populate them. + $(".field_latitude", formDiv).val(location.lat); + $(".field_longitude", formDiv).val(location.lng); + }); } - }); - L.control.watermark = function (opts) { - return new L.Control.Watermark(opts); - }; - L.control.watermark({ position: "bottomleft" }).addTo(map); - - map.on("move", function () { - var center = map.getCenter(); - $("#" + type + "_latitude").val(center.lat); - $("#" + type + "_longitude").val(center.lng); - if ($("#" + type + "_min_lat")) { - var bounds = map.getBounds(); - $("#" + type + "_min_lat").val(bounds._southWest.lat); - $("#" + type + "_max_lat").val(bounds._northEast.lat); - $("#" + type + "_min_lon").val(bounds._southWest.lng); - $("#" + type + "_max_lon").val(bounds._northEast.lng); + + if ($(".field_min_lat", formDiv) && $(".field_max_lat", formDiv) && + $(".field_min_lon", formDiv) && $(".field_max_lon", formDiv)) { + map.on("move", function () { + var bounds = map.getBounds(); + $(".field_min_lat").val(bounds._southWest.lat); + $(".field_max_lat").val(bounds._northEast.lat); + $(".field_min_lon").val(bounds._southWest.lng); + $(".field_max_lon").val(bounds._northEast.lng); + }); } - }); + } }; diff --git a/app/assets/javascripts/communities.js b/app/assets/javascripts/communities.js index 42ef8954b26..732eb559c55 100644 --- a/app/assets/javascripts/communities.js +++ b/app/assets/javascripts/communities.js @@ -1,9 +1,9 @@ -/*global showMap,formMapInput*/ +/*global showMap,formMapInit*/ $(document).ready(function () { - if ($("#community_map_form").length) { - formMapInput("community_map_form", "community"); + if ($("#community_form").length) { + formMapInit("community_form"); } else if ($("#community_map").length) { showMap("community_map"); } diff --git a/app/assets/stylesheets/communities.scss b/app/assets/stylesheets/communities.scss index f25fb70fd3e..7b6bd73c4e3 100644 --- a/app/assets/stylesheets/communities.scss +++ b/app/assets/stylesheets/communities.scss @@ -9,10 +9,4 @@ label { font-weight: bold; } - ul { - display: inline-block; - } - ul > li { - display: inline-block; - } } diff --git a/app/controllers/community_links_controller.rb b/app/controllers/community_links_controller.rb new file mode 100644 index 00000000000..766d6d187aa --- /dev/null +++ b/app/controllers/community_links_controller.rb @@ -0,0 +1,61 @@ +class CommunityLinksController < ApplicationController + layout "site" + before_action :authorize_web + + before_action :set_link, :only => [:destroy, :edit, :update] + + load_and_authorize_resource :except => [:create, :new] + authorize_resource + + def index + @community = Community.friendly.find(params[:community_id]) + @links = @community.community_links + end + + def new + return "missing parameter community_id" unless params.key?(:community_id) + + @community = Community.friendly.find(params[:community_id]) + @title = t ".title" + @link = CommunityLink.new + @link.community_id = params[:community_id] + end + + def edit; end + + def create + @community = Community.friendly.find(params[:community_id]) + @link = @community.community_links.build(link_params) + if @link.save + response.set_header("link_id", @link.id) # for testing + redirect_to @link.community, :notice => t(".success") + else + render "new" + end + end + + def update + if @link.update(link_params) + redirect_to @link.community, :notice => t(".success") + else + flash.now[:alert] = t(".failure") + render :edit + end + end + + def destroy + community_id = @link.community_id + @link.delete + redirect_to community_path(community_id) + end + + private + + def set_link + @link = CommunityLink.find(params[:id]) + end + + def link_params + params.require(:community_link).permit(:community_id, :text, :url) + end +end diff --git a/app/models/community.rb b/app/models/community.rb index 45ab6b1b876..c5caab1c9bb 100644 --- a/app/models/community.rb +++ b/app/models/community.rb @@ -35,6 +35,7 @@ class Community < ApplicationRecord friendly_id :name, :use => :slugged belongs_to :organizer, :class_name => "User" + has_many :community_links validates :name, :presence => true, :length => 1..255, :characters => true validates :description, :presence => true, :length => 1..1023, :characters => true diff --git a/app/models/community_link.rb b/app/models/community_link.rb new file mode 100644 index 00000000000..b0cda052600 --- /dev/null +++ b/app/models/community_link.rb @@ -0,0 +1,25 @@ +# == Schema Information +# +# Table name: community_links +# +# id :bigint(8) not null, primary key +# community_id :bigint(8) not null +# text :string not null +# url :string not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_community_links_on_community_id (community_id) +# +# Foreign Keys +# +# fk_rails_... (community_id => communities.id) +# + +class CommunityLink < ApplicationRecord + belongs_to :community + validates :text, :presence => true, :length => 1..255, :characters => true + validates :url, :presence => true, :length => 1..255, :url => { :schemes => ["https"] } +end diff --git a/app/views/communities/_form.html.erb b/app/views/communities/_form.html.erb index 18745618fcd..7356b0563d5 100644 --- a/app/views/communities/_form.html.erb +++ b/app/views/communities/_form.html.erb @@ -4,29 +4,46 @@

All fields are required.

-<%= bootstrap_form_for @community do |form| %> +<%= bootstrap_form_with :model => @community, :id => "community_form" do |form| %> <%= form.text_field :name, :id => :community_name %> <%= form.text_field :location, :id => :community_location %>
- <%= form.text_field :latitude, :wrapper_class => "col-sm-6", :id => :community_latitude %> - <%= form.text_field :longitude, :wrapper_class => "col-sm-6", :id => :community_longitude %> -
-
-
+
+ <%= form.text_field :latitude, :class => "field_latitude", :id => :community_latitude %> +
+
+ <%= form.text_field :longitude, :class => "field_longitude", :id => :community_longitude %>
+ <%= tag.div + "", + :id => "community_form_map", + :class => "content_map has_marker set_location border border-secondary-subtle rounded mb-3 z-0", + :data => { + :latitude => @community.latitude, + :longitude => @community.longitude, + :min_lat => @community.min_lat, + :max_lat => @community.max_lat, + :min_lon => @community.min_lon, + :max_lon => @community.max_lon + } %>
-
- <%= form.text_field :max_lat, :wrapper_class => "col-sm-4", :id => :community_max_lat %> +
+ <%= form.text_field :max_lat, :class => "field_max_lat", :id => :community_max_lat %> +
- <%= form.text_field :min_lon, :wrapper_class => "col-sm-4", :id => :community_min_lon %> -
- <%= form.text_field :max_lon, :wrapper_class => "col-sm-4", :id => :community_max_lon %> +
+ <%= form.text_field :min_lon, :class => "field_min_lon", :id => :community_min_lon %> +
+
+ <%= form.text_field :max_lon, :class => "field_max_lon", :id => :community_max_lon %> +
-
- <%= form.text_field :min_lat, :wrapper_class => "col-sm-4", :id => :community_min_lat %> +
+ <%= form.text_field :min_lat, :class => "field_min_lat", :id => :community_min_lat %> +
<%= form.text_area :description, :id => :community_description %> <%= form.primary %> diff --git a/app/views/communities/show.html.erb b/app/views/communities/show.html.erb index e0505b9283a..ad97348b2ff 100644 --- a/app/views/communities/show.html.erb +++ b/app/views/communities/show.html.erb @@ -17,7 +17,15 @@
<%# TODO: replace these attributes @map_coords %> - <%= tag.div :id => "community_map", :class => "content_map", :data => { :lat => @community.latitude, :lon => @community.longitude, :zoom => 11 } %> + <%= tag.div + "", + :id => "community_map", + :class => "content_map", + :data => { + :latitude => @community.latitude, + :longitude => @community.longitude, + :zoom => 11 + } %>

@@ -29,6 +37,22 @@

<%= auto_link @community.description %>

+
+ + +
<%= link_to @community.organizer.display_name, user_path(@community.organizer) %> diff --git a/app/views/community_links/_form.html.erb b/app/views/community_links/_form.html.erb new file mode 100644 index 00000000000..d95681aaac2 --- /dev/null +++ b/app/views/community_links/_form.html.erb @@ -0,0 +1,5 @@ +<%= bootstrap_form_for [@community, @link] do |form| %> + <%= form.text_field :text %> + <%= form.text_field :url %> + <%= form.primary %> +<% end %> diff --git a/app/views/community_links/edit.html.erb b/app/views/community_links/edit.html.erb new file mode 100644 index 00000000000..f2d508679a4 --- /dev/null +++ b/app/views/community_links/edit.html.erb @@ -0,0 +1,3 @@ +

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

+ +<%= render "form", :link => @link %> diff --git a/app/views/community_links/index.html.erb b/app/views/community_links/index.html.erb new file mode 100644 index 00000000000..58546c2cd1e --- /dev/null +++ b/app/views/community_links/index.html.erb @@ -0,0 +1,32 @@ +<% content_for :heading do %> +

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

+ +<% end %> + +<% if !@links.empty? %> + + + <% @links.each do |link| %> + + + + + <% end %> + +
+ <% link.url.slice! "https://" %> <%# prevent XSS %> + <%= link_to link.text, "https://#{link.url}" %> + + <%= link_to t(".edit"), edit_community_link_path(link) %> + <%= link_to t(".delete"), community_link_path(link), :method => :delete %> +
+<% end %> diff --git a/app/views/community_links/new.html.erb b/app/views/community_links/new.html.erb new file mode 100644 index 00000000000..7c2f12855e2 --- /dev/null +++ b/app/views/community_links/new.html.erb @@ -0,0 +1,5 @@ +<% content_for :heading do %> +

<%= @title %>

+<% end %> + +<%= render "form", :link => @link %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 8764744b3cc..98363bf43a0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -53,6 +53,7 @@ en: email_address_not_routable: is not routable display_name_is_user_n: can't be user_n unless n is your user id models: + url: "is not a valid secure URL" user_mute: attributes: subject: @@ -520,6 +521,22 @@ en: title_particular: "OpenStreetMap changeset #%{changeset_id} discussion" timeout: sorry: "Sorry, the list of changeset comments you requested took too long to retrieve." + community_links: + create: + success: "Community Link was successfully created." + edit: + edit_community_link: "Edit Community Link" + index: + delete: "Delete" + edit: "Edit" + new: "New" + title: "Community Links" + new: + all: "All" + title: "New Community Link" + update: + failure: "The community link could not be updated." + success: "The community link was successfully updated." communities: create: success: "Community was successfully created." @@ -528,7 +545,6 @@ en: index: all: "All Communities" communities_organized: "Communities Organized" - longitude: "Longitude" new: "New" new_title: "Create a new community" sorted_by: "Sorted by name" @@ -540,7 +556,9 @@ en: heading: "No community with the id: %{id}" body: "Sorry, there is no community with the id %{id}. Please check your spelling, or maybe the link you clicked is wrong." show: + edit: "Edit" header_title: "Community" + links: "Links" organizer: "Organizer" recent_changes: "Recent Changes" report: "Report" diff --git a/config/routes.rb b/config/routes.rb index 19e20f3a18b..81bc3735017 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -345,7 +345,10 @@ resources :redactions # communities - resources :communities + resources :communities do + resources :community_links, :only => [:create, :index, :new] + end + resources :community_links, :only => [:destroy, :edit, :update] # errors match "/400", :to => "errors#bad_request", :via => :all diff --git a/db/migrate/20240525143545_create_community_links.rb b/db/migrate/20240525143545_create_community_links.rb new file mode 100644 index 00000000000..dd6d9f498ea --- /dev/null +++ b/db/migrate/20240525143545_create_community_links.rb @@ -0,0 +1,11 @@ +class CreateCommunityLinks < ActiveRecord::Migration[7.0] + def change + create_table :community_links do |t| + t.references :community, :null => false, :foreign_key => true, :index => true + t.string :text, :null => false + t.string :url, :null => false + + t.timestamps + end + end +end diff --git a/db/structure.sql b/db/structure.sql index bcb516841c9..a9ec678801b 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -558,6 +558,39 @@ CREATE SEQUENCE public.communities_id_seq ALTER SEQUENCE public.communities_id_seq OWNED BY public.communities.id; +-- +-- Name: community_links; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.community_links ( + id bigint NOT NULL, + community_id bigint NOT NULL, + text character varying NOT NULL, + url character varying NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: community_links_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.community_links_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: community_links_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.community_links_id_seq OWNED BY public.community_links.id; + + -- -- Name: current_node_tags; Type: TABLE; Schema: public; Owner: - -- @@ -1815,6 +1848,13 @@ ALTER TABLE ONLY public.client_applications ALTER COLUMN id SET DEFAULT nextval( ALTER TABLE ONLY public.communities ALTER COLUMN id SET DEFAULT nextval('public.communities_id_seq'::regclass); +-- +-- Name: community_links id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.community_links ALTER COLUMN id SET DEFAULT nextval('public.community_links_id_seq'::regclass); + + -- -- Name: current_nodes id; Type: DEFAULT; Schema: public; Owner: - -- @@ -2084,6 +2124,14 @@ ALTER TABLE ONLY public.communities ADD CONSTRAINT communities_pkey PRIMARY KEY (id); +-- +-- Name: community_links community_links_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.community_links + ADD CONSTRAINT community_links_pkey PRIMARY KEY (id); + + -- -- Name: current_node_tags current_node_tags_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2716,6 +2764,13 @@ CREATE INDEX index_communities_on_organizer_id ON public.communities USING btree CREATE UNIQUE INDEX index_communities_on_slug ON public.communities USING btree (slug); +-- +-- Name: index_community_links_on_community_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_community_links_on_community_id ON public.community_links USING btree (community_id); + + -- -- Name: index_diary_entry_subscriptions_on_diary_entry_id; Type: INDEX; Schema: public; Owner: - -- @@ -3386,6 +3441,14 @@ ALTER TABLE ONLY public.oauth_access_tokens ADD CONSTRAINT fk_rails_ee63f25419 FOREIGN KEY (resource_owner_id) REFERENCES public.users(id) NOT VALID; +-- +-- Name: community_links fk_rails_f60a749c39; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.community_links + ADD CONSTRAINT fk_rails_f60a749c39 FOREIGN KEY (community_id) REFERENCES public.communities(id); + + -- -- Name: friends friends_friend_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -3727,6 +3790,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('21'), ('20240618193051'), ('20240605134916'), +('20240525143545'), ('20240525030520'), ('20240525020545'), ('20240405083825'), diff --git a/test/controllers/community_links_controller_test.rb b/test/controllers/community_links_controller_test.rb new file mode 100644 index 00000000000..cb43b5ead6e --- /dev/null +++ b/test/controllers/community_links_controller_test.rb @@ -0,0 +1,186 @@ +require "test_helper" +require "minitest/mock" + +class CommunityLinksControllerTest < ActionDispatch::IntegrationTest + ## + # test all routes which lead to this controller + # Following guidance from Ruby on Rails Guide + # https://guides.rubyonrails.org/testing.html#functional-tests-for-your-controllers + + def test_routes + assert_routing( + { :path => "/communities/foo/community_links", :method => :get }, + { :controller => "community_links", :action => "index", :community_id => "foo" } + ) + assert_routing( + { :path => "/community_links/1/edit", :method => :get }, + { :controller => "community_links", :action => "edit", :id => "1" } + ) + assert_routing( + { :path => "/community_links/1", :method => :put }, + { :controller => "community_links", :action => "update", :id => "1" } + ) + assert_routing( + { :path => "/communities/foo/community_links/new", :method => :get }, + { :controller => "community_links", :action => "new", :community_id => "foo" } + ) + assert_routing( + { :path => "/communities/foo/community_links", :method => :post }, + { :controller => "community_links", :action => "create", :community_id => "foo" } + ) + end + + def test_index_get + c = create(:community) + link = create(:community_link, :community_id => c.id) + + get community_community_links_path(c.id) + + assert_response :success + assert_template "index" + assert_match link.text, response.body + end + + def test_edit_get_no_session + l = create(:community_link) + + get edit_community_link_path(l) + + assert_response :redirect + assert_redirected_to login_path(:referer => edit_community_link_path(l)) + end + + def test_update_as_non_organizer + # Should this test be in abilities_test.rb? + link = create(:community_link) + session_for(create(:user)) + + put community_link_path link, :community_link => link + + assert_redirected_to :controller => :errors, :action => :forbidden + end + + def test_update_put_success + # TODO: When community_member is created switch to using that factory. + c = create(:community) + link1 = create(:community_link, :community_id => c.id) # original object + link2 = build(:community_link, :community_id => c.id) # new data + link_2_form = link2.attributes.except("id", "created_at", "updated_at") + session_for(c.organizer) + + # Update link1 with the values from link2. + put community_link_url(link1), :params => { :community_link => link_2_form.as_json }, :xhr => true + + assert_redirected_to community_path(link1.community) + assert_equal I18n.t("community_links.update.success"), flash[:notice] + link1.reload + # Assign the id of link1 to link2, so we can do an equality test easily. + link2.id = link1.id + assert_equal(link2, link1) + end + + def test_update_put_failure + c = create(:community) # original object + session_for(c.organizer) + link = create(:community_link, :community_id => c.id) # original object + def link.update(_params) + false + end + + controller_mock = CommunityLinksController.new + def controller_mock.set_link + @link = CommunityLink.new + end + + def controller_mock.render(_partial) + # Can't do assert_equal here. + # assert_equal :edit, partial + end + + CommunityLinksController.stub :new, controller_mock do + CommunityLink.stub :new, link do + assert_difference "CommunityLink.count", 0 do + put community_link_url(link), :params => { :community_link => link.as_json }, :xhr => true + end + end + end + + assert_equal I18n.t("community_links.update.failure"), flash[:alert] + end + + def test_new_no_login + # Make sure that you are redirected to the login page when you + # are not logged in + c = create(:community) + + get new_community_community_link_path(c) + + assert_response :redirect + assert_redirected_to login_path(:referer => new_community_community_link_path(c)) + end + + def test_new_form + # Now try again when logged in + c = create(:community) + session_for(c.organizer) + + get new_community_community_link_path(c) + + assert_response :success + assert_select "div.content-heading", :count => 1 do + assert_select "h1", :text => /Community Link/, :count => 1 + end + action = community_community_links_path(c) + assert_select "div#content", :count => 1 do + assert_select "form[action='#{action}'][method=post]", :count => 1 do + assert_select "input#community_link_text[name='community_link[text]']", :count => 1 + assert_select "input#community_link_url[name='community_link[url]']", :count => 1 + assert_select "input", :count => 3 + end + end + end + + def test_create_when_save_works + c = create(:community) + link_orig = create(:community_link, :community => c) + form = link_orig.attributes.except("id", "created_at", "updated_at") + session_for(c.organizer) + + link_new_id = nil + assert_difference "CommunityLink.count", 1 do + post community_community_links_path c.id, :community_link => form + link_new_id = @response.headers["link_id"] + end + + # Not sure what's going on with this assigns magic. + # assert_redirected_to "/community/#{assigns(:community_link).id}" + assert_equal I18n.t("community_links.create.success"), flash[:notice] + link_new = CommunityLink.find_by(:id => link_new_id) + # Assign the id link_new to link_orig, so we can do an equality test easily. + link_orig.id = link_new.id + assert_equal(link_orig, link_new) + end + + def test_create_when_save_fails + c = create(:community) + session_for(c.organizer) + link = build(:community_link, :community => c, :url => "invalid url") + form = link.attributes.except("id", "created_at", "updated_at") + + assert_no_difference "CommunityLink.count", 0 do + post community_community_links_path :community_link => form, :community_id => c.id + end + + assert_template :new + end + + def test_delete + c = create(:community) + link = create(:community_link, :community_id => c.id) + session_for(c.organizer) + + assert_difference "CommunityLink.count", -1 do + delete community_link_path(:id => link.id) + end + end +end diff --git a/test/factories/community_links.rb b/test/factories/community_links.rb new file mode 100644 index 00000000000..2446a9762ff --- /dev/null +++ b/test/factories/community_links.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :community_link do + community + text { "website" } + url { "https://example.com" } + end +end diff --git a/test/models/community_link_test.rb b/test/models/community_link_test.rb new file mode 100644 index 00000000000..501e994edf7 --- /dev/null +++ b/test/models/community_link_test.rb @@ -0,0 +1,21 @@ +require "test_helper" + +class CommunityLinkTest < ActiveSupport::TestCase + def test_community_link_validations + validate({}, true) + + validate({ :community_id => nil }, false) + validate({ :community_id => "" }, false) + + validate({ :text => nil }, false) + validate({ :text => "" }, false) + + validate({ :url => nil }, false) + validate({ :url => "" }, false) + + validate({ :url => "foo" }, false) + scheme = "https://" + validate({ :url => scheme + ("a" * (255 - scheme.length)) }, true) + validate({ :url => scheme + ("a" * (256 - scheme.length)) }, false) + end +end