diff --git a/.env-example b/.env-example
index 14c215885a..18d2e784f8 100644
--- a/.env-example
+++ b/.env-example
@@ -77,7 +77,9 @@ DECIDIM_ADMIN_PASSWORD_STRONG="false"
## Generate values with: bin/rails decidim:pwa:generate_vapid_keys
# VAPID_PUBLIC_KEY
# VAPID_PRIVATE_KEY
-RAILS_LOG_LEVEL=warn
+# RAILS_LOG_LEVEL=warn
+
+# DECIDIM_AWESOME_WEIGHTED_PROPOSAL_VOTING_ENABLED=disabled # or enabled
# Default notifications sending frequency : (daily, weekly, none, real_time)
# NOTIFICATIONS_SENDING_FREQUENCY=daily
diff --git a/Dockerfile b/Dockerfile
index f62346cd5b..63ec05e32e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,7 +6,7 @@ ENV RAILS_ENV=production \
WORKDIR /app
RUN apt-get update && \
- apt-get -y install libpq-dev curl git libicu-dev build-essential && \
+ apt-get -y install libpq-dev curl git libicu-dev build-essential p7zip-full && \
curl https://deb.nodesource.com/setup_16.x | bash && \
apt-get install -y nodejs && \
npm install --global yarn && \
@@ -41,7 +41,7 @@ ENV RAILS_ENV=production \
RAILS_LOG_TO_STDOUT=true
RUN apt update && \
- apt install -y postgresql-client imagemagick libproj-dev proj-bin libjemalloc2 && \
+ apt install -y postgresql-client imagemagick libproj-dev proj-bin libjemalloc2 p7zip-full && \
gem install bundler:2.4.9
WORKDIR /app
diff --git a/Dockerfile.local b/Dockerfile.local
index 8a5452123b..20fbdc5c7c 100644
--- a/Dockerfile.local
+++ b/Dockerfile.local
@@ -8,7 +8,7 @@ ENV RAILS_ENV=production \
# Install common dependencies
RUN apt-get update -q && \
apt-get install -yq --no-install-recommends \
- libpq-dev curl git libicu-dev build-essential openssl && \
+ libpq-dev curl git libicu-dev build-essential openssl p7zip-full && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
@@ -66,7 +66,7 @@ WORKDIR /app
# Install runtime dependencies
RUN apt-get update -q && \
apt-get install -yq --no-install-recommends \
- postgresql-client imagemagick libproj-dev proj-bin libjemalloc2 && \
+ postgresql-client imagemagick libproj-dev proj-bin libjemalloc2 p7zip-full && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
diff --git a/Gemfile b/Gemfile
index a33f8c0198..fbe02a049a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -26,6 +26,7 @@ gem "decidim-category_enhanced", "~> 0.0.1"
gem "decidim-cleaner"
gem "decidim-custom_proposal_states", git: "https://github.com/alecslupu-pfa/decidim-module-custom_proposal_states", branch: DECIDIM_BRANCH
gem "decidim-decidim_awesome", git: "https://github.com/decidim-ice/decidim-module-decidim_awesome", branch: DECIDIM_BRANCH
+gem "decidim-emitter", git: "https://github.com/OpenSourcePolitics/decidim-module-emitter.git"
gem "decidim-extended_socio_demographic_authorization_handler", git: "https://github.com/OpenSourcePolitics/decidim-module-extended_socio_demographic_authorization_handler.git",
branch: DECIDIM_BRANCH
gem "decidim-extra_user_fields", git: "https://github.com/OpenSourcePolitics/decidim-module-extra_user_fields.git", branch: "temp/twilio-compatibility-0.27"
diff --git a/Gemfile.lock b/Gemfile.lock
index f8408c2dbd..979cd98b7a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -7,6 +7,14 @@ GIT
decidim-core (>= 0.27.0)
deface (~> 1.5)
+GIT
+ remote: https://github.com/OpenSourcePolitics/decidim-module-emitter.git
+ revision: 8633ea56b422eecfe7d8730c89f191387f860e55
+ specs:
+ decidim-emitter (0.1.0)
+ decidim-core (~> 0.27.0)
+ decidim-participatory_processes (~> 0.27.0)
+
GIT
remote: https://github.com/OpenSourcePolitics/decidim-module-extended_socio_demographic_authorization_handler.git
revision: adec5e66cd07b5e5fdce5562453a7e8d6de88013
@@ -52,10 +60,10 @@ GIT
GIT
remote: https://github.com/OpenSourcePolitics/decidim-module-homepage_interactive_map.git
- revision: dd685166fdf953a11bd6a9e0dac56feca3bd0708
+ revision: 1ff222533cb3e7c30c8112a56c09c217c0530dbc
branch: release/0.27-stable
specs:
- decidim-homepage_interactive_map (2.0.0)
+ decidim-homepage_interactive_map (2.0.1)
decidim-admin (>= 0.25.0, < 0.28)
decidim-core (>= 0.25.0, < 0.28)
decidim-dev (>= 0.25.0, < 0.28)
@@ -1179,6 +1187,7 @@ DEPENDENCIES
decidim-custom_proposal_states!
decidim-decidim_awesome!
decidim-dev (~> 0.27.0)
+ decidim-emitter!
decidim-extended_socio_demographic_authorization_handler!
decidim-extra_user_fields!
decidim-friendly_signup!
diff --git a/app/commands/admin/reorder_scopes.rb b/app/commands/admin/reorder_scopes.rb
new file mode 100644
index 0000000000..ea2a3e43bf
--- /dev/null
+++ b/app/commands/admin/reorder_scopes.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Admin
+ class ReorderScopes < Decidim::Command
+ def initialize(organization, scope, ids)
+ @organization = organization
+ @scope = scope
+ @ids = ids
+ end
+
+ def call
+ return broadcast(:invalid) if @ids.blank?
+
+ reorder_scopes
+ broadcast(:ok)
+ end
+
+ def collection
+ @collection ||= Decidim::Scope.where(id: @ids, organization: @organization)
+ end
+
+ def reorder_scopes
+ transaction do
+ set_new_weights
+ end
+ end
+
+ def set_new_weights
+ @ids.each do |id|
+ current_scope = collection.find { |block| block.id == id.to_i }
+ next if current_scope.blank?
+
+ current_scope.update!(weight: @ids.index(id) + 1)
+ end
+ end
+ end
+end
diff --git a/app/forms/decidim/admin/attachment_form.rb b/app/forms/decidim/admin/attachment_form.rb
new file mode 100644
index 0000000000..c0ab45b5a6
--- /dev/null
+++ b/app/forms/decidim/admin/attachment_form.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Decidim
+ module Admin
+ # A form object used to create attachments in a participatory process.
+ #
+ class AttachmentForm < Form
+ include TranslatableAttributes
+
+ attribute :file
+ translatable_attribute :title, String
+ translatable_attribute :description, String
+ attribute :weight, Integer, default: 0
+ attribute :attachment_collection_id, Integer
+ attribute :send_notification_to_followers, Boolean, default: false
+
+ mimic :attachment
+
+ validates :file, presence: true, unless: :persisted?
+ validates :file, passthru: { to: Decidim::Attachment }
+ validates :title, :description, translatable_presence: true
+ validates :attachment_collection, presence: true, if: ->(form) { form.attachment_collection_id.present? }
+ validates :attachment_collection_id, inclusion: { in: :attachment_collection_ids }, allow_blank: true
+
+ delegate :attached_to, to: :context, prefix: false
+
+ alias organization current_organization
+
+ def attachment_collections
+ @attachment_collections ||= attached_to.attachment_collections
+ end
+
+ def attachment_collection
+ @attachment_collection ||= attachment_collections.find_by(id: attachment_collection_id)
+ end
+
+ private
+
+ def attachment_collection_ids
+ @attachment_collection_ids ||= attachment_collections.pluck(:id)
+ end
+ end
+ end
+end
diff --git a/app/forms/decidim/user_interest_scope_form.rb b/app/forms/decidim/user_interest_scope_form.rb
new file mode 100644
index 0000000000..319d2a3506
--- /dev/null
+++ b/app/forms/decidim/user_interest_scope_form.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Decidim
+ # The form object that handles the data behind updating a user's
+ # interests in their profile page.
+ class UserInterestScopeForm < Form
+ mimic :scope
+
+ attribute :name, JsonbAttributes
+ attribute :checked, Boolean
+ attribute :children, Array[UserInterestScopeForm]
+
+ def map_model(model_hash)
+ scope = model_hash[:scope]
+ user = model_hash[:user]
+
+ self.id = scope.id
+ self.name = scope.name
+ self.checked = user.interested_scopes_ids.include?(scope.id)
+ self.children = scope.children.sort_by(&:weight).map do |children_scope|
+ UserInterestScopeForm.from_model(scope: children_scope, user: user)
+ end
+ end
+ end
+end
diff --git a/app/forms/decidim/user_interests_form.rb b/app/forms/decidim/user_interests_form.rb
new file mode 100644
index 0000000000..6220393fa7
--- /dev/null
+++ b/app/forms/decidim/user_interests_form.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Decidim
+ # The form object that handles the data behind updating a user's
+ # interests in their profile page.
+ class UserInterestsForm < Form
+ mimic :user
+
+ attribute :scopes, Array[UserInterestScopeForm]
+
+ def newsletter_notifications_at
+ return unless newsletter_notifications
+
+ Time.current
+ end
+
+ def map_model(user)
+ self.scopes = user.organization.scopes.top_level.sort_by(&:weight).map do |scope|
+ UserInterestScopeForm.from_model(scope: scope, user: user)
+ end
+ end
+ end
+end
diff --git a/app/helpers/concerns/decidim/simple_proposal/scopes_helper_override.rb b/app/helpers/concerns/decidim/simple_proposal/scopes_helper_override.rb
new file mode 100644
index 0000000000..2654d05639
--- /dev/null
+++ b/app/helpers/concerns/decidim/simple_proposal/scopes_helper_override.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Decidim
+ module SimpleProposal
+ module ScopesHelperOverride
+ extend ActiveSupport::Concern
+ included do
+ def scopes_picker_field(form, name, root: false, options: { checkboxes_on_top: true })
+ options.merge!(selected: selected_scope(form)) if selected_scope(form)
+ form.select(name, simple_scope_options(root: root, options: options), include_blank: t("decidim.scopes.prompt"))
+ end
+
+ private
+
+ def selected_scope(form)
+ form.try(:scope_id) ||
+ form.try(:settings).try(:scope_id) ||
+ form.try(:object).try(:scope_id) ||
+ form.try(:object).try(:decidim_scope_id)
+ end
+
+ def simple_scope_options(root: false, options: {})
+ scopes_array = []
+ roots = root ? root.children : ancestors
+ roots.sort_by { |ancestor| ancestor.weight || 0 }.each do |ancestor|
+ children_after_parent(ancestor, scopes_array, "")
+ end
+ selected = options.has_key?(:selected) ? options[:selected] : params.dig(:filter, :decidim_scope_id)
+ options_for_select(scopes_array, selected)
+ end
+
+ def ancestors
+ @ancestors ||= current_organization.scopes.where(parent_id: nil)
+ end
+
+ def children_after_parent(ancestor, array, prefix)
+ array << ["#{prefix} #{translated_attribute(ancestor.name)}", ancestor.id]
+ ancestor.children.sort_by { |child| child.weight || 0 }.each do |child|
+ children_after_parent(child, array, "#{prefix}-")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/packs/entrypoints/application.js b/app/packs/entrypoints/application.js
index fc8ab3b017..1d2bfce7d8 100644
--- a/app/packs/entrypoints/application.js
+++ b/app/packs/entrypoints/application.js
@@ -17,3 +17,5 @@
// Activate Active Storage
// import * as ActiveStorage from "@rails/activestorage"
// ActiveStorage.start()
+
+import "src/decidim/admin/reorder_scopes";
diff --git a/app/packs/entrypoints/decidim_custom_scopes.scss b/app/packs/entrypoints/decidim_custom_scopes.scss
new file mode 100644
index 0000000000..eace17edd8
--- /dev/null
+++ b/app/packs/entrypoints/decidim_custom_scopes.scss
@@ -0,0 +1 @@
+@import "stylesheets/decidim/scopes/scopes-custom.scss";
\ No newline at end of file
diff --git a/app/packs/src/decidim/admin/reorder_scopes.js b/app/packs/src/decidim/admin/reorder_scopes.js
new file mode 100644
index 0000000000..b997c3c509
--- /dev/null
+++ b/app/packs/src/decidim/admin/reorder_scopes.js
@@ -0,0 +1,19 @@
+$(document).ready(() => {
+ let activeBlocks = Array.prototype.slice.call(document.querySelectorAll(".js-list-scopes li"));
+ const defaultOrder = activeBlocks.map(block => block.dataset.scopeId);
+
+ document.addEventListener("dragend", () => {
+ activeBlocks = Array.prototype.slice.call(document.querySelectorAll(".js-list-scopes li"));
+ let activeBlocksManifestName = activeBlocks.map(block => block.dataset.scopeId);
+ let sortUrl = document.querySelector(".js-list-scopes").dataset.sortUrl;
+
+ if (JSON.stringify(activeBlocksManifestName) === JSON.stringify(defaultOrder)) { return; }
+
+ $.ajax({
+ method: "PUT",
+ url: sortUrl,
+ contentType: "application/json",
+ data: JSON.stringify({ manifests: activeBlocksManifestName })
+ });
+ })
+});
\ No newline at end of file
diff --git a/app/packs/stylesheets/decidim/scopes/_scopes-custom.scss b/app/packs/stylesheets/decidim/scopes/_scopes-custom.scss
new file mode 100644
index 0000000000..fd6e3f4f1f
--- /dev/null
+++ b/app/packs/stylesheets/decidim/scopes/_scopes-custom.scss
@@ -0,0 +1,18 @@
+.draggable-list .draggable-content {
+ cursor: move;
+ justify-content: space-between;
+ align-items: center;
+ font-weight: 600;
+ border: none !important;
+ background-color: transparent !important;
+ padding: 0.5rem 1rem;
+}
+
+.custom-text {
+ color: black;
+}
+
+.custom-list {
+ border: 1px solid lightgray !important;
+ margin: 0.4rem
+}
\ No newline at end of file
diff --git a/app/services/decidim/download_your_data_exporter.rb b/app/services/decidim/download_your_data_exporter.rb
index 3cb8432f0c..893d16e383 100644
--- a/app/services/decidim/download_your_data_exporter.rb
+++ b/app/services/decidim/download_your_data_exporter.rb
@@ -30,7 +30,7 @@ def export
save_user_data(tmpdir, user_data)
save_user_attachments(tmpdir, user_attachments)
- SevenZipWrapper.compress_and_encrypt(filename: @path, password: @password, input_directory: tmpdir)
+ Decidim::SevenZipWrapper.compress_and_encrypt(filename: @path, password: @password, input_directory: tmpdir)
end
private
@@ -59,6 +59,9 @@ def save_user_data(tmpdir, user_data)
next if exporter_data.read == "\n"
file_name = File.join(tmpdir, "#{entity}-#{exporter_data.filename}")
+
+ dir_path = File.dirname(file_name)
+ FileUtils.mkdir_p(dir_path) unless Dir.exist?(dir_path)
File.write(file_name, exporter_data.read)
end
end
@@ -70,7 +73,8 @@ def save_user_attachments(tmpdir, user_attachments)
blobs = attachment.is_a?(ActiveStorage::Attached::One) ? [attachment.blob] : attachment.blobs
blobs.each do |blob|
- Dir.mkdir(File.join(tmpdir, entity.parameterize))
+ dir_path = File.join(tmpdir, entity.parameterize)
+ Dir.mkdir(dir_path) unless Dir.exist?(dir_path)
file_name = File.join(tmpdir, entity.parameterize, blob.filename.to_s)
blob.open do |blob_file|
File.write(file_name, blob_file.read.force_encoding("UTF-8"))
diff --git a/app/views/decidim/admin/attachments/_form.html.erb b/app/views/decidim/admin/attachments/_form.html.erb
new file mode 100644
index 0000000000..1f8a2230bc
--- /dev/null
+++ b/app/views/decidim/admin/attachments/_form.html.erb
@@ -0,0 +1,33 @@
+
+
+
+ <%= title %>
+
+
+
+
+
+ <%= form.translated :text_field, :title, autofocus: true %>
+
+
+
+ <%= form.number_field :weight %>
+
+
+
+ <%= form.translated :text_field, :description %>
+
+
+
+ <%= form.select :attachment_collection_id, @form.attachment_collections.collect { |c| [translated_attribute(c.name), c.id] }, include_blank: true %>
+
+
+
+ <%= form.upload :file, required: true %>
+
+
+
+ <%= form.check_box :send_notification_to_followers, label: t(".send_notification_to_followers") %>
+
+
+
diff --git a/app/views/decidim/admin/scopes/index.html.erb b/app/views/decidim/admin/scopes/index.html.erb
new file mode 100644
index 0000000000..4b9dce24ce
--- /dev/null
+++ b/app/views/decidim/admin/scopes/index.html.erb
@@ -0,0 +1,65 @@
+<% add_decidim_page_title(t("decidim.admin.titles.scopes")) %>
+
+
+
+
+
+
+ <% if parent_scope %>
+ <%= scope_breadcrumbs(parent_scope).join(" - ").html_safe %> <%= link_to t("actions.add", scope: "decidim.admin"), new_scope_scope_path(parent_scope), class: "button tiny button--title" if allowed_to? :create, :scope %><%= link_to t("actions.edit", scope: "decidim.admin"), edit_scope_path(parent_scope), class: "button tiny button--title" if allowed_to? :edit, :scope, scope: parent_scope %>
+ <% else %>
+ <%= t "decidim.admin.titles.scopes" %> <%= link_to t("actions.add", scope: "decidim.admin"), new_scope_path, class: "button tiny button--title" if allowed_to? :create, :scope %>
+ <% end %>
+
+
+
+ <% if @scopes.any? %>
+
+ <% else %>
+
<%= t("decidim.admin.scopes.no_scopes") %>
+ <% end %>
+
+
+
+
+
+
+<%= stylesheet_pack_tag "decidim_custom_scopes", media: "all" %>
+<%= javascript_pack_tag 'application' %>
\ No newline at end of file
diff --git a/app/views/decidim/participatory_processes/admin/participatory_processes/_form.html.erb b/app/views/decidim/participatory_processes/admin/participatory_processes/_form.html.erb
new file mode 100644
index 0000000000..258a900133
--- /dev/null
+++ b/app/views/decidim/participatory_processes/admin/participatory_processes/_form.html.erb
@@ -0,0 +1,234 @@
+
+
+
<%= t(".title") %>
+
+
+
+
+ <%= form.translated :text_field, :title, autofocus: true %>
+
+
+
+ <%= form.translated :text_field, :subtitle %>
+
+
+
+ <%= form.number_field :weight %>
+
+
+
+
+ <%= form.text_field :slug %>
+
+ <%== t(".slug_help", url: decidim_form_slug_url(:processes, form.object.slug)) %>
+
+
+
+
+ <%= form.text_field :hashtag %>
+
+
+
+
+ <%= form.translated :editor, :short_description %>
+
+
+
+ <%= form.translated :editor, :description, toolbar: :full, lines: 25 %>
+
+
+
+ <%= form.translated :editor, :announcement %>
+
<%== t(".announcement_help") %>
+
+
+
+ <% if Decidim::Map.available?(:geocoding) %>
+
+ <%= form.text_field :address %>
+
<%== t(".address_help") %>
+
+ <% end %>
+
+
+
<%= t(".duration") %>
+
+
+
+
+
+ <%= form.date_field :start_date %>
+
+
+
+ <%= form.date_field :end_date %>
+
+
+
+
+
+
<%= t(".images") %>
+
+
+
+
+
+ <%= form.upload :hero_image %>
+
+
+
+ <%= form.upload :banner_image %>
+
+
+
+
+
+
<%= t(".metadata") %>
+
+
+
+
+
+ <%= form.translated :text_field, :developer_group %>
+
+
+
+ <%= form.translated :text_field, :local_area %>
+
+
+
+
+ <%= form.translated :text_field, :meta_scope %>
+
+
+
+ <%= form.translated :text_field, :target %>
+
+
+
+ <%= form.translated :text_field, :participatory_scope %>
+
+
+
+ <%= form.translated :text_field, :participatory_structure %>
+
+
+
+
+
<%= t(".filters") %>
+
+
+
+
+ <%= form.check_box :scopes_enabled %>
+
+
+
+ <%= scopes_picker_field form, :scope_id, root: nil %>
+
+
+ <%= form.collection_select :scope_type_max_depth_id,
+ organization_scope_depths,
+ :id,
+ :name,
+ scope_type_depth_select_options,
+ scope_type_depth_select_html_options %>
+
+ <%== t(".scope_type_max_depth_help") %>
+
+
+
+
+
+ <%= form.areas_select :area_id,
+ areas_for_select(current_organization),
+ selected: current_participatory_process.try(:decidim_area_id),
+ include_blank: t(".select_an_area") %>
+
+
+
+
+
<%= t(".visbility") %>
+
+
+
+
+ <% if process_groups_for_select %>
+ <%= form.select :participatory_process_group_id,
+ process_groups_for_select,
+ include_blank: t(".select_process_group") %>
+ <% end %>
+
+
+
+ <%= form.check_box :private_space %>
+
+
+ <%= form.check_box :promoted %>
+
+
+
+
+
<%= t(".emitter") %>
+
+
+
+
+ <%= form.select :emitter_select, options_for_select(emitter_options), { :include_blank => true, label: t(".emitter_logo_select") }, class: "select-emitter" %>
+
+
+
+
+ <%= form.text_field :emitter_name_image, label: t(".emitter_name") %>
+
+
+ <%= form.upload :emitter_image, label: t(".emitter_logo"), help_i18n_scope: "decidim.admin.forms.file_help.emitter" %>
+
+ <% if form.object.emitter_name.present? %>
+
+ <%= form.text_field :emitter_read_name, { :readonly => true, :label => t(".emitter_now") } %>
+
+ <% end %>
+
+
+
+
<%= t(".related_processes") %>
+
+
+
+
+ <%= form.select(
+ :related_process_ids,
+ @form.processes.order(title: :asc).map{|process| [translated_attribute(process.title), process.id]},
+ { include_blank: true },
+ { multiple: true, class: "chosen-select" }
+ ) %>
+
+
+
+
+
<%= t(".other") %>
+
+
+
+
+ <%= form.check_box :show_statistics %>
+
+
+
+ <%= form.check_box :show_metrics %>
+
+
+ <% if @form.participatory_process_types_for_select.present? %>
+
+ <%= form.select(
+ :participatory_process_type_id,
+ @form.participatory_process_types_for_select,
+ include_blank: t(".select_participatory_process_type")
+ ) %>
+
+ <% end %>
+
+
+
+<%= javascript_pack_tag "decidim_participatory_processes_admin" %>
\ No newline at end of file
diff --git a/app/views/decidim/proposals/proposals/index.html.erb b/app/views/decidim/proposals/proposals/index.html.erb
index 5bcaade268..d6ab9a56ed 100644
--- a/app/views/decidim/proposals/proposals/index.html.erb
+++ b/app/views/decidim/proposals/proposals/index.html.erb
@@ -1,30 +1,28 @@
<%= render partial: "decidim/shared/component_announcement" %>
<% if component_settings.geocoding_enabled? %>
- <% cache @all_geocoded_proposals do %>
- <%= dynamic_map_for proposals_data_for_map(@all_geocoded_proposals) do %>
-