diff --git a/Gemfile b/Gemfile index a6c1dc71e..d4a2777a9 100644 --- a/Gemfile +++ b/Gemfile @@ -53,6 +53,9 @@ gem 'turnout', '~> 2.4.0' gem 'uglifier', '~> 3.2.0' gem 'unicorn', '~> 5.3.0' gem 'whenever', '~> 0.9.7', require: false +source 'https://rails-assets.org' do + gem 'rails-assets-leaflet' +end group :development, :test do gem "bullet", '~> 5.5.1' diff --git a/Gemfile.lock b/Gemfile.lock index f15f0c6e9..6435af58e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -334,6 +334,7 @@ GEM bundler (>= 1.3.0, < 2.0) railties (= 4.2.9) sprockets-rails + rails-assets-leaflet (1.1.0) rails-assets-markdown-it (8.2.2) rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) @@ -542,6 +543,7 @@ DEPENDENCIES poltergeist (~> 1.15.0) quiet_assets (~> 1.1.0) rails (= 4.2.9) + rails-assets-leaflet! rails-assets-markdown-it (~> 8.2.1)! redcarpet (~> 3.4.0) responders (~> 2.4.0) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index b10be62be..b73c07474 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -68,6 +68,8 @@ //= require custom //= require tag_autocomplete //= require polls_admin +//= require leaflet +//= require map var initialize_modules = function() { App.Comments.initialize(); @@ -105,6 +107,7 @@ var initialize_modules = function() { App.Imageable.initialize(); App.TagAutocomplete.initialize(); App.PollsAdmin.initialize(); + App.Map.initialize(); }; $(function(){ diff --git a/app/assets/javascripts/map.js.coffee b/app/assets/javascripts/map.js.coffee new file mode 100644 index 000000000..3f28024a7 --- /dev/null +++ b/app/assets/javascripts/map.js.coffee @@ -0,0 +1,78 @@ +App.Map = + + initialize: -> + maps = $('*[data-map]') + + if maps.length > 0 + $.each maps, (index, map) -> + App.Map.initializeMap map + + initializeMap: (element) -> + + mapCenterLatitude = $(element).data('map-center-latitude') + mapCenterLongitude = $(element).data('map-center-longitude') + markerLatitude = $(element).data('marker-latitude') + markerLongitude = $(element).data('marker-longitude') + zoom = $(element).data('map-zoom') + mapTilesProvider = $(element).data('map-tiles-provider') + mapAttribution = $(element).data('map-tiles-provider-attribution') + latitudeInputSelector = $(element).data('latitude-input-selector') + longitudeInputSelector = $(element).data('longitude-input-selector') + zoomInputSelector = $(element).data('zoom-input-selector') + removeMarkerSelector = $(element).data('marker-remove-selector') + editable = $(element).data('marker-editable') + marker = null; + markerIcon = L.divIcon( + className: 'map-marker' + iconSize: [30, 30] + iconAnchor: [15, 40] + html: '
') + + createMarker = (latitude, longitude) -> + markerLatLng = new (L.LatLng)(latitude, longitude) + marker = L.marker(markerLatLng, { icon: markerIcon, draggable: editable }) + if editable + marker.on 'dragend', updateFormfields + marker.addTo(map) + return marker + + removeMarker = (e) -> + e.preventDefault() + if marker + map.removeLayer(marker) + marker = null; + clearFormfields() + return + + moveOrPlaceMarker = (e) -> + if marker + marker.setLatLng(e.latlng) + else + marker = createMarker(e.latlng.lat, e.latlng.lng) + + updateFormfields() + return + + updateFormfields = -> + $(latitudeInputSelector).val marker.getLatLng().lat + $(longitudeInputSelector).val marker.getLatLng().lng + $(zoomInputSelector).val map.getZoom() + return + + clearFormfields = -> + $(latitudeInputSelector).val '' + $(longitudeInputSelector).val '' + $(zoomInputSelector).val '' + return + + mapCenterLatLng = new (L.LatLng)(mapCenterLatitude, mapCenterLongitude) + map = L.map(element.id).setView(mapCenterLatLng, zoom) + L.tileLayer(mapTilesProvider, attribution: mapAttribution).addTo map + + if markerLatitude && markerLongitude + marker = createMarker(markerLatitude, markerLongitude) + + if editable + $(removeMarkerSelector).on 'click', removeMarker + map.on 'zoomend', updateFormfields + map.on 'click', moveOrPlaceMarker diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss index 3dcf4a474..52c989f55 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -8,6 +8,7 @@ // 06. Polls // 07. Legislation // 08. CMS +// 09. Map // // 01. Global styles @@ -174,10 +175,19 @@ $sidebar-active: #f4fcd0; max-width: none; } - .menu.simple .active { - border-bottom: 2px solid $admin-color; - color: $admin-color; - font-weight: bold; + .menu.simple { + margin-bottom: $line-height / 2; + + h2 { + font-weight: bold; + margin-bottom: $line-height / 3; + } + + .active { + border-bottom: 2px solid $admin-color; + color: $admin-color; + font-weight: bold; + } } .tabs-panel { @@ -967,3 +977,51 @@ table { border: 0; } } + +// 09. Map +// -------------- + +.map { + width: 100%; + height: 350px; + + .map-marker { + visibility: visible; + position: absolute; + left: 50%; + top: 50%; + margin-top: -5px; + + .map-icon { + width: 30px; + height: 30px; + border-radius: 50% 50% 50% 0; + background: #00cae9; + transform: rotate(-45deg); + } + + .map-icon::after { + content: ''; + width: 14px; + height: 14px; + margin: 8px 0 0 8px; + background: #fff; + position: absolute; + border-radius: 50%; + } + } + + .map-attributtion { + visibility: visible; + height: auto; + } +} + +.map-marker { + visibility: hidden; +} + +.map-attributtion { + visibility: hidden; + height: 0; +} diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 361dcfde3..aff55c88c 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -18,3 +18,4 @@ @import 'datepicker_overrides'; @import 'jquery-ui/autocomplete'; @import 'autocomplete_overrides'; +@import 'leaflet'; diff --git a/app/assets/stylesheets/mixins.scss b/app/assets/stylesheets/mixins.scss index 89f962eeb..cc8fe9360 100644 --- a/app/assets/stylesheets/mixins.scss +++ b/app/assets/stylesheets/mixins.scss @@ -44,28 +44,6 @@ margin-bottom: $line-height; } - .document-form, - .image-form { - - .title{ - margin-bottom: $line-height; - } - - .document, - .image-form { - .file-name { - margin-top: 0; - } - } - - .document, - .image { - .loading-bar.errors { - margin-top: $line-height * 2; - } - } - } - .document, .image { @@ -107,12 +85,10 @@ &.complete { background-color: $success-color; - width: 100%; } &.errors { background-color: $alert-color; - width: 100%; margin-top: $line-height / 2; } } diff --git a/app/assets/stylesheets/participation.scss b/app/assets/stylesheets/participation.scss index 84e16ebe9..fbcda0ebb 100644 --- a/app/assets/stylesheets/participation.scss +++ b/app/assets/stylesheets/participation.scss @@ -249,11 +249,8 @@ .proposal-form, .budget-investment-form, .spending-proposal-form, -.document-form, .topic-new, -.topic-form, -.image-form, -.proposal .image-form { +.topic-form { .icon-debates, .icon-proposals, @@ -305,9 +302,7 @@ .proposal-form, .topic-form, -.topic-new, -.document-form, -.image-form { +.topic-new { .recommendations li::before { color: $proposals; @@ -317,8 +312,6 @@ .budget-investment-new, .proposal-form, .proposal-edit, -.image-form, -.document-form, .new_poll_question, .edit_poll_question { @include direct-uploads; @@ -856,12 +849,6 @@ display: none; } -.document-form { - max-width: 75rem; - margin-left: auto; - margin-right: auto; -} - .more-info { clear: both; color: $text-medium; diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index 70541a3a0..14dfc6658 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -14,6 +14,13 @@ class Admin::SettingsController < Admin::BaseController redirect_to admin_settings_path, notice: t("admin.settings.flash.updated") end + def update_map + Setting["map_latitude"] = params[:latitude].to_f + Setting["map_longitude"] = params[:longitude].to_f + Setting["map_zoom"] = params[:zoom].to_i + redirect_to admin_settings_path, notice: t("admin.settings.index.map.flash.update") + end + private def settings_params diff --git a/app/controllers/budgets/investments_controller.rb b/app/controllers/budgets/investments_controller.rb index fb82c51db..bb00f24af 100644 --- a/app/controllers/budgets/investments_controller.rb +++ b/app/controllers/budgets/investments_controller.rb @@ -107,7 +107,8 @@ module Budgets .permit(:title, :description, :external_url, :heading_id, :tag_list, :organization_name, :location, :terms_of_service, image_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy], - documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy]) + documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy], + map_location_attributes: [:latitude, :longitude, :zoom]) end def load_ballot diff --git a/app/controllers/documents_controller.rb b/app/controllers/documents_controller.rb index 73e90eb68..001d23446 100644 --- a/app/controllers/documents_controller.rb +++ b/app/controllers/documents_controller.rb @@ -1,24 +1,8 @@ class DocumentsController < ApplicationController before_action :authenticate_user! - before_action :find_documentable, except: :destroy - before_action :prepare_new_document, only: [:new] - before_action :prepare_document_for_creation, only: :create load_and_authorize_resource - def new - end - - def create - if @document.save - flash[:notice] = t "documents.actions.create.notice" - redirect_to params[:from] - else - flash[:alert] = t "documents.actions.create.alert" - render :new - end - end - def destroy respond_to do |format| format.html do @@ -39,25 +23,4 @@ class DocumentsController < ApplicationController end end - private - - def document_params - params.require(:document).permit(:title, :documentable_type, :documentable_id, - :attachment, :cached_attachment, :user_id) - end - - def find_documentable - @documentable = params[:documentable_type].constantize.find_or_initialize_by(id: params[:documentable_id]) - end - - def prepare_new_document - @document = Document.new(documentable: @documentable, user_id: current_user.id) - end - - def prepare_document_for_creation - @document = Document.new(document_params) - @document.documentable = @documentable - @document.user = current_user - end - end diff --git a/app/controllers/images_controller.rb b/app/controllers/images_controller.rb index 1bfe6e8cd..e273234f2 100644 --- a/app/controllers/images_controller.rb +++ b/app/controllers/images_controller.rb @@ -1,24 +1,8 @@ class ImagesController < ApplicationController before_action :authenticate_user! - before_filter :find_imageable, except: :destroy - before_filter :prepare_new_image, only: [:new] - before_filter :prepare_image_for_creation, only: :create load_and_authorize_resource - def new - end - - def create - if @image.save - flash[:notice] = t "images.actions.create.notice" - redirect_to params[:from] - else - flash[:alert] = t "images.actions.create.alert" - render :new - end - end - def destroy respond_to do |format| format.html do @@ -39,25 +23,4 @@ class ImagesController < ApplicationController end end - private - - def image_params - params.require(:image).permit(:title, :imageable_type, :imageable_id, - :attachment, :cached_attachment, :user_id) - end - - def find_imageable - @imageable = params[:imageable_type].constantize.find_or_initialize_by(id: params[:imageable_id]) - end - - def prepare_new_image - @image = Image.new(imageable: @imageable) - end - - def prepare_image_for_creation - @image = Image.new(image_params) - @image.imageable = @imageable - @image.user = current_user - end - end diff --git a/app/controllers/proposals_controller.rb b/app/controllers/proposals_controller.rb index 9e6fc521d..d1db0e4a2 100644 --- a/app/controllers/proposals_controller.rb +++ b/app/controllers/proposals_controller.rb @@ -77,7 +77,8 @@ class ProposalsController < ApplicationController params.require(:proposal).permit(:title, :question, :summary, :description, :external_url, :video_url, :responsible_name, :tag_list, :terms_of_service, :geozone_id, image_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy], - documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy] ) + documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy], + map_location_attributes: [:latitude, :longitude, :zoom]) end def retired_params diff --git a/app/helpers/documentables_helper.rb b/app/helpers/documentables_helper.rb index b95ea80c6..8d9f85578 100644 --- a/app/helpers/documentables_helper.rb +++ b/app/helpers/documentables_helper.rb @@ -1,9 +1,5 @@ module DocumentablesHelper - def can_create_document?(documentable) - can?(:create, Document.new(documentable: documentable)) && documentable.documents.size < documentable.class.max_documents_allowed - end - def documentable_class(documentable) documentable.class.name.parameterize('_') end diff --git a/app/helpers/documents_helper.rb b/app/helpers/documents_helper.rb index 228827516..d7e8d1dea 100644 --- a/app/helpers/documents_helper.rb +++ b/app/helpers/documents_helper.rb @@ -1,10 +1,5 @@ module DocumentsHelper - def document_note(document) - t "documents.new.#{document.documentable.class.name.parameterize.underscore}.note", - title: document.documentable.title - end - def document_attachment_file_name(document) document.attachment_file_name end @@ -40,7 +35,7 @@ module DocumentsHelper klass = document.errors[:attachment].any? ? "error" : "" klass = document.persisted? || document.cached_attachment.present? ? " hide" : "" html = builder.label :attachment, - t("documents.upload_document"), + t("documents.form.attachment_label"), class: "button hollow #{klass}" html += builder.file_field :attachment, label: false, diff --git a/app/helpers/imageables_helper.rb b/app/helpers/imageables_helper.rb index e45302792..b1c8059ce 100644 --- a/app/helpers/imageables_helper.rb +++ b/app/helpers/imageables_helper.rb @@ -1,9 +1,5 @@ module ImageablesHelper - def can_create_image?(imageable) - can?(:create, Image.new(imageable: imageable)) - end - def can_destroy_image?(imageable) imageable.image.present? && can?(:destroy, imageable.image) end diff --git a/app/helpers/images_helper.rb b/app/helpers/images_helper.rb index 79b6dbd1e..4e2bdff0e 100644 --- a/app/helpers/images_helper.rb +++ b/app/helpers/images_helper.rb @@ -9,11 +9,6 @@ module ImagesHelper end end - def image_note(image) - t "images.new.#{image.imageable.class.name.parameterize.underscore}.note", - title: image.imageable.title - end - def image_first_recommendation(image) t "images.#{image.imageable.class.name.parameterize.underscore}.recommendation_one_html", title: image.imageable.title diff --git a/app/helpers/map_locations_helper.rb b/app/helpers/map_locations_helper.rb new file mode 100644 index 000000000..ae45e7c1d --- /dev/null +++ b/app/helpers/map_locations_helper.rb @@ -0,0 +1,67 @@ +module MapLocationsHelper + + def map_location_available?(map_location) + map_location.present? && map_location.available? + end + + def map_location_latitude(map_location) + map_location.present? && map_location.latitude.present? ? map_location.latitude : Setting["map_latitude"] + end + + def map_location_longitude(map_location) + map_location.present? && map_location.longitude.present? ? map_location.longitude : Setting["map_longitude"] + end + + def map_location_zoom(map_location) + map_location.present? && map_location.zoom.present? ? map_location.zoom : Setting["map_zoom"] + end + + def map_location_input_id(prefix, attribute) + "#{prefix}_map_location_attributes_#{attribute}" + end + + def map_location_remove_marker_link_id(map_location) + "remove-marker-link-#{dom_id(map_location)}" + end + + def render_map(map_location, parent_class, editable, remove_marker_label) + map = content_tag_for :div, + map_location, + class: "map", + data: prepare_map_settings(map_location, editable, parent_class) + map += map_location_remove_marker(map_location, remove_marker_label) if editable + map + end + + def map_location_remove_marker(map_location, text) + content_tag :div, class: "text-right" do + content_tag :a, + id: map_location_remove_marker_link_id(map_location), + href: "#", + class: "location-map-remove-marker-button delete" do + text + end + end + end + + private + + def prepare_map_settings(map_location, editable, parent_class) + options = { + map: "", + map_center_latitude: map_location_latitude(map_location), + map_center_longitude: map_location_longitude(map_location), + map_zoom: map_location_zoom(map_location), + map_tiles_provider: Rails.application.secrets.map_tiles_provider, + map_tiles_provider_attribution: Rails.application.secrets.map_tiles_provider_attribution, + marker_editable: editable, + marker_latitude: map_location.latitude, + marker_longitude: map_location.longitude, + marker_remove_selector: "##{map_location_remove_marker_link_id(map_location)}", + latitude_input_selector: "##{map_location_input_id(parent_class, 'latitude')}", + longitude_input_selector: "##{map_location_input_id(parent_class, 'longitude')}", + zoom_input_selector: "##{map_location_input_id(parent_class, 'zoom')}" + } + end + +end diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb index 5f0c849e1..4278d4f8d 100644 --- a/app/models/abilities/administrator.rb +++ b/app/models/abilities/administrator.rb @@ -74,7 +74,7 @@ module Abilities cannot :comment_as_moderator, [::Legislation::Question, Legislation::Annotation] can [:create, :destroy], Document - can [:create, :destroy], Image + can [:destroy], Image can [:create, :destroy], DirectUpload end end diff --git a/app/models/abilities/common.rb b/app/models/abilities/common.rb index b4248c3b6..e4e92abaf 100644 --- a/app/models/abilities/common.rb +++ b/app/models/abilities/common.rb @@ -37,9 +37,9 @@ module Abilities can [:create, :destroy], Follow - can [:create, :destroy, :new], Document, documentable: { author_id: user.id } + can [:destroy], Document, documentable: { author_id: user.id } - can [:create, :destroy, :new], Image, imageable: { author_id: user.id } + can [:destroy], Image, imageable: { author_id: user.id } can [:create, :destroy], DirectUpload diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb index 857025375..ddf6e819c 100644 --- a/app/models/budget/investment.rb +++ b/app/models/budget/investment.rb @@ -8,6 +8,7 @@ class Budget include Followable include Communitable include Imageable + include Mappable include Documentable documentable max_documents_allowed: 3, max_file_size: 3.megabytes, diff --git a/app/models/concerns/mappable.rb b/app/models/concerns/mappable.rb new file mode 100644 index 000000000..c108bdca7 --- /dev/null +++ b/app/models/concerns/mappable.rb @@ -0,0 +1,9 @@ +module Mappable + extend ActiveSupport::Concern + + included do + has_one :map_location, dependent: :destroy + accepts_nested_attributes_for :map_location, allow_destroy: true + end + +end diff --git a/app/models/map_location.rb b/app/models/map_location.rb new file mode 100644 index 000000000..14e91d92c --- /dev/null +++ b/app/models/map_location.rb @@ -0,0 +1,10 @@ +class MapLocation < ActiveRecord::Base + + belongs_to :proposal + belongs_to :investment, class_name: Budget::Investment + + def available? + latitude.present? && longitude.present? && zoom.present? + end + +end diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 7a0abcb6e..da0fc109f 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -11,10 +11,11 @@ class Proposal < ActiveRecord::Base include Followable include Communitable include Imageable + include Mappable include Documentable documentable max_documents_allowed: 3, max_file_size: 3.megabytes, - accepted_content_types: [ "application/pdf" ] + accepted_content_types: [ "application/pdf" ] include EmbedVideosHelper acts_as_votable diff --git a/app/views/admin/poll/polls/_subnav.html.erb b/app/views/admin/poll/polls/_subnav.html.erb index 249ff407c..24d7a5b1f 100644 --- a/app/views/admin/poll/polls/_subnav.html.erb +++ b/app/views/admin/poll/polls/_subnav.html.erb @@ -1,8 +1,10 @@