Add polygon geographies to Budgets' map

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 <microweb10@gmail.com>
Co-Authored-By: Javi Martín <javim@elretirao.net>
This commit is contained in:
Matheus Miranda
2018-11-13 17:22:55 -02:00
committed by Javi Martín
parent d1f1e1dfea
commit de13e789dd
33 changed files with 379 additions and 26 deletions

View File

@@ -27,11 +27,12 @@
}); });
}, },
synchronizeInputs: function() { synchronizeInputs: function() {
var banners, inputs, processes, progress_bar; var banners, geozones, inputs, processes, progress_bar;
progress_bar = "[name='progress_bar[percentage]']"; progress_bar = "[name='progress_bar[percentage]']";
processes = "[name='legislation_process[background_color]'], [name='legislation_process[font_color]']"; processes = "[name='legislation_process[background_color]'], [name='legislation_process[font_color]']";
banners = "[name='banner[background_color]'], [name='banner[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({ inputs.on({
input: function() { input: function() {
$("[name='" + this.name + "']").val($(this).val()); $("[name='" + this.name + "']").val($(this).val());

View File

@@ -78,6 +78,7 @@
} }
App.Map.addInvestmentsMarkers(investmentsMarkers, createMarker); App.Map.addInvestmentsMarkers(investmentsMarkers, createMarker);
App.Map.addGeozones(map);
}, },
leafletMap: function(element) { leafletMap: function(element) {
var centerData, mapCenterLatLng; var centerData, mapCenterLatLng;
@@ -194,6 +195,25 @@
map.attributionControl.setPrefix(App.Map.attributionPrefix()); map.attributionControl.setPrefix(App.Map.attributionPrefix());
L.tileLayer(mapTilesProvider, { attribution: mapAttribution }).addTo(map); 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("<br>"));
polygon.addTo(map);
},
openMarkerPopup: function(e) { openMarkerPopup: function(e) {
var marker = e.target; var marker = e.target;
$.ajax("/investments/" + marker.options.id + "/json_data", { $.ajax("/investments/" + marker.options.id + "/json_data", {

View File

@@ -18,6 +18,13 @@
<%= f.hidden_field :price, value: 0 %> <%= f.hidden_field :price, value: 0 %>
<% end %> <% 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? %> <% if heading.budget.approval_voting? %>
<%= f.number_field :max_ballot_lines, <%= f.number_field :max_ballot_lines,
hint: t("admin.budget_headings.form.max_ballot_lines_info") %> hint: t("admin.budget_headings.form.max_ballot_lines_info") %>

View File

@@ -18,4 +18,8 @@ class Admin::BudgetHeadings::FormComponent < ApplicationComponent
def single_heading? def single_heading?
helpers.respond_to?(:single_heading?) && helpers.single_heading? helpers.respond_to?(:single_heading?) && helpers.single_heading?
end end
def geozone_options
Geozone.all.map { |geozone| [geozone.name, geozone.id] }
end
end end

View File

@@ -10,6 +10,7 @@
<% if budget.approval_voting? %> <% if budget.approval_voting? %>
<th><%= Budget::Heading.human_attribute_name(:max_ballot_lines) %></th> <th><%= Budget::Heading.human_attribute_name(:max_ballot_lines) %></th>
<% end %> <% end %>
<th><%= Budget::Heading.human_attribute_name(:geozone_id) %></th>
<th><%= t("admin.actions.actions") %></th> <th><%= t("admin.actions.actions") %></th>
</tr> </tr>
</thead> </thead>
@@ -23,6 +24,9 @@
<% if budget.approval_voting? %> <% if budget.approval_voting? %>
<td><%= heading.max_ballot_lines %></td> <td><%= heading.max_ballot_lines %></td>
<% end %> <% end %>
<td>
<%= geozone_for(heading) %>
</td>
<td> <td>
<%= render Admin::TableActionsComponent.new(heading) %> <%= render Admin::TableActionsComponent.new(heading) %>
</td> </td>

View File

@@ -14,4 +14,12 @@ class Admin::BudgetHeadings::HeadingsComponent < ApplicationComponent
def budget def budget
@budget ||= group.budget @budget ||= group.budget
end 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 end

View File

@@ -20,6 +20,25 @@
<%= f.text_field :html_map_coordinates, hint: t("admin.geozones.geozone.coordinates_help") %> <%= f.text_field :html_map_coordinates, hint: t("admin.geozones.geozone.coordinates_help") %>
</div> </div>
<div class="column">
<%= f.text_area :geojson, rows: "10", hint: t("admin.geozones.geozone.geojson_help") %>
</div>
<div class="small-12 large-3 column">
<%= f.label :color, nil, for: "color_input", id: "color_input_label" %>
<p class="help-text">
<%= t("admin.geozones.geozone.color_help", format_help: t("admin.shared.color_help")) %>
</p>
<div class="row collapse">
<div class="small-12 medium-6 column">
<%= f.text_field :color, label: false, type: :color %>
</div>
<div class="small-12 medium-6 column">
<%= f.text_field :color, label: false, id: "color_input" %>
</div>
</div>
</div>
<div class="small-12 column"> <div class="small-12 column">
<%= f.submit(value: t("admin.geozones.edit.form.submit_button"), <%= f.submit(value: t("admin.geozones.edit.form.submit_button"),
class: "button success") %> class: "button success") %>

View File

@@ -9,6 +9,7 @@
<th><%= t("admin.geozones.geozone.external_code") %></th> <th><%= t("admin.geozones.geozone.external_code") %></th>
<th><%= t("admin.geozones.geozone.census_code") %></th> <th><%= t("admin.geozones.geozone.census_code") %></th>
<th><%= t("admin.geozones.geozone.coordinates") %></th> <th><%= t("admin.geozones.geozone.coordinates") %></th>
<th><%= t("admin.geozones.geozone.geojson") %></th>
<th><%= t("admin.actions.actions") %></th> <th><%= t("admin.actions.actions") %></th>
</tr> </tr>
</thead> </thead>
@@ -19,7 +20,8 @@
<td><%= geozone.name %></td> <td><%= geozone.name %></td>
<td><%= geozone.external_code %></td> <td><%= geozone.external_code %></td>
<td><%= geozone.census_code %></td> <td><%= geozone.census_code %></td>
<td class="break"><%= geozone.html_map_coordinates %></td> <td><%= yes_no_text(geozone.html_map_coordinates.present?) %></td>
<td><%= yes_no_text(geozone.geojson.present?) %></td>
<td> <td>
<%= render Admin::TableActionsComponent.new(geozone) %> <%= render Admin::TableActionsComponent.new(geozone) %>
</td> </td>

View File

@@ -11,4 +11,12 @@ class Admin::Geozones::IndexComponent < ApplicationComponent
def title def title
t("admin.geozones.index.title") t("admin.geozones.index.title")
end end
def yes_no_text(condition)
if condition
t("shared.yes")
else
t("shared.no")
end
end
end end

View File

@@ -1,4 +1,4 @@
<div class="budgets-map"> <div class="budgets-map">
<h2><%= t("budgets.index.map") %></h2> <h2><%= t("budgets.index.map") %></h2>
<%= render_map(nil, investments_coordinates: coordinates) %> <%= render_map(nil, investments_coordinates: coordinates, geozones_data: geozones_data) %>
</div> </div>

View File

@@ -21,4 +21,16 @@ class Budgets::MapComponent < ApplicationComponent
MapLocation.where(investment_id: investments).map(&:json_data) MapLocation.where(investment_id: investments).map(&:json_data)
end 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 end

View File

@@ -1,10 +1,11 @@
class Shared::MapLocationComponent < ApplicationComponent 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 @map_location = map_location
@investments_coordinates = investments_coordinates @investments_coordinates = investments_coordinates
@form = form @form = form
@geozones_data = geozones_data
end end
def map_location def map_location
@@ -56,7 +57,8 @@ class Shared::MapLocationComponent < ApplicationComponent
marker_remove_selector: "##{remove_marker_id}", marker_remove_selector: "##{remove_marker_id}",
marker_investments_coordinates: investments_coordinates, marker_investments_coordinates: investments_coordinates,
marker_latitude: map_location.latitude.presence, marker_latitude: map_location.latitude.presence,
marker_longitude: map_location.longitude.presence marker_longitude: map_location.longitude.presence,
geozones: geozones_data
}.merge(input_selectors) }.merge(input_selectors)
end end

View File

@@ -47,6 +47,6 @@ class Admin::GeozonesController < Admin::BaseController
end end
def allowed_params def allowed_params
[:name, :external_code, :census_code, :html_map_coordinates] [:name, :external_code, :census_code, :html_map_coordinates, :geojson, :color]
end end
end end

View File

@@ -59,7 +59,7 @@ module Admin::BudgetHeadingsActions
end end
def allowed_params 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)] [*valid_attributes, translation_params(Budget::Heading)]
end end

View File

@@ -34,6 +34,7 @@ class Budget < ApplicationRecord
has_many :ballots, dependent: :destroy has_many :ballots, dependent: :destroy
has_many :groups, dependent: :destroy has_many :groups, dependent: :destroy
has_many :headings, through: :groups has_many :headings, through: :groups
has_many :geozones, through: :headings
has_many :lines, through: :ballots, class_name: "Budget::Ballot::Line" has_many :lines, through: :ballots, class_name: "Budget::Ballot::Line"
has_many :phases, class_name: "Budget::Phase" has_many :phases, class_name: "Budget::Phase"
has_many :budget_administrators, dependent: :destroy has_many :budget_administrators, dependent: :destroy

View File

@@ -22,6 +22,7 @@ class Budget
end end
belongs_to :group belongs_to :group
belongs_to :geozone
has_many :investments has_many :investments
has_many :content_blocks has_many :content_blocks

View File

@@ -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

View File

@@ -4,7 +4,9 @@ class Geozone < ApplicationRecord
has_many :proposals has_many :proposals
has_many :debates has_many :debates
has_many :users has_many :users
has_many :headings, class_name: "Budget::Heading", dependent: :nullify
validates :name, presence: true validates :name, presence: true
validates :geojson, geojson_format: true
scope :public_for_api, -> { all } scope :public_for_api, -> { all }
@@ -17,4 +19,28 @@ class Geozone < ApplicationRecord
association.klass.where(geozone: self).empty? association.klass.where(geozone: self).empty?
end end
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 end

View File

@@ -201,7 +201,9 @@ en:
geozone: geozone:
name: Name name: Name
external_code: "External code (optional)" external_code: "External code (optional)"
color: "Color (optional)"
census_code: "Census code (optional)" census_code: "Census code (optional)"
geojson: "GeoJSON data (optional)"
html_map_coordinates: "HTML <map> Coordinates (optional)" html_map_coordinates: "HTML <map> Coordinates (optional)"
milestone: milestone:
status_id: "Current status (optional)" status_id: "Current status (optional)"
@@ -545,6 +547,10 @@ en:
attributes: attributes:
max_per_day: max_per_day:
invalid: "You have reached the maximum number of private messages 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: image:
attributes: attributes:
attachment: attachment:

View File

@@ -198,6 +198,7 @@ en:
success_notice: "Heading deleted successfully" success_notice: "Heading deleted successfully"
unable_notice: "You cannot delete a Heading that has associated investments" unable_notice: "You cannot delete a Heading that has associated investments"
form: 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.' 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." 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." 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 external_code: External code
census_code: Census code census_code: Census code
code_help: Response code for this geozone on the census API 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 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: create:
notice: "Geozone created successfully" notice: "Geozone created successfully"
edit: edit:

View File

@@ -200,8 +200,10 @@ es:
description: "Descripción" description: "Descripción"
geozone: geozone:
name: Nombre name: Nombre
color: "Color (opcional)"
external_code: "Código externo (opcional)" external_code: "Código externo (opcional)"
census_code: "Código del censo (opcional)" census_code: "Código del censo (opcional)"
geojson: "Datos GeoJSON (opcional)"
html_map_coordinates: "Coordenadas HTML <map> (opcional)" html_map_coordinates: "Coordenadas HTML <map> (opcional)"
milestone: milestone:
status_id: "Estado actual (opcional)" status_id: "Estado actual (opcional)"
@@ -545,6 +547,10 @@ es:
attributes: attributes:
max_per_day: max_per_day:
invalid: "Has llegado al número máximo de mensajes privados por día" 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: image:
attributes: attributes:
attachment: attachment:

View File

@@ -198,6 +198,7 @@ es:
success_notice: "Partida presupuestaria eliminada correctamente" success_notice: "Partida presupuestaria eliminada correctamente"
unable_notice: "No se puede eliminar una partida presupuestaria con proyectos asociados" unable_notice: "No se puede eliminar una partida presupuestaria con proyectos asociados"
form: 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.' 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." 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." 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 external_code: Código externo
census_code: Código del censo census_code: Código del censo
code_help: Código de respuesta para esta zona en la API 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 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: create:
notice: "Zona creada correctamente" notice: "Zona creada correctamente"
edit: edit:

View File

@@ -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

View File

@@ -0,0 +1,5 @@
class AddGeozoneToHeadings < ActiveRecord::Migration[5.0]
def change
add_reference :budget_headings, :geozone, index: true, foreign_key: true
end
end

View File

@@ -238,9 +238,11 @@ ActiveRecord::Schema.define(version: 2023_05_23_090028) do
t.boolean "allow_custom_content", default: false t.boolean "allow_custom_content", default: false
t.text "latitude" t.text "latitude"
t.text "longitude" t.text "longitude"
t.integer "geozone_id"
t.integer "max_ballot_lines", default: 1 t.integer "max_ballot_lines", default: 1
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_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" t.index ["group_id"], name: "index_budget_headings_on_group_id"
end end
@@ -635,6 +637,8 @@ ActiveRecord::Schema.define(version: 2023_05_23_090028) do
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "census_code" t.string "census_code"
t.text "geojson"
t.string "color"
end end
create_table "geozones_polls", id: :serial, force: :cascade do |t| 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 "administrators", "users"
add_foreign_key "budget_administrators", "administrators" add_foreign_key "budget_administrators", "administrators"
add_foreign_key "budget_administrators", "budgets" add_foreign_key "budget_administrators", "budgets"
add_foreign_key "budget_headings", "geozones"
add_foreign_key "budget_investments", "communities" add_foreign_key "budget_investments", "communities"
add_foreign_key "budget_valuators", "budgets" add_foreign_key "budget_valuators", "budgets"
add_foreign_key "budget_valuators", "valuators" add_foreign_key "budget_valuators", "valuators"

View File

@@ -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

View File

@@ -1,6 +1,6 @@
require "rails_helper" 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 it "includes group name in the message when there are no headings" do
group = create(:budget_group, name: "Whole planet") 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.text.strip).to eq "There are no headings in the Whole planet group."
expect(page).to have_css "strong", exact_text: "Whole planet" expect(page).to have_css "strong", exact_text: "Whole planet"
end 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 end

View File

@@ -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

View File

@@ -1,11 +1,13 @@
require "rails_helper" require "rails_helper"
describe Budgets::MapComponent do describe Budgets::MapComponent do
let(:budget) { build(:budget) } before { Setting["feature.map"] = true }
let(:budget) { create(:budget, :accepting) }
describe "#render?" do describe "#render?" do
let(:budget) { build(:budget) }
it "is rendered after the informing phase when the map feature is enabled" do it "is rendered after the informing phase when the map feature is enabled" do
Setting["feature.map"] = true
budget.phase = "accepting" budget.phase = "accepting"
render_inline Budgets::MapComponent.new(budget) render_inline Budgets::MapComponent.new(budget)
@@ -14,7 +16,6 @@ describe Budgets::MapComponent do
end end
it "is not rendered during the informing phase" do it "is not rendered during the informing phase" do
Setting["feature.map"] = true
budget.phase = "informing" budget.phase = "informing"
render_inline Budgets::MapComponent.new(budget) render_inline Budgets::MapComponent.new(budget)
@@ -31,4 +32,25 @@ describe Budgets::MapComponent do
expect(page).not_to be_rendered expect(page).not_to be_rendered
end end
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 end

View File

@@ -8,10 +8,19 @@ FactoryBot.define do
sequence(:name) { |n| "District #{n}" } sequence(:name) { |n| "District #{n}" }
sequence(:external_code, &:to_s) sequence(:external_code, &:to_s)
sequence(:census_code, &:to_s) sequence(:census_code, &:to_s)
color { "#0081aa" }
trait :in_census do trait :in_census do
census_code { "01" } census_code { "01" }
end 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 end
factory :banner do factory :banner do

View File

@@ -12,6 +12,19 @@ describe Geozone do
expect(geozone).not_to be_valid expect(geozone).not_to be_valid
end 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 describe "#safe_to_destroy?" do
let(:geozone) { create(:geozone) } let(:geozone) { create(:geozone) }
@@ -33,5 +46,34 @@ describe Geozone do
create(:debate, geozone: geozone) create(:debate, geozone: geozone)
expect(geozone).not_to be_safe_to_destroy expect(geozone).not_to be_safe_to_destroy
end 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
end end

View File

@@ -112,14 +112,11 @@ describe "Budgets creation wizard", :admin do
click_link "Finish" click_link "Finish"
within "section", text: "Heading groups" do within "section", text: "Heading groups" do
within "section", text: "All city" do
within_table "Headings in All city" do within_table "Headings in All city" do
expect(page).to have_css "tbody tr", count: 1 expect(page).to have_css "tbody tr", count: 1
expect(page).to have_content "All city" expect(page).to have_content "All city"
end end
end
within "section", text: "Districts" do
within_table "Headings in Districts" do within_table "Headings in Districts" do
expect(page).to have_css "tbody tr", count: 2 expect(page).to have_css "tbody tr", count: 2
expect(page).to have_content "North" expect(page).to have_content "North"
@@ -127,5 +124,4 @@ describe "Budgets creation wizard", :admin do
end end
end end
end end
end
end end

View File

@@ -107,4 +107,39 @@ describe "Admin geozones", :admin do
expect(page).to have_content "Delete me!" expect(page).to have_content "Delete me!"
end end
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 end