From de13e789ddace29486849791f414f1fb9cd02f08 Mon Sep 17 00:00:00 2001 From: Matheus Miranda Date: Tue, 13 Nov 2018 17:22:55 -0200 Subject: [PATCH] Add polygon geographies to Budgets' map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Note that in the budgets wizard test we now create district with no associated geozone, so the text "all city" will appear in the districts table too, meaning we can't use `within "section", text: "All city" do` anymore since it would result in an ambiguous match. Co-Authored-By: Julian Herrero Co-Authored-By: Javi Martín --- app/assets/javascripts/forms.js | 5 ++- app/assets/javascripts/map.js | 20 +++++++++ .../budget_headings/form_component.html.erb | 7 ++++ .../admin/budget_headings/form_component.rb | 4 ++ .../headings_component.html.erb | 4 ++ .../budget_headings/headings_component.rb | 8 ++++ .../admin/geozones/form_component.html.erb | 19 +++++++++ .../admin/geozones/index_component.html.erb | 4 +- .../admin/geozones/index_component.rb | 8 ++++ app/components/budgets/map_component.html.erb | 2 +- app/components/budgets/map_component.rb | 12 ++++++ .../shared/map_location_component.rb | 8 ++-- app/controllers/admin/geozones_controller.rb | 2 +- .../concerns/admin/budget_headings_actions.rb | 2 +- app/models/budget.rb | 1 + app/models/budget/heading.rb | 1 + .../concerns/geojson_format_validator.rb | 23 ++++++++++ app/models/geozone.rb | 26 ++++++++++++ config/locales/en/activerecord.yml | 6 +++ config/locales/en/admin.yml | 6 ++- config/locales/es/activerecord.yml | 6 +++ config/locales/es/admin.yml | 6 ++- ...20181113184434_add_polygons_to_geozones.rb | 8 ++++ .../20190109010131_add_geozone_to_headings.rb | 5 +++ db/schema.rb | 5 +++ .../budget_headings/form_component_spec.rb | 32 ++++++++++++++ .../headings_component_spec.rb | 20 ++++++++- .../admin/geozones/index_component_spec.rb | 23 ++++++++++ spec/components/budgets/map_component_spec.rb | 28 +++++++++++-- spec/factories/administration.rb | 9 ++++ spec/models/geozone_spec.rb | 42 +++++++++++++++++++ .../admin/budgets_wizard/wizard_spec.rb | 18 ++++---- spec/system/admin/geozones_spec.rb | 35 ++++++++++++++++ 33 files changed, 379 insertions(+), 26 deletions(-) create mode 100644 app/models/concerns/geojson_format_validator.rb create mode 100644 db/migrate/20181113184434_add_polygons_to_geozones.rb create mode 100644 db/migrate/20190109010131_add_geozone_to_headings.rb create mode 100644 spec/components/admin/budget_headings/form_component_spec.rb create mode 100644 spec/components/admin/geozones/index_component_spec.rb diff --git a/app/assets/javascripts/forms.js b/app/assets/javascripts/forms.js index 0a2dfe107..23f2b4c83 100644 --- a/app/assets/javascripts/forms.js +++ b/app/assets/javascripts/forms.js @@ -27,11 +27,12 @@ }); }, synchronizeInputs: function() { - var banners, inputs, processes, progress_bar; + var banners, geozones, inputs, processes, progress_bar; progress_bar = "[name='progress_bar[percentage]']"; processes = "[name='legislation_process[background_color]'], [name='legislation_process[font_color]']"; banners = "[name='banner[background_color]'], [name='banner[font_color]']"; - inputs = $(progress_bar + ", " + processes + ", " + banners); + geozones = "[name='geozone[color]']"; + inputs = $(progress_bar + ", " + processes + ", " + banners + ", " + geozones); inputs.on({ input: function() { $("[name='" + this.name + "']").val($(this).val()); diff --git a/app/assets/javascripts/map.js b/app/assets/javascripts/map.js index 80a8c6af6..2315ae1cf 100644 --- a/app/assets/javascripts/map.js +++ b/app/assets/javascripts/map.js @@ -78,6 +78,7 @@ } App.Map.addInvestmentsMarkers(investmentsMarkers, createMarker); + App.Map.addGeozones(map); }, leafletMap: function(element) { var centerData, mapCenterLatLng; @@ -194,6 +195,25 @@ map.attributionControl.setPrefix(App.Map.attributionPrefix()); L.tileLayer(mapTilesProvider, { attribution: mapAttribution }).addTo(map); }, + addGeozones: function(map) { + var geozones = $(map._container).data("geozones"); + + if (geozones) { + geozones.forEach(function(geozone) { + App.Map.addGeozone(geozone, map); + }); + } + }, + addGeozone: function(geozone, map) { + var polygon = L.polygon(geozone.outline_points, { + color: geozone.color, + fillOpacity: 0.3, + className: "map-polygon" + }); + + polygon.bindPopup(geozone.headings.join("
")); + polygon.addTo(map); + }, openMarkerPopup: function(e) { var marker = e.target; $.ajax("/investments/" + marker.options.id + "/json_data", { diff --git a/app/components/admin/budget_headings/form_component.html.erb b/app/components/admin/budget_headings/form_component.html.erb index bcae27da5..af488f177 100644 --- a/app/components/admin/budget_headings/form_component.html.erb +++ b/app/components/admin/budget_headings/form_component.html.erb @@ -18,6 +18,13 @@ <%= f.hidden_field :price, value: 0 %> <% end %> + <% if feature?(:map) %> + <%= f.select :geozone_id, + geozone_options, + include_blank: t("geozones.none"), + hint: t("admin.budget_headings.form.geozone_info") %> + <% end %> + <% if heading.budget.approval_voting? %> <%= f.number_field :max_ballot_lines, hint: t("admin.budget_headings.form.max_ballot_lines_info") %> diff --git a/app/components/admin/budget_headings/form_component.rb b/app/components/admin/budget_headings/form_component.rb index 96f08a3c0..16dd3cbfd 100644 --- a/app/components/admin/budget_headings/form_component.rb +++ b/app/components/admin/budget_headings/form_component.rb @@ -18,4 +18,8 @@ class Admin::BudgetHeadings::FormComponent < ApplicationComponent def single_heading? helpers.respond_to?(:single_heading?) && helpers.single_heading? end + + def geozone_options + Geozone.all.map { |geozone| [geozone.name, geozone.id] } + end end diff --git a/app/components/admin/budget_headings/headings_component.html.erb b/app/components/admin/budget_headings/headings_component.html.erb index a9269ab26..93f42a88f 100644 --- a/app/components/admin/budget_headings/headings_component.html.erb +++ b/app/components/admin/budget_headings/headings_component.html.erb @@ -10,6 +10,7 @@ <% if budget.approval_voting? %> <%= Budget::Heading.human_attribute_name(:max_ballot_lines) %> <% end %> + <%= Budget::Heading.human_attribute_name(:geozone_id) %> <%= t("admin.actions.actions") %> @@ -23,6 +24,9 @@ <% if budget.approval_voting? %> <%= heading.max_ballot_lines %> <% end %> + + <%= geozone_for(heading) %> + <%= render Admin::TableActionsComponent.new(heading) %> diff --git a/app/components/admin/budget_headings/headings_component.rb b/app/components/admin/budget_headings/headings_component.rb index 396944392..ea2c251ca 100644 --- a/app/components/admin/budget_headings/headings_component.rb +++ b/app/components/admin/budget_headings/headings_component.rb @@ -14,4 +14,12 @@ class Admin::BudgetHeadings::HeadingsComponent < ApplicationComponent def budget @budget ||= group.budget end + + def geozone_for(heading) + if heading.geozone + link_to heading.geozone.name, edit_admin_geozone_path(heading.geozone) + else + t("geozones.none") + end + end end diff --git a/app/components/admin/geozones/form_component.html.erb b/app/components/admin/geozones/form_component.html.erb index b36153e8a..7d55ecf00 100644 --- a/app/components/admin/geozones/form_component.html.erb +++ b/app/components/admin/geozones/form_component.html.erb @@ -20,6 +20,25 @@ <%= f.text_field :html_map_coordinates, hint: t("admin.geozones.geozone.coordinates_help") %> +
+ <%= f.text_area :geojson, rows: "10", hint: t("admin.geozones.geozone.geojson_help") %> +
+ +
+ <%= f.label :color, nil, for: "color_input", id: "color_input_label" %> +

+ <%= t("admin.geozones.geozone.color_help", format_help: t("admin.shared.color_help")) %> +

+
+
+ <%= f.text_field :color, label: false, type: :color %> +
+
+ <%= f.text_field :color, label: false, id: "color_input" %> +
+
+
+
<%= f.submit(value: t("admin.geozones.edit.form.submit_button"), class: "button success") %> diff --git a/app/components/admin/geozones/index_component.html.erb b/app/components/admin/geozones/index_component.html.erb index 5ed12f1c1..74aeca62d 100644 --- a/app/components/admin/geozones/index_component.html.erb +++ b/app/components/admin/geozones/index_component.html.erb @@ -9,6 +9,7 @@ <%= t("admin.geozones.geozone.external_code") %> <%= t("admin.geozones.geozone.census_code") %> <%= t("admin.geozones.geozone.coordinates") %> + <%= t("admin.geozones.geozone.geojson") %> <%= t("admin.actions.actions") %> @@ -19,7 +20,8 @@ <%= geozone.name %> <%= geozone.external_code %> <%= geozone.census_code %> - <%= geozone.html_map_coordinates %> + <%= yes_no_text(geozone.html_map_coordinates.present?) %> + <%= yes_no_text(geozone.geojson.present?) %> <%= render Admin::TableActionsComponent.new(geozone) %> diff --git a/app/components/admin/geozones/index_component.rb b/app/components/admin/geozones/index_component.rb index 4701c84b8..d9cee9e84 100644 --- a/app/components/admin/geozones/index_component.rb +++ b/app/components/admin/geozones/index_component.rb @@ -11,4 +11,12 @@ class Admin::Geozones::IndexComponent < ApplicationComponent def title t("admin.geozones.index.title") end + + def yes_no_text(condition) + if condition + t("shared.yes") + else + t("shared.no") + end + end end diff --git a/app/components/budgets/map_component.html.erb b/app/components/budgets/map_component.html.erb index fb89cd1f0..33aea81a7 100644 --- a/app/components/budgets/map_component.html.erb +++ b/app/components/budgets/map_component.html.erb @@ -1,4 +1,4 @@

<%= t("budgets.index.map") %>

- <%= render_map(nil, investments_coordinates: coordinates) %> + <%= render_map(nil, investments_coordinates: coordinates, geozones_data: geozones_data) %>
diff --git a/app/components/budgets/map_component.rb b/app/components/budgets/map_component.rb index 103a97808..dcbc427d6 100644 --- a/app/components/budgets/map_component.rb +++ b/app/components/budgets/map_component.rb @@ -21,4 +21,16 @@ class Budgets::MapComponent < ApplicationComponent MapLocation.where(investment_id: investments).map(&:json_data) end + + def geozones_data + budget.geozones.map do |geozone| + { + outline_points: geozone.outline_points, + color: geozone.color, + headings: budget.headings.where(geozone: geozone).map do |heading| + link_to heading.name, budget_investments_path(budget, heading_id: heading.id) + end + } + end + end end diff --git a/app/components/shared/map_location_component.rb b/app/components/shared/map_location_component.rb index 69371228b..e82f1f8a1 100644 --- a/app/components/shared/map_location_component.rb +++ b/app/components/shared/map_location_component.rb @@ -1,10 +1,11 @@ class Shared::MapLocationComponent < ApplicationComponent - attr_reader :investments_coordinates, :form + attr_reader :investments_coordinates, :form, :geozones_data - def initialize(map_location, investments_coordinates: nil, form: nil) + def initialize(map_location, investments_coordinates: nil, form: nil, geozones_data: nil) @map_location = map_location @investments_coordinates = investments_coordinates @form = form + @geozones_data = geozones_data end def map_location @@ -56,7 +57,8 @@ class Shared::MapLocationComponent < ApplicationComponent marker_remove_selector: "##{remove_marker_id}", marker_investments_coordinates: investments_coordinates, marker_latitude: map_location.latitude.presence, - marker_longitude: map_location.longitude.presence + marker_longitude: map_location.longitude.presence, + geozones: geozones_data }.merge(input_selectors) end diff --git a/app/controllers/admin/geozones_controller.rb b/app/controllers/admin/geozones_controller.rb index be2f01ecb..794a5985f 100644 --- a/app/controllers/admin/geozones_controller.rb +++ b/app/controllers/admin/geozones_controller.rb @@ -47,6 +47,6 @@ class Admin::GeozonesController < Admin::BaseController end def allowed_params - [:name, :external_code, :census_code, :html_map_coordinates] + [:name, :external_code, :census_code, :html_map_coordinates, :geojson, :color] end end diff --git a/app/controllers/concerns/admin/budget_headings_actions.rb b/app/controllers/concerns/admin/budget_headings_actions.rb index 202aa5ff1..1a1db8314 100644 --- a/app/controllers/concerns/admin/budget_headings_actions.rb +++ b/app/controllers/concerns/admin/budget_headings_actions.rb @@ -59,7 +59,7 @@ module Admin::BudgetHeadingsActions end def allowed_params - valid_attributes = [:price, :population, :allow_custom_content, :latitude, :longitude, :max_ballot_lines] + valid_attributes = [:price, :population, :allow_custom_content, :latitude, :longitude, :max_ballot_lines, :geozone_id] [*valid_attributes, translation_params(Budget::Heading)] end diff --git a/app/models/budget.rb b/app/models/budget.rb index 6cbe93b0d..4fc250bf4 100644 --- a/app/models/budget.rb +++ b/app/models/budget.rb @@ -34,6 +34,7 @@ class Budget < ApplicationRecord has_many :ballots, dependent: :destroy has_many :groups, dependent: :destroy has_many :headings, through: :groups + has_many :geozones, through: :headings has_many :lines, through: :ballots, class_name: "Budget::Ballot::Line" has_many :phases, class_name: "Budget::Phase" has_many :budget_administrators, dependent: :destroy diff --git a/app/models/budget/heading.rb b/app/models/budget/heading.rb index 94c61511e..7b8fbde57 100644 --- a/app/models/budget/heading.rb +++ b/app/models/budget/heading.rb @@ -22,6 +22,7 @@ class Budget end belongs_to :group + belongs_to :geozone has_many :investments has_many :content_blocks diff --git a/app/models/concerns/geojson_format_validator.rb b/app/models/concerns/geojson_format_validator.rb new file mode 100644 index 000000000..e2a88f0e8 --- /dev/null +++ b/app/models/concerns/geojson_format_validator.rb @@ -0,0 +1,23 @@ +class GeojsonFormatValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + if value.present? + geojson = parse_json(value) + + unless geojson?(geojson) + record.errors.add(attribute, :invalid) + end + end + end + + private + + def parse_json(geojson_data) + JSON.parse(geojson_data) rescue nil + end + + def geojson?(geojson) + return false unless geojson.is_a?(Hash) + + geojson.dig("geometry", "coordinates").is_a?(Array) + end +end diff --git a/app/models/geozone.rb b/app/models/geozone.rb index 57b5e6cb4..be2930ca8 100644 --- a/app/models/geozone.rb +++ b/app/models/geozone.rb @@ -4,7 +4,9 @@ class Geozone < ApplicationRecord has_many :proposals has_many :debates has_many :users + has_many :headings, class_name: "Budget::Heading", dependent: :nullify validates :name, presence: true + validates :geojson, geojson_format: true scope :public_for_api, -> { all } @@ -17,4 +19,28 @@ class Geozone < ApplicationRecord association.klass.where(geozone: self).empty? end end + + def outline_points + normalized_coordinates.map { |longlat| [longlat.last, longlat.first] } + end + + private + + def normalized_coordinates + if geojson.present? + if geojson.match(/"coordinates"\s*:\s*\[{4}/) + coordinates.reduce([], :concat).reduce([], :concat) + elsif geojson.match(/"coordinates"\s*:\s*\[{3}/) + coordinates.reduce([], :concat) + else + coordinates + end + else + [] + end + end + + def coordinates + JSON.parse(geojson)["geometry"]["coordinates"] + end end diff --git a/config/locales/en/activerecord.yml b/config/locales/en/activerecord.yml index d6563e507..f2089ca9c 100644 --- a/config/locales/en/activerecord.yml +++ b/config/locales/en/activerecord.yml @@ -201,7 +201,9 @@ en: geozone: name: Name external_code: "External code (optional)" + color: "Color (optional)" census_code: "Census code (optional)" + geojson: "GeoJSON data (optional)" html_map_coordinates: "HTML Coordinates (optional)" milestone: status_id: "Current status (optional)" @@ -545,6 +547,10 @@ en: attributes: max_per_day: invalid: "You have reached the maximum number of private messages per day" + geozone: + attributes: + geojson: + invalid: "The GeoJSON provided does not follow the correct format. It must follow the \"Polygon\" or \"MultiPolygon\" type format." image: attributes: attachment: diff --git a/config/locales/en/admin.yml b/config/locales/en/admin.yml index e40fba1c8..b9eafb023 100644 --- a/config/locales/en/admin.yml +++ b/config/locales/en/admin.yml @@ -198,6 +198,7 @@ en: success_notice: "Heading deleted successfully" unable_notice: "You cannot delete a Heading that has associated investments" form: + geozone_info: "When a heading is associated with a geozone, the geozone will appear on this budget's map if the geozone includes GeoJSON data." max_ballot_lines_info: 'Maximum number of projects a user can vote on this heading during the "Voting projects" phase. Only for budgets using approval voting.' population_info: "Budget Heading population field is used for Statistic purposes at the end of the Budget to show for each Heading that represents an area with population what percentage voted. The field is optional so you can leave it empty if it doesn't apply." coordinates_info: "If latitude and longitude are provided, the investments page for this heading will include a map. This map will be centered using those coordinates." @@ -1391,8 +1392,11 @@ en: external_code: External code census_code: Census code code_help: Response code for this geozone on the census API - coordinates: Coordinates + color_help: "Color of the zone in a budget's map. %{format_help}" + coordinates: Coordinates available coordinates_help: Coordinates that will generate a clickable area on an HTML image map + geojson: GeoJSON available + geojson_help: "Must follow the \"Polygon\" or \"MultiPolygon\" type format; on a budget's map, a polygon based on this data will appear when a heading is associated to this geozone" create: notice: "Geozone created successfully" edit: diff --git a/config/locales/es/activerecord.yml b/config/locales/es/activerecord.yml index ed149e75b..b3a321a76 100644 --- a/config/locales/es/activerecord.yml +++ b/config/locales/es/activerecord.yml @@ -200,8 +200,10 @@ es: description: "Descripción" geozone: name: Nombre + color: "Color (opcional)" external_code: "Código externo (opcional)" census_code: "Código del censo (opcional)" + geojson: "Datos GeoJSON (opcional)" html_map_coordinates: "Coordenadas HTML (opcional)" milestone: status_id: "Estado actual (opcional)" @@ -545,6 +547,10 @@ es: attributes: max_per_day: invalid: "Has llegado al número máximo de mensajes privados por día" + geozone: + attributes: + geojson: + invalid: "Los datos GeoJSON proporcionados no tienen el formato correcto. Deben tener un tipo del formato \"Polygon\" o \"MultiPolygon\"." image: attributes: attachment: diff --git a/config/locales/es/admin.yml b/config/locales/es/admin.yml index 8234b6cc8..2514975b1 100644 --- a/config/locales/es/admin.yml +++ b/config/locales/es/admin.yml @@ -198,6 +198,7 @@ es: success_notice: "Partida presupuestaria eliminada correctamente" unable_notice: "No se puede eliminar una partida presupuestaria con proyectos asociados" form: + geozone_info: "Cuando se asocia una partida a una zona, la zona aparecerá en el mapa de este presupuesto si la zona incluye datos de GeoJSON." max_ballot_lines_info: 'Máximo número de proyectos que un usuario puede votar en esta partida durante la fase "Votación final". Solamente se aplica a presupuestos con votación por aprobación.' population_info: "El campo población de las partidas presupuestarias se usa con fines estadísticos únicamente, con el objetivo de mostrar el porcentaje de votos habidos en cada partida que represente un área con población. Es un campo opcional, así que puedes dejarlo en blanco si no aplica." coordinates_info: "Si se añaden los campos latitud y longitud, en la página de proyectos de esta partida aparecerá un mapa, que estará centrado en esas coordenadas." @@ -1391,8 +1392,11 @@ es: external_code: Código externo census_code: Código del censo code_help: Código de respuesta para esta zona en la API del censo - coordinates: Coordenadas + color_help: "Color con el que aparecerá esta zona en el mapa de un presupuesto. %{format_help}" + coordinates: Coordenadas disponibles coordinates_help: Coordenadas que generarán una zona clicable en un mapa de imagen HTML + geojson: GeoJSON disponible + geojson_help: "Deben tener un tipo del formato \"Polygon\" o \"MultiPolygon\"; en el mapa de un presupuesto aparecerá un polígono basado en estos datos si una partida se asocia a esta geozona" create: notice: "Zona creada correctamente" edit: diff --git a/db/migrate/20181113184434_add_polygons_to_geozones.rb b/db/migrate/20181113184434_add_polygons_to_geozones.rb new file mode 100644 index 000000000..b3b4687c2 --- /dev/null +++ b/db/migrate/20181113184434_add_polygons_to_geozones.rb @@ -0,0 +1,8 @@ +class AddPolygonsToGeozones < ActiveRecord::Migration[5.0] + def change + change_table :geozones do |t| + t.text :geojson + t.string :color + end + end +end diff --git a/db/migrate/20190109010131_add_geozone_to_headings.rb b/db/migrate/20190109010131_add_geozone_to_headings.rb new file mode 100644 index 000000000..563d36b31 --- /dev/null +++ b/db/migrate/20190109010131_add_geozone_to_headings.rb @@ -0,0 +1,5 @@ +class AddGeozoneToHeadings < ActiveRecord::Migration[5.0] + def change + add_reference :budget_headings, :geozone, index: true, foreign_key: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 75b8d5713..165b1ffa7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -238,9 +238,11 @@ ActiveRecord::Schema.define(version: 2023_05_23_090028) do t.boolean "allow_custom_content", default: false t.text "latitude" t.text "longitude" + t.integer "geozone_id" t.integer "max_ballot_lines", default: 1 t.datetime "created_at" t.datetime "updated_at" + t.index ["geozone_id"], name: "index_budget_headings_on_geozone_id" t.index ["group_id"], name: "index_budget_headings_on_group_id" end @@ -635,6 +637,8 @@ ActiveRecord::Schema.define(version: 2023_05_23_090028) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "census_code" + t.text "geojson" + t.string "color" end create_table "geozones_polls", id: :serial, force: :cascade do |t| @@ -1775,6 +1779,7 @@ ActiveRecord::Schema.define(version: 2023_05_23_090028) do add_foreign_key "administrators", "users" add_foreign_key "budget_administrators", "administrators" add_foreign_key "budget_administrators", "budgets" + add_foreign_key "budget_headings", "geozones" add_foreign_key "budget_investments", "communities" add_foreign_key "budget_valuators", "budgets" add_foreign_key "budget_valuators", "valuators" diff --git a/spec/components/admin/budget_headings/form_component_spec.rb b/spec/components/admin/budget_headings/form_component_spec.rb new file mode 100644 index 000000000..11ade0905 --- /dev/null +++ b/spec/components/admin/budget_headings/form_component_spec.rb @@ -0,0 +1,32 @@ +require "rails_helper" + +describe Admin::BudgetHeadings::FormComponent do + describe "geozone field" do + let(:heading) { create(:budget_heading) } + let(:component) { Admin::BudgetHeadings::FormComponent.new(heading, path: "/", action: nil) } + before { Setting["feature.map"] = true } + + it "is shown when the map feature is enabled" do + render_inline component + + expect(page).to have_select "Scope of operation" + end + + it "is not shown when the map feature is disabled" do + Setting["feature.map"] = false + + render_inline component + + expect(page).not_to have_select "Scope of operation" + end + + it "includes all existing geozones plus an option for all city" do + create(:geozone, name: "Under the sea") + create(:geozone, name: "Above the skies") + + render_inline component + + expect(page).to have_select "Scope of operation", options: ["All city", "Under the sea", "Above the skies"] + end + end +end diff --git a/spec/components/admin/budget_headings/headings_component_spec.rb b/spec/components/admin/budget_headings/headings_component_spec.rb index e45549ce2..e36c5ab4c 100644 --- a/spec/components/admin/budget_headings/headings_component_spec.rb +++ b/spec/components/admin/budget_headings/headings_component_spec.rb @@ -1,6 +1,6 @@ require "rails_helper" -describe Admin::BudgetHeadings::HeadingsComponent do +describe Admin::BudgetHeadings::HeadingsComponent, controller: Admin::BaseController do it "includes group name in the message when there are no headings" do group = create(:budget_group, name: "Whole planet") @@ -9,4 +9,22 @@ describe Admin::BudgetHeadings::HeadingsComponent do expect(page.text.strip).to eq "There are no headings in the Whole planet group." expect(page).to have_css "strong", exact_text: "Whole planet" end + + describe "#geozone_for" do + it "shows the geozone associated to the heading" do + heading = create(:budget_heading, name: "Local", geozone: create(:geozone, name: "Here")) + + render_inline Admin::BudgetHeadings::HeadingsComponent.new(heading.group.headings) + + expect(page.find("tr", text: "Local")).to have_content "Here" + end + + it "shows a generic location for headings with no associated geozone" do + heading = create(:budget_heading, name: "Universal", geozone: nil) + + render_inline Admin::BudgetHeadings::HeadingsComponent.new(heading.group.headings) + + expect(page.find("tr", text: "Universal")).to have_content "All city" + end + end end diff --git a/spec/components/admin/geozones/index_component_spec.rb b/spec/components/admin/geozones/index_component_spec.rb new file mode 100644 index 000000000..6d3fe3dd4 --- /dev/null +++ b/spec/components/admin/geozones/index_component_spec.rb @@ -0,0 +1,23 @@ +require "rails_helper" + +describe Admin::Geozones::IndexComponent, controller: Admin::BaseController do + describe "Coordinates description" do + it "includes whether coordinates are defined or not" do + geozones = [ + create(:geozone, :with_geojson, name: "GeoJSON", external_code: "1", census_code: "2"), + create(:geozone, :with_html_coordinates, name: "HTML", external_code: "3", census_code: "4"), + create(:geozone, :with_geojson, :with_html_coordinates, name: "With both", external_code: "6", census_code: "7"), + create(:geozone, name: "With none", external_code: "8", census_code: "9") + ] + + render_inline Admin::Geozones::IndexComponent.new(geozones) + + expect(page).to have_table with_rows: [ + ["GeoJSON", "1", "2", "No", "Yes", "Edit Delete"], + ["HTML", "3", "4", "Yes", "No", "Edit Delete"], + ["With both", "6", "7", "Yes", "Yes", "Edit Delete"], + ["With none", "8", "9", "No", "No", "Edit Delete"] + ] + end + end +end diff --git a/spec/components/budgets/map_component_spec.rb b/spec/components/budgets/map_component_spec.rb index 60996af24..d0c44f2ab 100644 --- a/spec/components/budgets/map_component_spec.rb +++ b/spec/components/budgets/map_component_spec.rb @@ -1,11 +1,13 @@ require "rails_helper" describe Budgets::MapComponent do - let(:budget) { build(:budget) } + before { Setting["feature.map"] = true } + let(:budget) { create(:budget, :accepting) } describe "#render?" do + let(:budget) { build(:budget) } + it "is rendered after the informing phase when the map feature is enabled" do - Setting["feature.map"] = true budget.phase = "accepting" render_inline Budgets::MapComponent.new(budget) @@ -14,7 +16,6 @@ describe Budgets::MapComponent do end it "is not rendered during the informing phase" do - Setting["feature.map"] = true budget.phase = "informing" render_inline Budgets::MapComponent.new(budget) @@ -31,4 +32,25 @@ describe Budgets::MapComponent do expect(page).not_to be_rendered end end + + describe "#geozones_data" do + it "renders data for the geozones associated with the budget" do + create(:budget_heading, geozone: create(:geozone, color: "#0000ff"), budget: budget) + create(:budget_heading, geozone: create(:geozone, color: "#ff0000"), budget: create(:budget)) + + render_inline Budgets::MapComponent.new(budget) + + expect(page).to have_css "[data-geozones*='#0000ff']" + expect(page).not_to have_css "[data-geozones*='#ff0000']" + end + + it "renders empty geozone data when there are no geozones" do + create(:budget_heading, geozone: nil, budget: budget) + create(:budget_heading, geozone: create(:geozone, color: "#ff0000"), budget: create(:budget)) + + render_inline Budgets::MapComponent.new(budget) + + expect(page).to have_css "[data-geozones='[]']" + end + end end diff --git a/spec/factories/administration.rb b/spec/factories/administration.rb index d366cb3a3..780bd1e16 100644 --- a/spec/factories/administration.rb +++ b/spec/factories/administration.rb @@ -8,10 +8,19 @@ FactoryBot.define do sequence(:name) { |n| "District #{n}" } sequence(:external_code, &:to_s) sequence(:census_code, &:to_s) + color { "#0081aa" } trait :in_census do census_code { "01" } end + + trait :with_html_coordinates do + html_map_coordinates { "30,139,45,153,77,148,107,165" } + end + + trait :with_geojson do + geojson { '{ "geometry": { "type": "Polygon", "coordinates": [[-0.117,51.513],[-0.118,51.512],[-0.119,51.514]] } }' } + end end factory :banner do diff --git a/spec/models/geozone_spec.rb b/spec/models/geozone_spec.rb index 3f3dfb9df..65c269610 100644 --- a/spec/models/geozone_spec.rb +++ b/spec/models/geozone_spec.rb @@ -12,6 +12,19 @@ describe Geozone do expect(geozone).not_to be_valid end + it "is valid without geojson" do + geozone.geojson = nil + expect(geozone).to be_valid + end + + it "is not valid with invalid geojson file format" do + geozone.geojson = '{"geo\":{"type":"Incorrect key","coordinates": [ + [40.8792937308316, -3.9259027239257], + [40.8788966596619, -3.9249047078766], + [40.8789131852224, -3.9247799675785]]}}' + expect(geozone).not_to be_valid + end + describe "#safe_to_destroy?" do let(:geozone) { create(:geozone) } @@ -33,5 +46,34 @@ describe Geozone do create(:debate, geozone: geozone) expect(geozone).not_to be_safe_to_destroy end + + it "is false when already linked to a heading" do + create(:budget_heading, geozone: geozone) + expect(geozone).not_to be_safe_to_destroy + end + end + + describe "#outline_points" do + it "returns empty array when geojson is nil" do + expect(geozone.outline_points).to eq([]) + end + + it "returns coordinates array when geojson is not nil" do + geozone = build(:geozone, geojson: '{ + "geometry": { + "type": "Polygon", + "coordinates": [ + [40.8792937308316, -3.9259027239257], + [40.8788966596619, -3.9249047078766], + [40.8789131852224, -3.9247799675785] + ] + } + }') + + expect(geozone.outline_points).to eq( + [[-3.9259027239257, 40.8792937308316], + [-3.9249047078766, 40.8788966596619], + [-3.9247799675785, 40.8789131852224]]) + end end end diff --git a/spec/system/admin/budgets_wizard/wizard_spec.rb b/spec/system/admin/budgets_wizard/wizard_spec.rb index bce830d92..85cb9c514 100644 --- a/spec/system/admin/budgets_wizard/wizard_spec.rb +++ b/spec/system/admin/budgets_wizard/wizard_spec.rb @@ -112,19 +112,15 @@ describe "Budgets creation wizard", :admin do click_link "Finish" within "section", text: "Heading groups" do - within "section", text: "All city" do - within_table "Headings in All city" do - expect(page).to have_css "tbody tr", count: 1 - expect(page).to have_content "All city" - end + within_table "Headings in All city" do + expect(page).to have_css "tbody tr", count: 1 + expect(page).to have_content "All city" end - within "section", text: "Districts" do - within_table "Headings in Districts" do - expect(page).to have_css "tbody tr", count: 2 - expect(page).to have_content "North" - expect(page).to have_content "South" - end + within_table "Headings in Districts" do + expect(page).to have_css "tbody tr", count: 2 + expect(page).to have_content "North" + expect(page).to have_content "South" end end end diff --git a/spec/system/admin/geozones_spec.rb b/spec/system/admin/geozones_spec.rb index 392f8f8aa..9328916b7 100644 --- a/spec/system/admin/geozones_spec.rb +++ b/spec/system/admin/geozones_spec.rb @@ -107,4 +107,39 @@ describe "Admin geozones", :admin do expect(page).to have_content "Delete me!" end end + + scenario "Show polygons when a heading is associated with a geozone" do + Setting["feature.map"] = true + + geojson = '{ "geometry": { "type": "Polygon", "coordinates": [[-0.1,51.5],[-0.2,51.4],[-0.3,51.6]] } }' + geozone = create(:geozone, name: "Polygon me!") + budget = create(:budget) + group = create(:budget_group, budget: budget) + heading = create(:budget_heading, name: "Area 51", group: group) + + visit edit_admin_geozone_path(geozone) + fill_in "GeoJSON data (optional)", with: geojson + fill_in "Color (optional)", with: "#f5c211" + click_button "Save changes" + + expect(page).to have_content "Geozone updated successfully" + + visit edit_admin_budget_group_heading_path(budget, group, heading) + select "Polygon me!", from: "Scope of operation" + + click_button "Save heading" + + expect(page).to have_content "Heading updated successfully" + + visit budget_path(budget) + + expect(page).to have_css ".map-polygon[fill='#f5c211']" + within(".map-location") { expect(page).not_to have_link "Area 51" } + + find(".map-polygon").click + + within ".map-location" do + expect(page).to have_link "Area 51", href: budget_investments_path(budget, heading_id: heading.id) + end + end end