decidim-decidim_geo 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (146) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE-AGPLv3.txt +661 -0
  3. data/README.md +172 -0
  4. data/Rakefile +119 -0
  5. data/app/cells/decidim/geo/content_blocks/geo_maps/show.erb +15 -0
  6. data/app/cells/decidim/geo/content_blocks/geo_maps_cell.rb +114 -0
  7. data/app/cells/decidim/geo/content_blocks/geo_maps_settings_form_cell.rb +14 -0
  8. data/app/cells/decidim/geo/geo_collection_map/show.erb +4 -0
  9. data/app/cells/decidim/geo/geo_collection_map_cell.rb +102 -0
  10. data/app/commands/decidim/admin/create_scope.rb +56 -0
  11. data/app/commands/decidim/admin/create_scope_type.rb +47 -0
  12. data/app/commands/decidim/admin/update_scope.rb +60 -0
  13. data/app/commands/decidim/admin/update_scope_type.rb +62 -0
  14. data/app/commands/decidim/geo/admin/create_shapefile.rb +51 -0
  15. data/app/commands/decidim/geo/admin/update_geo_config.rb +54 -0
  16. data/app/commands/decidim/geo/command.rb +14 -0
  17. data/app/controllers/decidim/admin/scopes_controller.rb +115 -0
  18. data/app/controllers/decidim/geo/admin/application_controller.rb +21 -0
  19. data/app/controllers/decidim/geo/admin/geo_configs_controller.rb +44 -0
  20. data/app/controllers/decidim/geo/admin/shapefiles_controller.rb +41 -0
  21. data/app/controllers/decidim/geo/application_controller.rb +13 -0
  22. data/app/controllers/decidim/geo/shapefile_controller.rb +11 -0
  23. data/app/forms/decidim/admin/scope_form.rb +43 -0
  24. data/app/forms/decidim/admin/scope_type_form.rb +27 -0
  25. data/app/forms/decidim/geo/admin/geo_config_form.rb +20 -0
  26. data/app/forms/decidim/geo/admin/shapefile_form.rb +23 -0
  27. data/app/helpers/decidim/admin/scopes_helper.rb +72 -0
  28. data/app/jobs/decidim/geo/shp2pgsql_job.rb +14 -0
  29. data/app/models/decidim/debates/debate.rb +226 -0
  30. data/app/models/decidim/geo/application_record.rb +10 -0
  31. data/app/models/decidim/geo/geo_config.rb +28 -0
  32. data/app/models/decidim/geo/shapedata.rb +23 -0
  33. data/app/models/decidim/geo/shapefile.rb +27 -0
  34. data/app/models/decidim/geo/space_location.rb +16 -0
  35. data/app/models/decidim/scope.rb +111 -0
  36. data/app/models/decidim/scope_type.rb +26 -0
  37. data/app/overrides/decidim/assemblies/admin/assemblies/_form/insert_location_form.html.erb.deface +13 -0
  38. data/app/overrides/decidim/assemblies/assemblies/show/insert_location.html.erb.deface +7 -0
  39. data/app/overrides/decidim/participatory_processes/admin/participatory_processes/_form/insert_location_form.html.erb.deface +13 -0
  40. data/app/overrides/decidim/participatory_processes/participatory_process_groups/show/insert_map.html.erb.deface +6 -0
  41. data/app/overrides/layouts/decidim/_assembly_header/insert_map.html.erb.deface +25 -0
  42. data/app/overrides/layouts/decidim/_process_header/insert_map.html.erb.deface +22 -0
  43. data/app/overrides/layouts/decidim/_wrapper/insert_map.html.erb.deface +5 -0
  44. data/app/overrides/remove_default_meetings_map.rb +4 -0
  45. data/app/overrides/remove_default_proposals_map.rb +4 -0
  46. data/app/packs/entrypoints/decidim_geo.js +3 -0
  47. data/app/packs/images/decidim/geo/not-geolocated.svg +3 -0
  48. data/app/packs/src/decidim/geo/actions/index.js +25 -0
  49. data/app/packs/src/decidim/geo/api/index.js +53 -0
  50. data/app/packs/src/decidim/geo/api/queries.js +77 -0
  51. data/app/packs/src/decidim/geo/bootstrap.js +6 -0
  52. data/app/packs/src/decidim/geo/index.js +101 -0
  53. data/app/packs/src/decidim/geo/models/configStore.js +44 -0
  54. data/app/packs/src/decidim/geo/models/dropdownFilterStore.js +65 -0
  55. data/app/packs/src/decidim/geo/models/filterStore.js +121 -0
  56. data/app/packs/src/decidim/geo/models/geoDatasourceNode/GeoDatasourceNode.js +124 -0
  57. data/app/packs/src/decidim/geo/models/geoDatasourceNode/index.js +1 -0
  58. data/app/packs/src/decidim/geo/models/geoScope/GeoScope.js +128 -0
  59. data/app/packs/src/decidim/geo/models/geoScope/index.js +1 -0
  60. data/app/packs/src/decidim/geo/models/geoStore.js +118 -0
  61. data/app/packs/src/decidim/geo/models/pointStore.js +134 -0
  62. data/app/packs/src/decidim/geo/models/scopeDropdownStore.js +20 -0
  63. data/app/packs/src/decidim/geo/ui/DrawerDetail/fallback.js +42 -0
  64. data/app/packs/src/decidim/geo/ui/DrawerDetail/index.js +2 -0
  65. data/app/packs/src/decidim/geo/ui/DrawerDetail/meetings.js +75 -0
  66. data/app/packs/src/decidim/geo/ui/DrawerMenuItem/fallback.js +45 -0
  67. data/app/packs/src/decidim/geo/ui/DrawerMenuItem/index.js +2 -0
  68. data/app/packs/src/decidim/geo/ui/DrawerMenuItem/meetings.js +62 -0
  69. data/app/packs/src/decidim/geo/ui/createClasses.js +7 -0
  70. data/app/packs/src/decidim/geo/ui/createCustomMarker.js +14 -0
  71. data/app/packs/src/decidim/geo/ui/createDomElement.js +2 -0
  72. data/app/packs/src/decidim/geo/ui/createDrawer.js +170 -0
  73. data/app/packs/src/decidim/geo/ui/createDrawerActions.js +197 -0
  74. data/app/packs/src/decidim/geo/ui/createFilterDropdown.js +320 -0
  75. data/app/packs/src/decidim/geo/ui/createGeoScopeLayer.js +20 -0
  76. data/app/packs/src/decidim/geo/ui/createGeoScopeMenuItem.js +8 -0
  77. data/app/packs/src/decidim/geo/ui/createNodeMarker.js +9 -0
  78. data/app/packs/src/decidim/geo/ui/createNodeMenuItem.js +13 -0
  79. data/app/packs/src/decidim/geo/ui/index.js +8 -0
  80. data/app/packs/src/decidim/geo/ui/initMap.js +22 -0
  81. data/app/packs/src/decidim/geo/utils/index.js +30 -0
  82. data/app/packs/src/decidim/geo/utils/saveConfig.js +15 -0
  83. data/app/packs/stylesheets/decidim/geo/_geo.scss +2 -0
  84. data/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_decidimGeo.scss +476 -0
  85. data/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_override.scss +65 -0
  86. data/app/permissions/decidim/geo/admin/permissions.rb +24 -0
  87. data/app/permissions/decidim/geo/permissions.rb +15 -0
  88. data/app/uploaders/decidim/geo/shapefile_uploader.rb +14 -0
  89. data/app/validators/geocoding_validator.rb +30 -0
  90. data/app/views/decidim/admin/scope_types/_form.html.erb +10 -0
  91. data/app/views/decidim/admin/scope_types/index.html.erb +46 -0
  92. data/app/views/decidim/admin/scopes/_form.html.erb +20 -0
  93. data/app/views/decidim/admin/scopes/index.html.erb +55 -0
  94. data/app/views/decidim/geo/admin/geo_configs/_form.html.erb +47 -0
  95. data/app/views/decidim/geo/admin/geo_configs/index.html.erb +8 -0
  96. data/app/views/decidim/geo/admin/shapefiles/_form.html.erb +29 -0
  97. data/app/views/decidim/geo/admin/shapefiles/index.html.erb +38 -0
  98. data/app/views/decidim/geo/admin/shapefiles/new.html.erb +8 -0
  99. data/app/views/layouts/decidim/decidim_geo/admin/application.html.erb +19 -0
  100. data/config/assets.rb +9 -0
  101. data/config/i18n-tasks.yml +10 -0
  102. data/config/locales/de.yml +102 -0
  103. data/config/locales/en.yml +108 -0
  104. data/config/locales/fr.yml +102 -0
  105. data/db/migrate/20221019184712_add_postgis_extension_to_database.rb +5 -0
  106. data/db/migrate/20221025195520_create_decidim_geo.rb +22 -0
  107. data/db/migrate/20231013082325_create_decidim_geo_config.rb +11 -0
  108. data/db/migrate/20231206115531_add_spaces_config_to_decidim_geo_configs.rb +6 -0
  109. data/db/migrate/20240309004347_add_organization_to_shapefiles.rb +6 -0
  110. data/db/migrate/20240326052727_create_space_locations.rb +18 -0
  111. data/lib/decidim/api/geo_config_type.rb +15 -0
  112. data/lib/decidim/api/geo_coordinates_type.rb +22 -0
  113. data/lib/decidim/api/geo_datasource_type.rb +147 -0
  114. data/lib/decidim/api/geo_dates_type.rb +15 -0
  115. data/lib/decidim/api/geo_query_extension.rb +25 -0
  116. data/lib/decidim/api/geo_scope_api_type.rb +26 -0
  117. data/lib/decidim/api/geo_shapedata_type.rb +18 -0
  118. data/lib/decidim/api/geo_shapefile_type.rb +22 -0
  119. data/lib/decidim/api/input_filters/assembly_input_filter.rb +13 -0
  120. data/lib/decidim/api/input_filters/geo_datasource_input_filter.rb +18 -0
  121. data/lib/decidim/api/input_filters/geoencoded_input_filter.rb +13 -0
  122. data/lib/decidim/api/input_filters/has_scopeable_input_filter.rb +17 -0
  123. data/lib/decidim/api/input_filters/process_input_filter.rb +13 -0
  124. data/lib/decidim/api/input_filters/resource_type_input_filter.rb +13 -0
  125. data/lib/decidim/api/input_filters/scope_input_filter.rb +13 -0
  126. data/lib/decidim/api/input_filters/time_input_filter.rb +13 -0
  127. data/lib/decidim/api/meetings_input_filter.rb +14 -0
  128. data/lib/decidim/api/query_extension.rb +320 -0
  129. data/lib/decidim/api/shapefile_query_extention.rb +18 -0
  130. data/lib/decidim/api/shapefile_type.rb +18 -0
  131. data/lib/decidim/decidim_geo/admin.rb +10 -0
  132. data/lib/decidim/decidim_geo/admin_engine.rb +69 -0
  133. data/lib/decidim/decidim_geo/api.rb +23 -0
  134. data/lib/decidim/decidim_geo/engine.rb +58 -0
  135. data/lib/decidim/decidim_geo/load_shp/app_load_shp.rb +85 -0
  136. data/lib/decidim/decidim_geo/space_location/assembly_create_command_override.rb +26 -0
  137. data/lib/decidim/decidim_geo/space_location/assembly_form_override.rb +33 -0
  138. data/lib/decidim/decidim_geo/space_location/assembly_update_command_override.rb +21 -0
  139. data/lib/decidim/decidim_geo/space_location/participatory_process_command_override.rb +21 -0
  140. data/lib/decidim/decidim_geo/space_location/participatory_process_form_override.rb +33 -0
  141. data/lib/decidim/decidim_geo/space_location/space_override.rb +15 -0
  142. data/lib/decidim/decidim_geo/test/factories.rb +13 -0
  143. data/lib/decidim/decidim_geo/version.rb +14 -0
  144. data/lib/decidim/decidim_geo.rb +25 -0
  145. data/lib/tasks/decidim_geo_webpacker_tasks.rake +61 -0
  146. metadata +335 -0
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Debates
5
+ # The data store for a Debate in the Decidim::Debates component. It stores a
6
+ # title, description and any other useful information to render a custom
7
+ # debate.
8
+ class Debate < Debates::ApplicationRecord
9
+ include Decidim::HasComponent
10
+ include Decidim::HasCategory
11
+ include Decidim::Resourceable
12
+ include Decidim::Followable
13
+ include Decidim::Comments::CommentableWithComponent
14
+ include Decidim::Comments::HasAvailabilityAttributes
15
+ include Decidim::ScopableResource
16
+ include Decidim::Authorable
17
+ include Decidim::Reportable
18
+ include Decidim::HasReference
19
+ include Decidim::Traceable
20
+ include Decidim::Loggable
21
+ include Decidim::NewsletterParticipant
22
+ include Decidim::Searchable
23
+ include Decidim::TranslatableResource
24
+ include Decidim::TranslatableAttributes
25
+ include Decidim::Endorsable
26
+ include Decidim::Randomable
27
+
28
+ if Decidim.version == '0.27.4'
29
+ include Decidim::DownloadYourData
30
+ include Decidim::FilterableResource
31
+ end
32
+
33
+ belongs_to :last_comment_by, polymorphic: true, foreign_type: "last_comment_by_type", optional: true
34
+ component_manifest_name "debates"
35
+
36
+ validates :title, presence: true
37
+
38
+ translatable_fields :title, :description, :instructions, :information_updates
39
+ searchable_fields({
40
+ scope_id: :decidim_scope_id,
41
+ participatory_space: { component: :participatory_space },
42
+ A: :title,
43
+ D: :description,
44
+ datetime: :start_time
45
+ },
46
+ index_on_create: ->(debate) { debate.visible? },
47
+ index_on_update: ->(debate) { debate.visible? })
48
+
49
+ scope :open, -> { where(closed_at: nil) }
50
+ scope :closed, -> { where.not(closed_at: nil) }
51
+ scope :authored_by, ->(author) { where(author: author) }
52
+ scope :commented_by, lambda { |author|
53
+ joins(:comments).where(
54
+ decidim_comments_comments:
55
+ {
56
+ decidim_author_id: author.id,
57
+ decidim_author_type: author.class.base_class.name
58
+ }
59
+ )
60
+ }
61
+
62
+ if Decidim.version == '0.27.4'
63
+ scope_search_multi :with_any_state, [:open, :closed]
64
+ end
65
+
66
+ def self.log_presenter_class_for(_log)
67
+ Decidim::Debates::AdminLog::DebatePresenter
68
+ end
69
+
70
+ def comments_start_time
71
+ start_time
72
+ end
73
+
74
+ def comments_end_time
75
+ end_time
76
+ end
77
+
78
+ # Public: Overrides the `reported_content_url` Reportable concern method.
79
+ def reported_content_url
80
+ ResourceLocatorPresenter.new(self).url
81
+ end
82
+
83
+ # Public: Overrides the `reported_attributes` Reportable concern method.
84
+ def reported_attributes
85
+ [:title, :description]
86
+ end
87
+
88
+ # Public: Overrides the `reported_searchable_content_extras` Reportable concern method.
89
+ def reported_searchable_content_extras
90
+ [normalized_author.name]
91
+ end
92
+
93
+ # Public: Calculates whether the current debate is an AMA-styled one or not.
94
+ # AMA-styled debates are those that have a start and end time set, and comments
95
+ # are only open during that timelapse. AMA stands for Ask Me Anything, a type
96
+ # of debate inspired by Reddit.
97
+ #
98
+ # Returns a Boolean.
99
+ def ama?
100
+ start_time.present? && end_time.present?
101
+ end
102
+
103
+ # Public: Checks whether the debate is an AMA-styled one and is open.
104
+ #
105
+ # Returns a boolean.
106
+ def open_ama?
107
+ ama? && Time.current.between?(start_time, end_time)
108
+ end
109
+
110
+ # Public: Checks if the debate is open or not.
111
+ #
112
+ # Returns a boolean.
113
+ def open?
114
+ (ama? && open_ama?) || !ama?
115
+ end
116
+
117
+ # Public: Overrides the `accepts_new_comments?` CommentableWithComponent concern method.
118
+ def accepts_new_comments?
119
+ return false unless open?
120
+ return false if closed?
121
+
122
+ commentable? && !comments_blocked? && comments_allowed?
123
+ end
124
+
125
+ # Public: Overrides the `comments_have_alignment?` Commentable concern method.
126
+ def comments_have_alignment?
127
+ true
128
+ end
129
+
130
+ # Public: Overrides the `comments_have_votes?` Commentable concern method.
131
+ def comments_have_votes?
132
+ true
133
+ end
134
+
135
+ # Public: Identifies the commentable type in the API.
136
+ def commentable_type
137
+ self.class.name
138
+ end
139
+
140
+ # Public: Override Commentable concern method `users_to_notify_on_comment_created`
141
+ def users_to_notify_on_comment_created
142
+ return Decidim::User.where(id: followers).or(Decidim::User.where(id: component.participatory_space.admins)).distinct if official?
143
+
144
+ followers
145
+ end
146
+
147
+ # Public: Overrides the `allow_resource_permissions?` Resourceable concern method.
148
+ def allow_resource_permissions?
149
+ true
150
+ end
151
+
152
+ def self.export_serializer
153
+ Decidim::Debates::DownloadYourDataDebateSerializer
154
+ end
155
+
156
+ def self.newsletter_participant_ids(component)
157
+ authors_ids = Decidim::Debates::Debate.where(component: component)
158
+ .where(decidim_author_type: Decidim::UserBaseEntity.name)
159
+ .where.not(author: nil)
160
+ .group(:decidim_author_id)
161
+ .pluck(:decidim_author_id).flatten.compact
162
+ commentators_ids = Decidim::Comments::Comment.user_commentators_ids_in(Decidim::Debates::Debate.where(component: component))
163
+ (authors_ids + commentators_ids).flatten.compact.uniq
164
+ end
165
+
166
+ # Checks whether the user can edit the debate.
167
+ #
168
+ # user - the user to check for authorship
169
+ def editable_by?(user)
170
+ !closed? && authored_by?(user)
171
+ end
172
+
173
+ # Checks whether the debate is closed or not.
174
+ #
175
+ def closed?
176
+ closed_at.present? && conclusions.present?
177
+ end
178
+
179
+ # Checks whether the user can edit the debate.
180
+ #
181
+ # user - the user to check for authorship
182
+ def closeable_by?(user)
183
+ authored_by?(user)
184
+ end
185
+
186
+ # Public: Updates the comments counter cache. We have to do it these
187
+ # way in order to properly calculate the counter with hidden
188
+ # comments.
189
+ #
190
+ # rubocop:disable Rails/SkipsModelValidations
191
+ def update_comments_count
192
+ comments_count = comments.not_hidden.not_deleted.count
193
+ last_comment = comments.not_hidden.not_deleted.order("created_at DESC").first
194
+
195
+ update_columns(
196
+ last_comment_at: last_comment&.created_at,
197
+ last_comment_by_id: last_comment&.decidim_user_group_id || last_comment&.decidim_author_id,
198
+ last_comment_by_type: last_comment&.decidim_author_type,
199
+ comments_count: comments_count,
200
+ updated_at: Time.current
201
+ )
202
+ end
203
+ # rubocop:enable Rails/SkipsModelValidations
204
+
205
+ if Decidim.version == '0.27.4'
206
+ # Create i18n ransackers for :title and :description.
207
+ # Create the :search_text ransacker alias for searching from both of these.
208
+ ransacker_i18n_multi :search_text, [:title, :description]
209
+ end
210
+
211
+ def self.ransackable_scopes(_auth_object = nil)
212
+ [:with_any_state, :with_any_origin, :with_any_category, :with_any_scope]
213
+ end
214
+
215
+ def self.ransack(params = {}, options = {})
216
+ DebateSearch.new(self, params, options)
217
+ end
218
+
219
+ private
220
+
221
+ def comments_blocked?
222
+ component.current_settings.comments_blocked
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Geo
5
+ # Abstract class from which all models in this engine inherit.
6
+ class ApplicationRecord < ActiveRecord::Base
7
+ self.abstract_class = true
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Geo
5
+ # The data store for geo configuration default.
6
+ class GeoConfig < ApplicationRecord
7
+
8
+ self.table_name = 'decidim_geo_configs'
9
+
10
+ validates :longitude, :latitude, :zoom, :presence => true
11
+
12
+ def self.geo_config_default
13
+ Decidim::Geo::GeoConfig
14
+ .first_or_create(latitude: 0,
15
+ longitude: 0,
16
+ zoom: 13,
17
+ tile: tile_layer_default)
18
+ end
19
+
20
+ def self.tile_layer_default
21
+ return "" unless Decidim.config.maps.present?
22
+
23
+ Decidim.config.maps[:dynamic][:tile_layer][:url]
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Geo
5
+ # Data fo the shapfiles uploaded
6
+ class Shapedata < ApplicationRecord
7
+
8
+ self.table_name = 'decidim_geo_shapefile_datas'
9
+ validates :data, :presence => true
10
+
11
+ belongs_to :scope, inverse_of: :shapedata, optional: true, foreign_key: "decidim_scopes_id"
12
+
13
+ def execute_statement(sql)
14
+ Shapedata.connection.exec_query(sql)
15
+ end
16
+
17
+ def shapedata?
18
+ return false
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Geo
5
+ # The data store for a Proposal in the Decidim::Proposals component.
6
+ class Shapefile < ApplicationRecord
7
+ #include Decidim::TranslatableResource
8
+ include Decidim::HasUploadValidations
9
+
10
+ self.table_name = 'decidim_geo_shapefiles'
11
+
12
+ has_many :shapedatas, foreign_key: "decidim_geo_shapefiles_id"
13
+
14
+ belongs_to :scope_type, inverse_of: :shapefile, optional: true, foreign_key: "decidim_scope_types_id"
15
+
16
+ #translatable_fields :title, :description
17
+
18
+ validates :title, :description, :presence => true
19
+
20
+ has_one_attached :shapefile
21
+ validates_upload :shapefile, uploader: Decidim::Geo::ShapefileUploader
22
+ belongs_to :organization,
23
+ foreign_key: "decidim_organization_id",
24
+ class_name: "Decidim::Organization"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Geo
5
+ # Data fo the shapfiles uploaded
6
+ class SpaceLocation < ApplicationRecord
7
+ self.table_name = 'decidim_geo_space_locations'
8
+ belongs_to :decidim_geo_space, polymorphic: true
9
+
10
+ def has_address?
11
+ !latitude.nil? && !longitude.nil?
12
+ end
13
+ end
14
+ end
15
+ end
16
+
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ # Scopes are used in some entities through Decidim to help users know which is
5
+ # the scope of a participatory process.
6
+ # (i.e. does it affect the whole city or just a district?)
7
+ class Scope < ApplicationRecord
8
+ include Decidim::Traceable
9
+ include Decidim::Loggable
10
+ include Decidim::TranslatableResource
11
+
12
+ translatable_fields :name
13
+
14
+ belongs_to :organization,
15
+ foreign_key: "decidim_organization_id",
16
+ class_name: "Decidim::Organization",
17
+ inverse_of: :scopes
18
+
19
+ belongs_to :scope_type,
20
+ class_name: "Decidim::ScopeType",
21
+ inverse_of: :scopes,
22
+ optional: true
23
+
24
+ belongs_to :parent,
25
+ class_name: "Decidim::Scope",
26
+ inverse_of: :children,
27
+ optional: true
28
+
29
+ has_many :children,
30
+ foreign_key: "parent_id",
31
+ class_name: "Decidim::Scope",
32
+ inverse_of: :parent,
33
+ dependent: :destroy
34
+
35
+ has_one :shapedata,
36
+ foreign_key: "decidim_scopes_id",
37
+ class_name: "Decidim::Geo::Shapedata",
38
+ inverse_of: :scope,
39
+ dependent: :nullify
40
+
41
+ before_validation :update_part_of, on: :update
42
+
43
+ validates :name, :code, :organization, presence: true
44
+ validates :code, uniqueness: { scope: :organization }
45
+ validate :forbid_cycles
46
+
47
+ after_create_commit :create_part_of
48
+
49
+ # Scope to return only the top level scopes.
50
+ #
51
+ # Returns an ActiveRecord::Relation.
52
+ def self.top_level(organization_id = nil)
53
+ query = where parent_id: nil
54
+ query = query.where(decidim_organization_id: organization_id) if organization_id
55
+ query
56
+ end
57
+
58
+ def self.log_presenter_class_for(_log)
59
+ Decidim::AdminLog::ScopePresenter
60
+ end
61
+
62
+ def descendants
63
+ @descendants ||= organization.scopes.where("? = ANY(decidim_scopes.part_of)", id)
64
+ end
65
+
66
+ def ancestor_of?(scope)
67
+ scope && scope.part_of.member?(id)
68
+ end
69
+
70
+ # Gets the scopes from the part_of list in descending order (first the top level scope, last itself)
71
+ #
72
+ # root - The root scope to start retrieval. If present, ignores top level scopes until reaching the root scope.
73
+ #
74
+ # Returns an array of Scope objects
75
+ def part_of_scopes(root = nil)
76
+ scope_ids = part_of
77
+ scope_ids.select! { |id| id == root.id || !root.part_of.member?(id) } if root
78
+ organization.scopes.where(id: scope_ids).sort { |s1, s2| part_of.index(s2.id) <=> part_of.index(s1.id) }
79
+ end
80
+
81
+ # Allow ransacker to search for a key in a hstore column (`name`.`en`)
82
+ ransacker :name do |parent|
83
+ Arel::Nodes::InfixOperation.new("->>", parent.table[:name], Arel::Nodes.build_quoted(I18n.locale.to_s))
84
+ end
85
+
86
+ private
87
+
88
+ def forbid_cycles
89
+ return unless parent
90
+
91
+ errors.add(:parent_id, :cycle_detected) if parent.part_of.include?(id)
92
+ end
93
+
94
+ def create_part_of
95
+ build_part_of
96
+ save if changed?
97
+ end
98
+
99
+ def update_part_of
100
+ build_part_of
101
+ end
102
+
103
+ def build_part_of
104
+ if parent
105
+ part_of.clear.append(id).concat(parent.reload.part_of)
106
+ else
107
+ part_of.clear.append(id)
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ # Scope types allows to use different types of scopes in participatory process
5
+ # (municipalities, provinces, states, countries, etc.)
6
+ # Override to include shapefiles field
7
+ class ScopeType < ApplicationRecord
8
+ include Decidim::TranslatableResource
9
+
10
+ translatable_fields :name, :plural
11
+ belongs_to :organization,
12
+ foreign_key: "decidim_organization_id",
13
+ class_name: "Decidim::Organization",
14
+ inverse_of: :scope_types
15
+
16
+ has_one :shapefile,
17
+ class_name: "Decidim::Geo::Shapefile",
18
+ inverse_of: :scope_type,
19
+ foreign_key: "decidim_scope_types_id",
20
+ dependent: :nullify
21
+
22
+ has_many :scopes, class_name: "Decidim::Scope", inverse_of: :scope_type, dependent: :nullify
23
+
24
+ validates :name, presence: true
25
+ end
26
+ end
@@ -0,0 +1,13 @@
1
+ <!-- insert_after ".card-section:nth-child(2)" -->
2
+ <% if Decidim::Map.available?(:geocoding) %>
3
+ <div class="card-divider">
4
+ <h2 class="card-title"><%= t(".decidim_geo_space_address") %></h2>
5
+ </div>
6
+
7
+ <div class="card-section">
8
+ <div class="row column">
9
+ <%= form.geocoding_field :decidim_geo_space_address, autocomplete: true %>
10
+ <p class="help-text"><%= t(".decidim_geo_space_address_help") %></p>
11
+ </div>
12
+ </div>
13
+ <% end %>
@@ -0,0 +1,7 @@
1
+ <!-- insert_before "erb[loud]:contains('social_handler_links')" -->
2
+ <% if current_participatory_space.decidim_geo_space_location && current_participatory_space.decidim_geo_space_location.has_address?%>
3
+ <div class="definition-data__item scope">
4
+ <span class="definition-data__title"><%= t("decidim_geo_space_location", scope: "decidim.assemblies.show") %></span>
5
+ <%= current_participatory_space.decidim_geo_space_location.address %>
6
+ </div>
7
+ <% end %>
@@ -0,0 +1,13 @@
1
+ <!-- insert_after ".card-section:nth-child(2)" -->
2
+ <% if Decidim::Map.available?(:geocoding) %>
3
+ <div class="card-divider">
4
+ <h2 class="card-title"><%= t(".decidim_geo_space_address") %></h2>
5
+ </div>
6
+
7
+ <div class="card-section">
8
+ <div class="row column">
9
+ <%= form.geocoding_field :decidim_geo_space_address, autocomplete: true %>
10
+ <p class="help-text"><%= t(".decidim_geo_space_address_help") %></p>
11
+ </div>
12
+ </div>
13
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <!-- insert_before '.wrapper' -->
2
+ <%= cell("decidim/geo/content_blocks/geo_maps", @group,
3
+ id: "ProcessGroup",
4
+ filters: @group.participatory_processes.map {|process| { processFilter: { processId: process.id } } },
5
+ scopes: @group.participatory_processes.scope.map{|s| s.id}
6
+ ) %>
@@ -0,0 +1,25 @@
1
+ <!-- insert_after '.process-header' -->
2
+ <%
3
+ assemblies = []
4
+ # recursivly get all the children assemblies.
5
+ def iter_children(assembly, collection)
6
+ assembly.children.map {|ch| iter_children(ch, collection) } if assembly.children
7
+ collection.push(assembly.id)
8
+ end
9
+ iter_children(current_participatory_space, assemblies) unless current_participatory_space.scope.present?
10
+ %>
11
+ <%= cell("decidim/geo/content_blocks/geo_maps", current_participatory_space,
12
+ id: "Assembly",
13
+ filters: if current_participatory_space.scope.present?
14
+ [
15
+ { scopeFilter: { scopeId: current_participatory_space.scope.id } }
16
+ ]
17
+ else
18
+ assemblies.map { |assembly_id| { assemblyFilter: { assemblyId: assembly_id } } }
19
+ end,
20
+ scopes: if current_participatory_space.scope.present?
21
+ [current_participatory_space.scope.id]
22
+ else
23
+ []
24
+ end
25
+ ) %>
@@ -0,0 +1,22 @@
1
+ <!-- insert_after '.process-header' -->
2
+ <% if controller.controller_name != "participatory_process_steps" %>
3
+ <%= cell("decidim/geo/content_blocks/geo_maps", current_participatory_space,
4
+ id: "Process",
5
+ filters: if current_participatory_space.scope.present?
6
+ # If the process has a scope, filter by its scope
7
+ [
8
+ { scopeFilter: { scopeId: current_participatory_space.scope.id } }
9
+ ]
10
+ else
11
+ # Else filter on the process data
12
+ [
13
+ { processFilter: { processId: current_participatory_space.id } }
14
+ ]
15
+ end,
16
+ scopes: if current_participatory_space.scope.present?
17
+ [current_participatory_space.scope.id]
18
+ else
19
+ []
20
+ end
21
+ ) %>
22
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <!-- insert_after '.show-for-medium > .navbar' -->
2
+ <%= cell(
3
+ "decidim/geo/geo_collection_map",
4
+ id: "Indexes",
5
+ ) %>
@@ -0,0 +1,4 @@
1
+ Deface::Override.new(:virtual_path => "decidim/meetings/meetings/index",
2
+ :name => "remove_default_meetings_map",
3
+ :remove => "erb[silent]:contains('if current_component.settings.maps_enabled? && search.result.not_online.exists?')",
4
+ :closing_selector => "erb[silent]:contains('end')")
@@ -0,0 +1,4 @@
1
+ Deface::Override.new(:virtual_path => "decidim/proposals/proposals/index",
2
+ :name => "remove_default_proposals_map",
3
+ :remove => "erb[loud]:contains('dynamic_map_for proposals_data_for_map(@all_geocoded_proposals) do')",
4
+ :closing_selector => "erb[silent]:contains('end')")
@@ -0,0 +1,3 @@
1
+ require.context("../images", true);
2
+
3
+ import "src/decidim/geo/index.js";
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" viewBox="0 0 17 16" fill="none">
2
+ <path d="M11.4334 10.7334L8.35339 7.64671L8.28006 7.57337L2.70006 2.00004L1.85339 2.84671L3.97339 4.96671C3.89339 5.30004 3.85339 5.64004 3.85339 6.00004C3.85339 9.50004 8.52006 14.6667 8.52006 14.6667C8.52006 14.6667 9.63339 13.4334 10.7667 11.7667L13.0067 14L13.8534 13.1467M8.52006 4.33337C8.96209 4.33337 9.38601 4.50897 9.69857 4.82153C10.0111 5.13409 10.1867 5.55801 10.1867 6.00004C10.1867 6.48671 9.96673 6.92671 9.63339 7.23337L12.0534 9.66671C12.7067 8.41337 13.1867 7.12004 13.1867 6.00004C13.1867 4.76236 12.6951 3.57538 11.8199 2.70021C10.9447 1.82504 9.75774 1.33337 8.52006 1.33337C7.18673 1.33337 6.01339 1.88004 5.16006 2.76004L7.28673 4.88671C7.59339 4.54671 8.02673 4.33337 8.52006 4.33337Z" fill="#808080"/>
3
+ </svg>
@@ -0,0 +1,25 @@
1
+ export const displayNestedLayers = (leafletContainer, checked) => {
2
+ if (
3
+ leafletContainer.classList.contains("decidimGeo__customControl__input") &&
4
+ leafletContainer.checked !== checked
5
+ ) {
6
+ leafletContainer.dispatchEvent(
7
+ new MouseEvent("click", {
8
+ view: window,
9
+ bubbles: false,
10
+ cancelable: false
11
+ })
12
+ );
13
+ //leafletContainer.checked = checked;
14
+ //This leads layers to unsynchronize with the input state.
15
+ // Bubbling the click seems to ensure leaflet events are correctly triggered.
16
+ }
17
+
18
+ if (leafletContainer.children) {
19
+ return [...leafletContainer.children].map((child) =>
20
+ displayNestedLayers(child, checked)
21
+ );
22
+ } else {
23
+ return;
24
+ }
25
+ };
@@ -0,0 +1,53 @@
1
+ import * as queries from "./queries";
2
+ import { getDecidimData } from "../utils";
3
+ import configStore from "../models/configStore";
4
+
5
+ const makeQuery =
6
+ (queryName, responseKey = undefined) =>
7
+ async (params = {}) => {
8
+ const _responseKey = responseKey ? responseKey : queryName;
9
+ const { locale } = configStore.getState();
10
+ const variables = { ...(params.variables || {}), locale: locale };
11
+ const queryParams = { ...params, variables };
12
+ const response = await getDecidimData(queries[queryName], queryParams);
13
+ return response.data[_responseKey];
14
+ };
15
+
16
+ const _getGeoDatasource = makeQuery("geoDatasource");
17
+ const _getGeoDatasourceIds = makeQuery("geoDatasourceIds", "geoDatasource");
18
+
19
+ export const getGeoDatasource = async (params = {}, fetchAll = true) => {
20
+ let results = [];
21
+ if (!params.variables) {
22
+ params.variables = {};
23
+ }
24
+ const apiQuery = fetchAll ? _getGeoDatasource : _getGeoDatasourceIds;
25
+ let page;
26
+ try {
27
+ page = await apiQuery(params);
28
+ } catch (error) {
29
+ console.error(error);
30
+ throw error;
31
+ }
32
+ if (!page) return [];
33
+ const { hasNextPage = false, endCursor = "" } = page?.pageInfo || {};
34
+ results = results.concat(page.nodes);
35
+ let hasMore = hasNextPage;
36
+ params.variables.after = endCursor;
37
+ while (hasMore) {
38
+ try {
39
+ page = await apiQuery(params);
40
+ } catch (error) {
41
+ console.error(error);
42
+ return { nodes: results, edges: page.edge };
43
+ }
44
+ const { endCursor = params.variables.after, hasNextPage } = page.pageInfo || {};
45
+ results = results.concat(page.nodes);
46
+ hasMore = hasNextPage;
47
+ params.variables.after = endCursor;
48
+ }
49
+
50
+ return { nodes: results, edges: page.edge };
51
+ };
52
+ export const getGeoConfig = makeQuery("geoConfig");
53
+ export const getGeoScopes = makeQuery("geoScope");