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? %> +
+ + + + + + + + +
<%= t("models.scope.fields.name", scope: "decidim.admin") %><%= t("models.scope.fields.scope_type", scope: "decidim.admin") %>
+ + +
    "> + <% @scopes.each do |scope| %> +
  • +
    +
    + <%= icon "move", class: "icon--small", role: "img", "aria-hidden": true %> + <%= link_to translated_attribute(scope.name), scope_scopes_path(scope), class:"custom-text" %> +
    +
    + <%= icon_link_to "zoom-in", scope_scopes_path(scope), t("actions.browse", scope: "decidim.admin"), class: "action-icon--browse", method: :get, data: {} %> + + <% if allowed_to? :update, :scope, scope: scope %> + <%= icon_link_to "pencil", [:edit, scope], t("actions.edit", scope: "decidim.admin"), class: "action-icon--edit", method: :get, data: {} %> + <% end %> + + <% if allowed_to? :destroy, :scope, scope: scope %> + <%= icon_link_to "circle-x", scope, t("actions.destroy", scope: "decidim.admin"), class: "action-icon--remove", method: :delete, data: { confirm: t("actions.confirm_destroy", scope: "decidim.admin") } %> + <% end %> +
    +
    +
  • + <% end %> +
+ +
+
+ <% 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 %> - <% end %> <% end %> <%= render partial: "voting_rules" %> diff --git a/config/application.rb b/config/application.rb index 80c2f74eaa..26a54c15b8 100644 --- a/config/application.rb +++ b/config/application.rb @@ -46,21 +46,32 @@ class Application < Rails::Application end config.after_initialize do + # Controllers require "extends/controllers/decidim/devise/sessions_controller_extends" require "extends/controllers/decidim/editor_images_controller_extends" - require "extends/services/decidim/iframe_disabler_extends" - require "extends/helpers/decidim/icon_helper_extends" - require "extends/commands/decidim/initiatives/admin/update_initiative_answer_extends" + require "extends/controllers/decidim/proposals/proposals_controller_extends" + require "extends/controllers/decidim/newsletters_controller_extends" + require "extends/controllers/decidim/admin/scopes_controller_extends" + require "extends/controllers/decidim/scopes_controller_extends" require "extends/controllers/decidim/initiatives/committee_requests_controller_extends" + # Models require "extends/models/decidim/budgets/project_extends" require "extends/models/decidim/authorization_extends" + require "extends/models/decidim/decidim_awesome/proposal_extra_field_extends" + # Services + require "extends/services/decidim/iframe_disabler_extends" + # Helpers + require "extends/helpers/decidim/meetings/directory/application_helper_extends" + require "extends/helpers/decidim/icon_helper_extends" + require "extends/helpers/decidim/check_boxes_tree_helper_extends" + # Forms + require "extends/forms/decidim/initiatives/initiative_form_extends" require "extends/forms/decidim/initiatives/admin/initiative_form_extends" + # Commands + require "extends/commands/decidim/initiatives/admin/update_initiative_answer_extends" require "extends/commands/decidim/budgets/admin/import_proposals_to_budgets_extends" - require "extends/controllers/decidim/newsletters_controller_extends" require "extends/commands/decidim/admin/destroy_participatory_space_private_user_extends" - require "extends/controllers/decidim/proposals/proposals_controller_extends" - require "extends/forms/decidim/initiatives/initiative_form_extends" - require "extends/models/decidim/decidim_awesome/proposal_extra_field_extends" + require "extends/commands/decidim/admin/create_attachment_extends" Decidim::GraphiQL::Rails.config.tap do |config| config.initial_query = "{\n deployment {\n version\n branch\n remote\n upToDate\n currentCommit\n latestCommit\n locallyModified\n }\n}".html_safe diff --git a/config/environments/development.rb b/config/environments/development.rb index d667dc6158..6ed8a74c31 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -60,4 +60,5 @@ # Setting this to 100 years should be enough config.global_id.expires_in = 100.years config.deface.enabled = ENV.fetch("DEFACE_ENABLED", nil) == "true" + config.log_tags = [:uuid, :remote_ip] end diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 7a81861781..db9522009e 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -119,6 +119,7 @@ ignore_missing: - decidim.term_customizer.admin.actions.* - decidim.term_customizer.admin.add_translations.index.* - decidim.term_customizer.admin.models.translations.fields.* + - decidim.participatory_processes.admin.participatory_processes.form.* # Consider these keys used: ignore_unused: diff --git a/config/initializers/decidim_awesome.rb b/config/initializers/decidim_awesome.rb new file mode 100644 index 0000000000..24a30cdbc2 --- /dev/null +++ b/config/initializers/decidim_awesome.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Decidim::DecidimAwesome.configure do |config| + config.weighted_proposal_voting = Rails.application.secrets.dig(:decidim, :decidim_awesome, :weighted_proposal_voting_enabled)&.to_sym +end diff --git a/config/initializers/half_signup.rb b/config/initializers/half_signup.rb index 29512c6406..33b729485b 100644 --- a/config/initializers/half_signup.rb +++ b/config/initializers/half_signup.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +return unless defined?(Decidim::HalfSignup) + Decidim::HalfSignup.configure do |config| config.show_tos_page_after_signup = Rails.application.secrets.dig(:decidim, :half_signup, :show_tos_page_after_signup) config.auth_code_length = 4 diff --git a/config/locales/en.yml b/config/locales/en.yml index 56be2f097d..6eaf94bee8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -16,13 +16,34 @@ en: file: importing file decidim: admin: + actions: + add: Add + browse: Browse + confirm_destroy: Confirm destroy + destroy: Destroy + edit: Edit + attachments: + form: + send_notification_to_followers: Send a notification to all the people following the consultation who have agreed to receive email notifications exports: export_as: "%{name} as %{export_format}" notice: Your export is currently in progress. You'll receive an email when it's complete. + models: + scope: + fields: + name: Name + scope_type: Scope type participatory_space_private_users: create: error: Error success: Success + scopes: + no_scopes: No scopes at this level. + update: + error: There was a problem updating this scope. + success: Scope updated successfully + titles: + scopes: Scopes amendments: emendation: announcement: @@ -124,6 +145,9 @@ en: see_all_initiatives: See all initiatives unavailable_scope: Unavailable scope meetings: + application_helper: + filter_scope_values: + all: All directory: meetings: index: @@ -175,6 +199,7 @@ en: change: Change choose: Choose currently_selected: Currently selected + prompt: Select a scope shared: login_modal: close_modal: Close modal diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 6e725c5912..d25c7083c1 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -16,15 +16,36 @@ fr: file: importer un fichier d'utilisateurs decidim: admin: + actions: + add: Ajouter + browse: Naviguer + confirm_destroy: Confirmer la suppression + destroy: Supprimer + edit: Modifier + attachments: + form: + send_notification_to_followers: Envoyer une notification à toutes les personnes qui suivent la concertation ayant accepté de recevoir des notifications par mail exports: export_as: "%{name} au format %{export_format}" notice: Votre exportation est en cours. Vous recevrez un e-mail quand elle sera terminée. menu: admin_accountability: Admin accountability + models: + scope: + fields: + name: Nom + scope_type: Type de secteur participatory_space_private_users: create: error: Erreur success: Succès + scopes: + no_scopes: Aucun secteur à ce niveau. + update: + error: Il y a eu une erreur lors de la mise à jour du secteur. + success: Secteur mis à jour avec succès. + titles: + scopes: Secteurs amendments: emendation: announcement: @@ -126,6 +147,9 @@ fr: see_all_initiatives: Voir toutes les pétitions unavailable_scope: Portée indisponible meetings: + application_helper: + filter_scope_values: + all: Tous directory: meetings: index: @@ -177,6 +201,7 @@ fr: change: Modifier choose: Sélectionner currently_selected: Sélectionné + prompt: Sélectionnez un périmètre d'application shared: login_modal: close_modal: Fermer diff --git a/config/secrets.yml b/config/secrets.yml index 2f166fdfd3..78f94c3731 100644 --- a/config/secrets.yml +++ b/config/secrets.yml @@ -13,6 +13,8 @@ default: &default asset_host: <%= ENV["ASSET_HOST"] %> decidim: + decidim_awesome: + weighted_proposal_voting_enabled: <%= ENV.fetch("DECIDIM_AWESOME_WEIGHTED_PROPOSAL_VOTING_ENABLED", "disabled") %> admin_password: expiration_days: <%= ENV.fetch("DECIDIM_ADMIN_PASSWORD_EXPIRATION_DAYS", 365).to_i %> min_length: <%= ENV.fetch("DECIDIM_ADMIN_PASSWORD_MIN_LENGTH", 15).to_i %> diff --git a/db/migrate/20240412112810_add_weight_to_scopes.rb b/db/migrate/20240412112810_add_weight_to_scopes.rb new file mode 100644 index 0000000000..bbcbc86565 --- /dev/null +++ b/db/migrate/20240412112810_add_weight_to_scopes.rb @@ -0,0 +1,5 @@ +class AddWeightToScopes < ActiveRecord::Migration[6.1] + def change + add_column :decidim_scopes, :weight, :integer, default: 0 + end +end \ No newline at end of file diff --git a/db/migrate/20241118114335_add_emitter_to_decidim_participatory_process.decidim_emitter.rb b/db/migrate/20241118114335_add_emitter_to_decidim_participatory_process.decidim_emitter.rb new file mode 100644 index 0000000000..ba525aa3c4 --- /dev/null +++ b/db/migrate/20241118114335_add_emitter_to_decidim_participatory_process.decidim_emitter.rb @@ -0,0 +1,14 @@ +# This migration comes from decidim_emitter (originally 20240417082337) +class AddEmitterToDecidimParticipatoryProcess < ActiveRecord::Migration[6.1] + def up + # Ensure that the column is a string and check if it exists + add_column :decidim_participatory_processes, :emitter, :string, if_not_exists: true + change_column :decidim_participatory_processes, :emitter, :string + + add_column :decidim_participatory_processes, :emitter_name, :text, if_not_exists: true + end + + def down + remove_column :decidim_participatory_processes, :emitter_name, if_exists: true + end +end diff --git a/db/schema.rb b/db/schema.rb index b817eee201..cbad410846 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2024_10_28_094242) do +ActiveRecord::Schema.define(version: 2024_11_18_114335) do # These are extensions that must be enabled in order to support this database enable_extension "ltree" @@ -1518,6 +1518,8 @@ t.float "longitude" t.boolean "display_linked_assemblies", default: false t.bigint "decidim_participatory_process_type_id" + t.string "emitter" + t.text "emitter_name" t.index ["decidim_area_id"], name: "index_decidim_participatory_processes_on_decidim_area_id" t.index ["decidim_organization_id", "slug"], name: "index_unique_process_slug_and_organization", unique: true t.index ["decidim_organization_id"], name: "index_decidim_processes_on_decidim_organization_id" @@ -1764,6 +1766,7 @@ t.string "code", null: false t.integer "part_of", default: [], null: false, array: true t.jsonb "geojson" + t.integer "weight", default: 0 t.index ["decidim_organization_id", "code"], name: "index_decidim_scopes_on_decidim_organization_id_and_code", unique: true t.index ["decidim_organization_id"], name: "index_decidim_scopes_on_decidim_organization_id" t.index ["parent_id"], name: "index_decidim_scopes_on_parent_id" diff --git a/lib/extends/commands/decidim/admin/create_attachment_extends.rb b/lib/extends/commands/decidim/admin/create_attachment_extends.rb new file mode 100644 index 0000000000..ea1efbf28c --- /dev/null +++ b/lib/extends/commands/decidim/admin/create_attachment_extends.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require "active_support/concern" + +module CreateAttachmentExtends + extend ActiveSupport::Concern + + included do + def notify_followers + return unless @attachment.attached_to.is_a?(Decidim::Followable) + return unless form.send_notification_to_followers + + Decidim::EventsManager.publish( + event: "decidim.events.attachments.attachment_created", + event_class: Decidim::AttachmentCreatedEvent, + resource: @attachment, + followers: @attachment.attached_to.followers, + extra: { force_email: true }, + force_send: true + ) + end + end +end + +Decidim::Admin::CreateAttachment.include(CreateAttachmentExtends) diff --git a/lib/extends/controllers/decidim/admin/scopes_controller_extends.rb b/lib/extends/controllers/decidim/admin/scopes_controller_extends.rb new file mode 100644 index 0000000000..3db4a906b1 --- /dev/null +++ b/lib/extends/controllers/decidim/admin/scopes_controller_extends.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require "active_support/concern" + +module Decidim + module Admin + module ScopesControllerExtends + extend ActiveSupport::Concern + included do + def index + enforce_permission_to :read, :scope + @scopes = children_scopes.sort_by(&:weight) + end + + def update + enforce_permission_to :update, :scope, scope: scope + @form = form(ScopeForm).from_params(params) + + return update_scopes if params[:id] == "refresh_scopes" + + UpdateScope.call(scope, @form) do + on(:ok) do + flash[:notice] = I18n.t("scopes.update.success", scope: "decidim.admin") + redirect_to current_scopes_path + end + + on(:invalid) do + flash.now[:alert] = I18n.t("scopes.update.error", scope: "decidim.admin") + render :edit + end + end + end + + private + + def update_scopes + ::Admin::ReorderScopes.call(current_organization, :scopes, params[:manifests]) do + on(:ok) do + flash[:notice] = I18n.t("scopes.update.success", scope: "decidim.admin") + end + end + end + end + end + end +end + +Decidim::Admin::ScopesController.include(Decidim::Admin::ScopesControllerExtends) diff --git a/lib/extends/controllers/decidim/scopes_controller_extends.rb b/lib/extends/controllers/decidim/scopes_controller_extends.rb new file mode 100644 index 0000000000..62197557b9 --- /dev/null +++ b/lib/extends/controllers/decidim/scopes_controller_extends.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "active_support/concern" + +module ScopesControllerExtends + extend ActiveSupport::Concern + included do + def picker + enforce_permission_to :pick, :scope + + context = picker_context(root, title, max_depth) + required = params&.[](:required) != "false" + + scopes, parent_scopes = resolve_picker_scopes(root, current) + + render( + :picker, + layout: nil, + locals: { + required: required, + title: title, + root: root, + current: (current || root), + scopes: scopes&.sort_by(&:weight), + parent_scopes: parent_scopes.sort_by(&:weight), + picker_target_id: (params[:target_element_id] || "content"), + global_value: params[:global_value], + max_depth: max_depth, + context: context + } + ) + end + end +end + +Decidim::ScopesController.include(ScopesControllerExtends) diff --git a/lib/extends/helpers/decidim/check_boxes_tree_helper_extends.rb b/lib/extends/helpers/decidim/check_boxes_tree_helper_extends.rb new file mode 100644 index 0000000000..9688decdf4 --- /dev/null +++ b/lib/extends/helpers/decidim/check_boxes_tree_helper_extends.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module CheckBoxesTreeHelperExtends + def filter_scopes_values + return filter_scopes_values_from_parent(current_component.scope) if current_component.scope.present? + + main_scopes = current_participatory_space.scopes.top_level + .includes(:scope_type, :children) + .sort_by(&:weight) + filter_scopes_values_from(main_scopes) + end + + def filter_scopes_values_from_parent(scope) + scopes_values = [] + scope.children.sort_by(&:weight).each do |child| + unless child.children + scopes_values << Decidim::CheckBoxesTreeHelper::TreePoint.new(child.id.to_s, translated_attribute(child.name, current_participatory_space.organization)) + next + end + scopes_values << Decidim::CheckBoxesTreeHelper::TreeNode.new( + Decidim::CheckBoxesTreeHelper::TreePoint.new(child.id.to_s, translated_attribute(child.name, current_participatory_space.organization)), + scope_children_to_tree(child) + ) + end + + filter_tree_from(scopes_values) + end + + def filter_scopes_values_from(scopes) + scopes_values = scopes.compact.sort_by(&:weight).flat_map do |scope| + Decidim::CheckBoxesTreeHelper::TreeNode.new( + Decidim::CheckBoxesTreeHelper::TreePoint.new(scope.id.to_s, translated_attribute(scope.name, current_participatory_space.organization)), + scope_children_to_tree(scope) + ) + end + + scopes_values.prepend(Decidim::CheckBoxesTreeHelper::TreePoint.new("global", t("decidim.scopes.global"))) if current_participatory_space.scope.blank? + + filter_tree_from(scopes_values) + end + + def scope_children_to_tree(scope) + return if scope.scope_type && scope.scope_type == current_participatory_space.try(:scope_type_max_depth) + return unless scope.children.any? + + sorted_children = scope.children.includes(:scope_type, :children).sort_by(&:weight) + + sorted_children.flat_map do |child| + Decidim::CheckBoxesTreeHelper::TreeNode.new( + Decidim::CheckBoxesTreeHelper::TreePoint.new(child.id.to_s, translated_attribute(child.name, current_participatory_space.organization)), + scope_children_to_tree(child) + ) + end + end +end + +Decidim::CheckBoxesTreeHelper.module_eval do + prepend(CheckBoxesTreeHelperExtends) +end diff --git a/lib/extends/helpers/decidim/meetings/directory/application_helper_extends.rb b/lib/extends/helpers/decidim/meetings/directory/application_helper_extends.rb new file mode 100644 index 0000000000..60e56f1f02 --- /dev/null +++ b/lib/extends/helpers/decidim/meetings/directory/application_helper_extends.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "active_support/concern" +module ApplicationHelperExtends + extend ActiveSupport::Concern + include Decidim::CheckBoxesTreeHelper + + included do + def directory_filter_scopes_values + main_scopes = current_organization.scopes.top_level + scopes_values = main_scopes.includes(:scope_type, :children).sort_by(&:weight).flat_map do |scope| + TreeNode.new( + TreePoint.new(scope.id.to_s, translated_attribute(scope.name, current_organization)), + scope_children_to_tree(scope) + ) + end + + scopes_values.prepend(TreePoint.new("global", t("decidim.scopes.global"))) + + TreeNode.new( + TreePoint.new("", t("decidim.meetings.application_helper.filter_scope_values.all")), + scopes_values + ) + end + + def scope_children_to_tree(scope) + return unless scope.children.any? + + scope.children.includes(:scope_type, :children).sort_by(&:weight).flat_map do |child| + TreeNode.new( + TreePoint.new(child.id.to_s, translated_attribute(child.name, current_organization)), + scope_children_to_tree(child) + ) + end + end + end +end + +Decidim::Meetings::Directory::ApplicationHelper.include(ApplicationHelperExtends) diff --git a/spec/commands/decidim/admin/create_attachment_spec.rb b/spec/commands/decidim/admin/create_attachment_spec.rb new file mode 100644 index 0000000000..35425848ae --- /dev/null +++ b/spec/commands/decidim/admin/create_attachment_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::Admin + describe CreateAttachment do + subject { described_class.call(form, attached_to, user) } + let(:user) { create(:user) } + let(:send_notification) { true } + let(:form) do + instance_double( + AttachmentForm, + title: { + en: "An image", + ca: "Una imatge", + es: "Una imagen" + }, + description: { + en: "A city", + ca: "Una ciutat", + es: "Una ciudad" + }, + file: file, + attachment_collection: nil, + send_notification_to_followers: send_notification, + weight: 0 + ) + end + let(:file) { upload_test_file(Decidim::Dev.test_file("city.jpeg", "image/jpeg")) } + let(:attached_to) { create(:participatory_process) } + + describe "when valid" do + before do + allow(form).to receive(:invalid?).and_return(false) + end + + it "broadcasts :ok and creates the component" do + expect do + subject + end.to broadcast(:ok) + + expect(Decidim::Attachment.count).to eq(1) + end + + it "notifies the followers" do + follower = create(:user, organization: attached_to.organization) + create(:follow, followable: attached_to, user: follower) + + expect(Decidim::EventsManager) + .to receive(:publish) + .with( + event: "decidim.events.attachments.attachment_created", + event_class: Decidim::AttachmentCreatedEvent, + resource: kind_of(Decidim::Attachment), + followers: [follower], + extra: { force_email: true }, + force_send: true + ) + + subject + end + + context "when send notification option is false" do + let(:send_notification) { false } + + it "does not notify the followers" do + follower = create(:user, organization: attached_to.organization) + create(:follow, followable: attached_to, user: follower) + + expect(Decidim::EventsManager) + .not_to receive(:publish) + + subject + end + end + + it "traces the action", versioning: true do + expect(Decidim.traceability) + .to receive(:perform_action!) + .with(:create, Decidim::Attachment, user) + .and_call_original + + expect { subject }.to change(Decidim::ActionLog, :count) + action_log = Decidim::ActionLog.last + expect(action_log.action).to eq("create") + expect(action_log.version).to be_present + end + end + + describe "when invalid" do + before do + allow(form).to receive(:invalid?).and_return(true) + end + + it "broadcasts invalid" do + expect do + subject + end.to broadcast(:invalid) + + expect(Decidim::Attachment.count).to eq(0) + end + end + end +end diff --git a/spec/factories.rb b/spec/factories.rb index febc0d8333..2c9271e1b1 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -4,6 +4,7 @@ require "decidim/proposals/test/factories" require "decidim/budgets/test/factories" require "decidim/debates/test/factories" +require "decidim/emitter/test/factories" require "decidim/meetings/test/factories" require "decidim/accountability/test/factories" require "decidim/system/test/factories" diff --git a/spec/helpers/decidim/check_boxes_tree_helper_spec.rb b/spec/helpers/decidim/check_boxes_tree_helper_spec.rb new file mode 100644 index 0000000000..df858de861 --- /dev/null +++ b/spec/helpers/decidim/check_boxes_tree_helper_spec.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + describe CheckBoxesTreeHelper do + let(:helper) do + Class.new(ActionView::Base) do + include CheckBoxesTreeHelper + include TranslatableAttributes + end.new(ActionView::LookupContext.new(ActionController::Base.view_paths), {}, []) + end + + let!(:organization) { create(:organization) } + let!(:participatory_space) { create(:participatory_process, organization: organization) } + let!(:component) { create(:component, participatory_space: participatory_space) } + + before do + allow(helper).to receive(:current_participatory_space).and_return(participatory_space) + allow(helper).to receive(:current_component).and_return(component) + end + + describe "#filter_scopes_values" do + let(:root) { helper.filter_scopes_values } + let(:leaf) { helper.filter_scopes_values.leaf } + let(:nodes) { helper.filter_scopes_values.node } + + context "when the participatory space does not have a scope" do + it "returns the global scope" do + expect(leaf.value).to eq("") + expect(nodes.count).to eq(1) + expect(nodes.first).to be_a(Decidim::CheckBoxesTreeHelper::TreePoint) + expect(nodes.first.value).to eq("global") + end + end + + context "when the participatory space has a scope with subscopes" do + let(:participatory_space) { create(:participatory_process, :with_scope, organization: organization) } + let!(:subscopes) { create_list :subscope, 5, parent: participatory_space.scope } + + it "returns all the subscopes" do + expect(leaf.value).to eq("") + expect(root).to be_a(Decidim::CheckBoxesTreeHelper::TreeNode) + expect(root.node.count).to eq(5) + end + end + + context "when the component does not have a scope" do + before do + component.update!(settings: { scopes_enabled: true, scope_id: nil }) + end + + it "returns the global scope" do + expect(leaf.value).to eq("") + expect(nodes.count).to eq(1) + expect(nodes.first).to be_a(Decidim::CheckBoxesTreeHelper::TreePoint) + expect(nodes.first.value).to eq("global") + end + end + + context "when the component has a scope with subscopes" do + let(:participatory_space) { create(:participatory_process, :with_scope, organization: organization) } + let!(:subscopes) { create_list :subscope, 5, parent: participatory_space.scope } + + before do + component.update!(settings: { scopes_enabled: true, scope_id: participatory_space.scope.id }) + end + + it "returns all the subscopes" do + expect(leaf.value).to eq("") + expect(root).to be_a(Decidim::CheckBoxesTreeHelper::TreeNode) + expect(root.node.count).to eq(5) + end + end + end + + context "when there is weight in the scopes" do + let(:participatory_space) { create(:participatory_process, :with_scope, organization: organization) } + let!(:subscopes) { create_list(:subscope, 5, parent: participatory_space.scope) } + + before do + subscopes.shuffle.each_with_index { |subscope, index| subscope.update!(weight: index) } + end + + it "returns the subscopes sorted by weight" do + expected_ids = subscopes.sort_by(&:weight).map { |subscope| subscope.id.to_s } + actual_values = helper.filter_scopes_values.node.map { |node| node.values.first.value.to_s } + expect(actual_values).to eq(expected_ids) + end + + it "assigns weights correctly after shuffle" do + weights = subscopes.map(&:weight) + expect(weights).to match_array([0, 1, 2, 3, 4]) + end + + it "sorts subscopes correctly by weight" do + sorted_subscopes = subscopes.sort_by(&:weight) + expect(subscopes.sort_by(&:weight)).to eq(sorted_subscopes) + end + + it "checks that the helper method returns sorted subscopes" do + sorted_subscopes = subscopes.sort_by(&:weight).map { |subscope| subscope.id.to_s } + expect(helper.filter_scopes_values.node.map { |node| node.values.first.value.to_s }).to eq(sorted_subscopes) + end + + it "returns false when the subscopes are not sorted by weight" do + unsorted_subscopes = subscopes.shuffle + unsorted_values = unsorted_subscopes.map { |subscope| subscope.id.to_s } + expect(helper.filter_scopes_values.node.map { |node| node.values.first.value.to_s }).not_to eq(unsorted_values) + end + + it "returns false when subscopes are not sorted in ascending order of weight" do + reversed_subscopes = subscopes.sort_by(&:weight).reverse + reversed_values = reversed_subscopes.map { |subscope| subscope.id.to_s } + expect(helper.filter_scopes_values.node.map { |node| node.values.first.value.to_s }).not_to eq(reversed_values) + end + end + end +end diff --git a/spec/helpers/decidim/meetings/directory/application_helper_spec.rb b/spec/helpers/decidim/meetings/directory/application_helper_spec.rb new file mode 100644 index 0000000000..fc0075add0 --- /dev/null +++ b/spec/helpers/decidim/meetings/directory/application_helper_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Meetings + module Directory + describe ApplicationHelper do + let(:helper) do + Class.new(ActionView::Base) do + include ApplicationHelper + include CheckBoxesTreeHelper + include TranslatableAttributes + end.new(ActionView::LookupContext.new(ActionController::Base.view_paths), {}, []) + end + let!(:organization) { create(:organization) } + let!(:parent_scope) { create(:scope, organization: organization) } + let!(:scope_one) { create(:scope, organization: organization, parent: parent_scope, weight: 1) } + let!(:scope_two) { create(:scope, organization: organization, parent: parent_scope, weight: 2) } + let!(:scope_three) { create(:scope, organization: organization, parent: parent_scope, weight: 3) } + + before do + allow(helper).to receive(:current_organization).and_return(organization) + end + + describe "#directory_filter_scopes_values" do + let(:root) { helper.directory_filter_scopes_values } + let(:leaf) { root.leaf } + let(:nodes) { root.node } + + context "when the organization has a scope with children" do + it "returns all the children ordered by weight" do + expect(root).to be_a(Decidim::CheckBoxesTreeHelper::TreeNode) + expect(nodes.last.node.count).to eq(3) + expect(nodes.last.node.first.leaf.label).to eq(scope_one.name["en"]) + expect(nodes.last.node.last.leaf.label).to eq(scope_three.name["en"]) + end + + context "and the weight of scope's children changes" do + it "returns the children ordered by the new weight" do + scope_one.update(weight: 4) + expect(root).to be_a(Decidim::CheckBoxesTreeHelper::TreeNode) + expect(nodes.last.node.count).to eq(3) + expect(nodes.last.node.first.leaf.label).to eq(scope_two.name["en"]) + expect(nodes.last.node.last.leaf.label).to eq(scope_one.name["en"]) + end + end + end + end + end + end + end +end diff --git a/spec/models/decidim/decidim_awesome/proposal_extra_field_spec.rb b/spec/models/decidim/decidim_awesome/proposal_extra_field_spec.rb index 1223979e07..b903553fbd 100644 --- a/spec/models/decidim/decidim_awesome/proposal_extra_field_spec.rb +++ b/spec/models/decidim/decidim_awesome/proposal_extra_field_spec.rb @@ -8,6 +8,7 @@ module Decidim::DecidimAwesome let(:extra_fields) { create(:awesome_proposal_extra_fields, proposal: create(:extended_proposal)) } let(:proposal) { create(:extended_proposal) } + let(:component) { create(:component, settings: { awesome_voting_manifest: "default" }) } it { is_expected.to be_valid } @@ -183,44 +184,10 @@ module Decidim::DecidimAwesome end end - describe "all_vote_weights" do - let!(:extra_fields) { create(:awesome_proposal_extra_fields, proposal: proposal) } - let!(:another_extra_fields) { create(:awesome_proposal_extra_fields, proposal: another_proposal) } - let!(:unrelated_another_extra_fields) { create(:awesome_proposal_extra_fields, :with_votes, proposal: create(:extended_proposal)) } - let(:another_proposal) { create(:proposal, component: proposal.component) } - let!(:votes) do - vote = create(:proposal_vote, proposal: proposal, author: create(:user, organization: proposal.organization)) - create(:awesome_vote_weight, vote: vote, weight: 1) - end - let!(:other_votes) do - vote = create(:proposal_vote, proposal: another_proposal, author: create(:user, organization: proposal.organization)) - create(:awesome_vote_weight, vote: vote, weight: 2) - end - - it "returns all vote weights for a component" do - expect(proposal.reload.all_vote_weights).to contain_exactly(1, 2) - expect(another_proposal.reload.all_vote_weights).to contain_exactly(1, 2) - expect(proposal.vote_weights).to eq({ "1" => 1, "2" => 0 }) - expect(another_proposal.vote_weights).to eq({ "1" => 0, "2" => 1 }) - end - - context "when wrong cache exists" do - before do - # rubocop:disable Rails/SkipsModelValidations: - # we don't want to trigger the active record hooks - extra_fields.update_columns(vote_weight_totals: { "3" => 1, "4" => 1 }) - # rubocop:enable Rails/SkipsModelValidations: - end - - it "returns all vote weights for a component" do - expect(proposal.reload.extra_fields.vote_weight_totals).to eq({ "3" => 1, "4" => 1 }) - expect(proposal.vote_weights).to eq({ "1" => 0, "2" => 0 }) - proposal.update_vote_weights! - expect(proposal.vote_weights).to eq({ "1" => 1, "2" => 0 }) - expect(another_proposal.reload.vote_weights).to eq({ "1" => 0, "2" => 1 }) - expect(proposal.extra_fields.vote_weight_totals).to eq({ "1" => 1 }) - expect(another_proposal.extra_fields.vote_weight_totals).to eq({ "2" => 1 }) - end + describe "weighted_proposal_voting_enabled" do + it "is disabled by default" do + default_value = Rails.application.secrets.dig(:decidim, :decidim_awesome, :weighted_proposal_voting_enabled) + expect(default_value).to eq("disabled") end end diff --git a/spec/shared/manage_processes_examples.rb b/spec/shared/manage_processes_examples.rb new file mode 100644 index 0000000000..b72c16f0a4 --- /dev/null +++ b/spec/shared/manage_processes_examples.rb @@ -0,0 +1,194 @@ +# frozen_string_literal: true + +shared_examples "manage processes examples" do + context "when viewing the processes list" do + let!(:process_group) { create(:participatory_process_group, organization: organization) } + let!(:process_with_group) { create(:participatory_process, organization: organization, participatory_process_group: process_group) } + let!(:process_without_group) { create(:participatory_process, organization: organization) } + let(:model_name) { participatory_process.class.model_name } + let(:resource_controller) { Decidim::ParticipatoryProcesses::Admin::ParticipatoryProcessesController } + + def filter_by_group(group_title) + visit current_path + within(".card-title") do + click_button("Process Groups") + click_link(group_title) + end + end + + it "allows the user to filter processes by process_group" do + filter_by_group(translated(process_group.title)) + + expect(page).to have_content(translated(process_with_group.title)) + expect(page).not_to have_content(translated(process_without_group.title)) + end + + describe "listing processes" do + it_behaves_like "filtering collection by published/unpublished" + it_behaves_like "filtering collection by private/public" + end + + context "when processes are filtered by process_group" do + before { filter_by_group(translated(process_group.title)) } + + it "allows the user to edit the process_group" do + click_link translated(process_group.title) + + expect(page).to have_content("Edit process group") + end + + describe "listing processes filtered by group" do + it_behaves_like "filtering collection by published/unpublished" do + let!(:published_space) { process_with_group } + let!(:unpublished_space) { create(:participatory_process, :unpublished, organization: organization, participatory_process_group: process_group) } + end + + it_behaves_like "filtering collection by private/public" do + let!(:public_space) { process_with_group } + let!(:private_space) { create(:participatory_process, :private, organization: organization, participatory_process_group: process_group) } + end + end + end + end + + context "when previewing processes" do + context "when the process is unpublished" do + let!(:participatory_process) { create(:participatory_process, :unpublished, organization: organization) } + + it "allows the user to preview the unpublished process" do + within find("tr", text: translated(participatory_process.title)) do + click_link "Preview" + end + + expect(page).to have_css(".process-header") + expect(page).to have_content(translated(participatory_process.title)) + end + end + + context "when the process is published" do + let!(:participatory_process) { create(:participatory_process, organization: organization) } + + it "allows the user to preview the published process" do + within find("tr", text: translated(participatory_process.title)) do + click_link "Preview" + end + + expect(page).to have_current_path decidim_participatory_processes.participatory_process_path(participatory_process) + expect(page).to have_content(translated(participatory_process.title)) + end + end + end + + context "when viewing a missing process" do + it_behaves_like "a 404 page" do + let(:target_path) { decidim_admin_participatory_processes.participatory_process_path(99_999_999) } + end + end + + context "when updating a participatory process" do + let(:image3_filename) { "city3.jpeg" } + let(:image3_path) { Decidim::Dev.asset(image3_filename) } + + before do + click_link translated(participatory_process.title) + end + + it "updates a participatory_process" do + fill_in_i18n( + :participatory_process_title, + "#participatory_process-title-tabs", + en: "My new title", + es: "Mi nuevo título", + ca: "El meu nou títol" + ) + dynamically_attach_file(:participatory_process_banner_image, image3_path, remove_before: true) + + page.execute_script("$('#participatory_process_end_date').focus()") + page.find(".datepicker-dropdown .day", text: "22").click + + within ".edit_participatory_process" do + find("*[type=submit]").click + end + + expect(page).to have_admin_callout("successfully") + + within ".container" do + expect(page).to have_selector("input[value='My new title']") + expect(page).to have_css("img[src*='#{image3_filename}']") + end + end + end + + context "when publishing a process" do + let!(:participatory_process) { create(:participatory_process, :unpublished, organization: organization) } + + before do + click_link translated(participatory_process.title) + end + + it "publishes the process" do + click_link "Publish" + expect(page).to have_content("successfully published") + expect(page).to have_content("Unpublish") + expect(page).to have_current_path decidim_admin_participatory_processes.edit_participatory_process_path(participatory_process) + + participatory_process.reload + expect(participatory_process).to be_published + end + end + + context "when unpublishing a process" do + let!(:participatory_process) { create(:participatory_process, organization: organization) } + + before do + click_link translated(participatory_process.title) + end + + it "unpublishes the process" do + click_link "Unpublish" + expect(page).to have_content("successfully unpublished") + expect(page).to have_content("Publish") + expect(page).to have_current_path decidim_admin_participatory_processes.edit_participatory_process_path(participatory_process) + + participatory_process.reload + expect(participatory_process).not_to be_published + end + end + + context "when there are multiple organizations in the system" do + let!(:external_participatory_process) { create(:participatory_process) } + + before do + visit decidim_admin_participatory_processes.participatory_processes_path + end + + it "doesn't let the admin manage processes form other organizations" do + within "table" do + expect(page).to have_no_content(external_participatory_process.title["en"]) + end + end + end + + context "when the process has a scope" do + let(:scope) { create(:scope, organization: organization) } + + before do + participatory_process.update!(scopes_enabled: true, scope: scope) + end + + it "disables the scope for a participatory process" do + click_link translated(participatory_process.title) + + uncheck :participatory_process_scopes_enabled + + expect(page).to have_selector("#participatory_process_scope_id.disabled") + expect(page).to have_selector("#participatory_process_scope_id .picker-values div input[disabled]", visible: :all) + + within ".edit_participatory_process" do + find("*[type=submit]").click + end + + expect(page).to have_admin_callout("successfully") + end + end +end diff --git a/spec/shared/participatory_process_administration_by_admin_shared_context.rb b/spec/shared/participatory_process_administration_by_admin_shared_context.rb new file mode 100644 index 0000000000..014fd52a16 --- /dev/null +++ b/spec/shared/participatory_process_administration_by_admin_shared_context.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +shared_context "when admin administrating a participatory process" do + let!(:user) do + create(:user, :admin, :confirmed, organization: organization) + end + include_context "when administrating a participatory process" +end diff --git a/spec/shared/participatory_process_administration_shared_context.rb b/spec/shared/participatory_process_administration_shared_context.rb new file mode 100644 index 0000000000..ff0cc89db9 --- /dev/null +++ b/spec/shared/participatory_process_administration_shared_context.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +shared_context "when administrating a participatory process" do + let(:organization) { create(:organization) } + let!(:participatory_process) { create(:participatory_process, organization: organization) } +end diff --git a/spec/system/admin/admin_manages_participatory_processes_spec.rb b/spec/system/admin/admin_manages_participatory_processes_spec.rb new file mode 100644 index 0000000000..ec97e30dbb --- /dev/null +++ b/spec/system/admin/admin_manages_participatory_processes_spec.rb @@ -0,0 +1,560 @@ +# frozen_string_literal: true + +require "spec_helper" +require "decidim/core/test/shared_examples/has_contextual_help" + +describe "Participatory Processes", type: :system do + let(:organization) { create(:organization) } + let(:show_metrics) { true } + let(:show_statistics) { true } + let(:hashtag) { true } + let(:base_description) { { en: "Description", ca: "Descripció", es: "Descripción" } } + let(:short_description) { { en: "Short description", ca: "Descripció curta", es: "Descripción corta" } } + let(:base_process) do + create( + :participatory_process, + :active, + organization: organization, + description: base_description, + short_description: short_description, + show_metrics: show_metrics, + show_statistics: show_statistics + ) + end + + before do + switch_to_host(organization.host) + end + + context "when there are no processes and directly accessing form URL" do + it_behaves_like "a 404 page" do + let(:target_path) { decidim_participatory_processes.participatory_processes_path } + end + end + + context "when there are no processes and accessing from the homepage" do + it "does not show the menu link" do + visit decidim.root_path + + within ".main-nav" do + expect(page).to have_no_content("Processes") + end + end + end + + context "when the process does not exist" do + it_behaves_like "a 404 page" do + let(:target_path) { decidim_participatory_processes.participatory_process_path(99_999_999) } + end + end + + context "when there are some processes and all are unpublished" do + before do + create(:participatory_process, :unpublished, organization: organization) + create(:participatory_process, :published) + end + + context "and directly accessing from URL" do + it_behaves_like "a 404 page" do + let(:target_path) { decidim_participatory_processes.participatory_processes_path } + end + end + + context "and accessing from the homepage" do + it "the menu link is not shown" do + visit decidim.root_path + + within ".main-nav" do + expect(page).to have_no_content("Processes") + end + end + end + end + + context "when there are some published processes" do + let!(:participatory_process) { base_process } + let!(:promoted_process) { create(:participatory_process, :promoted, organization: organization) } + let!(:unpublished_process) { create(:participatory_process, :unpublished, organization: organization) } + let!(:past_process) { create :participatory_process, :past, organization: organization } + let!(:upcoming_process) { create :participatory_process, :upcoming, organization: organization } + let!(:grouped_process) { create :participatory_process, organization: organization } + let!(:group) { create :participatory_process_group, participatory_processes: [grouped_process], organization: organization } + + it_behaves_like "shows contextual help" do + let(:index_path) { decidim_participatory_processes.participatory_processes_path } + let(:manifest_name) { :participatory_processes } + end + + it_behaves_like "editable content for admins" do + let(:target_path) { decidim_participatory_processes.participatory_processes_path } + end + + context "when requesting the processes path" do + before do + visit decidim_participatory_processes.participatory_processes_path + end + + it_behaves_like "accessible page" + + context "when emitter is defined", :slow do + context "when no emitter" do + it "doesn't displays logo or text" do + within "#participatory_process_#{promoted_process.id}" do + expect(page).not_to have_css(".emitter-header") + end + end + end + + context "when emitter" do + let(:base_process) do + create( + :participatory_process, + :active, + :with_emitter, + organization: organization, + description: { en: "Description", ca: "Descripció", es: "Descripción" }, + short_description: { en: "Short description", ca: "Descripció curta", es: "Descripción corta" }, + show_metrics: show_metrics, + show_statistics: show_statistics, + developer_group: { en: "Developer group" } + ) + end + + it "displays logo and text" do + within "#participatory_process_#{base_process.id}" do + within ".emitter-header" do + expect(page).to have_css("img", count: 1) + expect(page).to have_content("Consultation published by Developer group") + end + end + end + end + end + + context "and accessing from the homepage" do + it "the menu link is not shown" do + visit decidim.root_path + + within ".main-nav" do + expect(page).to have_content("Processes") + click_link "Processes" + end + + expect(page).to have_current_path decidim_participatory_processes.participatory_processes_path + end + end + + context "with highlighted processes" do + before do + promoted_process.title["en"] = "D'Artagnan #{promoted_process.title["en"]}" + promoted_process.save! + visit decidim_participatory_processes.participatory_processes_path + end + + it_behaves_like "accessible page" + + context "when emitter is defined", :slow do + context "when no emitter" do + it "doesn't displays logo or text" do + within "#participatory_process_#{promoted_process.id}" do + expect(page).not_to have_css(".emitter-header") + end + end + end + + context "when emitter" do + let(:base_process) do + create( + :participatory_process, + :active, + :with_emitter, + organization: organization, + description: { en: "Description", ca: "Descripció", es: "Descripción" }, + short_description: { en: "Short description", ca: "Descripció curta", es: "Descripción corta" }, + show_metrics: show_metrics, + show_statistics: show_statistics, + developer_group: { en: "Developer group" } + ) + end + + it "displays logo and text" do + within "#participatory_process_#{base_process.id}" do + within ".emitter-header" do + expect(page).to have_css("img", count: 1) + expect(page).to have_content("Consultation published by Developer group") + end + end + end + end + + it "lists all the highlighted processes" do + within "#highlighted-processes" do + expect(page).to have_content(translated(promoted_process.title, locale: :en)) + expect(page).to have_selector(".card--full", count: 1) + end + end + end + + it "lists the active processes" do + within "#processes-grid" do + within "#processes-grid h3" do + expect(page).to have_content("3 ACTIVE PROCESSES") + end + + expect(page).to have_content(translated(participatory_process.title, locale: :en)) + expect(page).to have_content(translated(promoted_process.title, locale: :en)) + expect(page).to have_content(translated(group.title, locale: :en)) + expect(page).to have_selector(".card", count: 3) + + expect(page).to have_no_content(translated(unpublished_process.title, locale: :en)) + expect(page).to have_no_content(translated(past_process.title, locale: :en)) + expect(page).to have_no_content(translated(upcoming_process.title, locale: :en)) + expect(page).to have_no_content(translated(grouped_process.title, locale: :en)) + end + end + + it "links to the individual process page" do + first(".card__link", text: translated(participatory_process.title, locale: :en)).click + + expect(page).to have_current_path decidim_participatory_processes.participatory_process_path(participatory_process) + end + + context "with active steps" do + let!(:step) { create(:participatory_process_step, participatory_process: participatory_process) } + let!(:active_step) do + create(:participatory_process_step, + :active, + participatory_process: participatory_process, + title: { en: "Active step", ca: "Fase activa", es: "Fase activa" }) + end + + it "links to the active step" do + visit decidim_participatory_processes.participatory_processes_path + + within find("#processes-grid .column", text: translated(participatory_process.title)) do + within ".card__footer" do + expect(page).to have_content("Current phase:\nActive step") + end + end + end + + context "when the active step has CTA text and url set" do + let(:cta_path) { "my_path" } + let(:cta_text) { { en: "Take action!", ca: "Take action!", es: "Take action!" } } + + before do + active_step.update!(cta_path: cta_path, cta_text: cta_text) + end + + it "shows a CTA button" do + visit decidim_participatory_processes.participatory_processes_path + + within "#participatory_process_#{participatory_process.id}" do + expect(page).to have_link("Take action!") + end + end + + context "when cta_text is empty in current locale" do + let(:cta_text) { { en: "", ca: "Take action!", es: "Take action!" } } + + it "displays the regular cta button" do + visit decidim_participatory_processes.participatory_processes_path + + within "#participatory_process_#{participatory_process.id}" do + expect(page).not_to have_link("Take action!") + expect(page).to have_link("More info") + end + end + end + + context "when process is promoted" do + let(:cta_text) { { en: "Take promoted action!", ca: "Take promoted action!", es: "Take promoted action!" } } + let!(:active_step) do + create(:participatory_process_step, + :active, + participatory_process: promoted_process, + title: { en: "Active step", ca: "Fase activa", es: "Fase activa" }) + end + + it "shows a CTA button" do + visit decidim_participatory_processes.participatory_processes_path + + within "#highlighted-processes" do + expect(page).to have_link("Take promoted action!") + end + end + end + + context "when user switch locale" do + before do + visit decidim_participatory_processes.participatory_processes_path + within_language_menu do + click_link "Català" + end + end + + it "displays the regular cta button" do + within "#participatory_process_#{participatory_process.id}" do + expect(page).to have_link("Take action!", href: "/processes/#{participatory_process.slug}/my_path") + end + end + end + end + end + + context "when there are promoted participatory process groups" do + let!(:promoted_group) { create(:participatory_process_group, :promoted, :with_participatory_processes, organization: organization) } + let(:promoted_items_titles) { page.all("#highlighted-processes .card__title").map(&:text) } + + before do + promoted_group.title["en"] = "D'Artagnan #{promoted_group.title["en"]}" + promoted_group.save! + visit decidim_participatory_processes.participatory_processes_path + end + + it "shows a highligted processes section" do + expect(page).to have_content("HIGHLIGHTED PROCESSES") + end + + it "lists only promoted groups" do + expect(promoted_items_titles).to include(translated(promoted_group.title, locale: :en)) + expect(promoted_items_titles).not_to include(translated(group.title, locale: :en)) + end + + it "lists all the highlighted process groups" do + within "#highlighted-processes" do + expect(page).to have_content(translated(promoted_group.title, locale: :en)) + expect(page).to have_selector(".card--full", count: 2) + end + end + + context "and promoted group has defined a CTA content block" do + let(:cta_settings) do + { + button_url: "https://example.org/action", + button_text_en: "cta text", + description_en: "cta description" + } + end + + before do + create( + :content_block, + organization: organization, + scope_name: :participatory_process_group_homepage, + scoped_resource_id: promoted_group.id, + manifest_name: :cta, + settings: cta_settings + ) + visit decidim_participatory_processes.participatory_processes_path + end + + it "shows a CTA button inside group card" do + within("#highlighted-processes") do + expect(page).to have_link(cta_settings[:button_text_en], href: cta_settings[:button_url]) + end + end + + context "and promoted group belongs to another organization" do + let!(:promoted_group) { create(:participatory_process_group, :promoted, :with_participatory_processes) } + + it "shows a CTA button inside group card" do + within("#highlighted-processes") do + expect(page).not_to have_link(cta_settings[:button_text_en], href: cta_settings[:button_url]) + end + end + end + end + end + end + end + + context "when going to the participatory process page" do + let!(:participatory_process) { base_process } + let!(:proposals_component) { create(:extended_proposal_component, :published, participatory_space: participatory_process, manifest_name: :proposals) } + let!(:meetings_component) { create(:component, :unpublished, participatory_space: participatory_process, manifest_name: :meetings) } + + before do + create_list(:extended_proposal, 3, component: proposals_component) + allow(Decidim).to receive(:component_manifests).and_return([proposals_component.manifest, meetings_component.manifest]) + end + + it_behaves_like "editable content for admins" do + let(:target_path) { decidim_participatory_processes.participatory_process_path(participatory_process) } + end + + context "when requesting the participatory process path" do + before do + visit decidim_participatory_processes.participatory_process_path(participatory_process) + end + + context "when requesting the process path" do + it "shows the details of the given process" do + within "main" do + expect(page).to have_content(translated(participatory_process.title, locale: :en)) + expect(page).to have_content(translated(participatory_process.subtitle, locale: :en)) + expect(page).to have_content(translated(participatory_process.description, locale: :en)) + expect(page).to have_content(translated(participatory_process.short_description, locale: :en)) + expect(page).to have_content(translated(participatory_process.meta_scope, locale: :en)) + expect(page).to have_content(translated(participatory_process.developer_group, locale: :en)) + expect(page).to have_content(translated(participatory_process.local_area, locale: :en)) + expect(page).to have_content(translated(participatory_process.target, locale: :en)) + expect(page).to have_content(translated(participatory_process.participatory_scope, locale: :en)) + expect(page).to have_content(translated(participatory_process.participatory_structure, locale: :en)) + expect(page).to have_content(I18n.l(participatory_process.end_date, format: :long)) + expect(page).to have_content(participatory_process.hashtag) + end + end + + it_behaves_like "has attachments" do + let(:attached_to) { participatory_process } + end + + it_behaves_like "has attachment collections" do + let(:attached_to) { participatory_process } + let(:collection_for) { participatory_process } + end + + context "and it belongs to a group" do + let!(:group) { create :participatory_process_group, participatory_processes: [participatory_process], organization: organization } + + it "has a link to the group the process belongs to" do + visit decidim_participatory_processes.participatory_process_path(participatory_process) + + expect(page).to have_link(translated(group.title, locale: :en), href: decidim_participatory_processes.participatory_process_group_path(group)) + end + end + + context "when it has some linked processes" do + let(:published_process) { create :participatory_process, :published, organization: organization } + let(:unpublished_process) { create :participatory_process, :unpublished, organization: organization } + + it "only shows the published linked processes" do + participatory_process + .link_participatory_space_resources( + [published_process, unpublished_process], + "related_processes" + ) + visit decidim_participatory_processes.participatory_process_path(participatory_process) + expect(page).to have_content(translated(published_process.title)) + expect(page).to have_no_content(translated(unpublished_process.title)) + end + end + + context "and the process has some components" do + it "shows the components" do + within ".process-nav" do + expect(page).to have_content(translated(proposals_component.name, locale: :en).upcase) + expect(page).to have_no_content(translated(meetings_component.name, locale: :en).upcase) + end + end + + context "and the process metrics are enabled" do + let(:organization) { create(:organization) } + let(:metrics) do + Decidim.metrics_registry.filtered(highlight: true, scope: "participatory_process").each do |metric_registry| + create(:metric, metric_type: metric_registry.metric_name, day: Time.zone.today - 1.week, organization: organization, participatory_space_type: Decidim::ParticipatoryProcess.name, participatory_space_id: participatory_process.id, cumulative: 5, quantity: 2) + end + end + + before do + metrics + visit current_path + end + + it "shows the metrics charts" do + expect(page).to have_css("h3.section-heading", text: "METRICS") + + within "#metrics" do + expect(page).to have_css("input#metrics-space_type[value='Decidim::ParticipatoryProcess']", visible: :hidden) + expect(page).to have_css("input#metrics-space_id[value='#{participatory_process.id}']", visible: :hidden) + Decidim.metrics_registry.filtered(highlight: true, scope: "participatory_process").each do |metric_registry| + expect(page).to have_css(%(##{metric_registry.metric_name}_chart)) + end + end + end + + it "renders a link to all metrics" do + within "#metrics" do + expect(page).to have_link("Show all metrics") + end + end + + it "click link" do + click_link("Show all metrics") + have_current_path(decidim_participatory_processes.all_metrics_participatory_process_path(participatory_process)) + end + end + + context "and the process statistics are enabled" do + let(:show_statistics) { true } + + it "the stats for those components are visible" do + within ".section-statistics" do + expect(page).to have_css("h3.section-heading", text: "STATISTICS") + expect(page).to have_css(".statistic__title", text: "PROPOSALS") + expect(page).to have_css(".statistic__number", text: "3") + expect(page).to have_no_css(".statistic__title", text: "MEETINGS") + expect(page).to have_no_css(".statistic__number", text: "0") + end + end + end + + context "and the process statistics are not enabled" do + let(:show_statistics) { false } + + it "the stats for those components are not visible" do + expect(page).to have_no_css("h3.section-heading", text: "STATISTICS") + expect(page).to have_no_css(".statistic__title", text: "PROPOSALS") + expect(page).to have_no_css(".statistic__number", text: "3") + end + end + + context "and the process metrics are not enabled" do + let(:show_metrics) { false } + + it "the metrics for the participatory processes are not rendered" do + expect(page).to have_no_css("h4.section-heading", text: "METRICS") + end + + it "has no link to all metrics" do + expect(page).to have_no_link("Show all metrics") + end + end + + context "and the process doesn't have hashtag" do + let(:hashtag) { false } + + it "the hashtags for those components are not visible" do + expect(page).to have_no_content("#") + end + end + end + + context "when assemblies are linked to participatory process" do + let!(:published_assembly) { create(:assembly, :published, organization: organization) } + let!(:unpublished_assembly) { create(:assembly, :unpublished, organization: organization) } + let!(:private_assembly) { create(:assembly, :published, :private, :opaque, organization: organization) } + let!(:transparent_assembly) { create(:assembly, :published, :private, :transparent, organization: organization) } + + before do + published_assembly.link_participatory_space_resources(participatory_process, "included_participatory_processes") + unpublished_assembly.link_participatory_space_resources(participatory_process, "included_participatory_processes") + private_assembly.link_participatory_space_resources(participatory_process, "included_participatory_processes") + transparent_assembly.link_participatory_space_resources(participatory_process, "included_participatory_processes") + visit decidim_participatory_processes.participatory_process_path(participatory_process) + end + + it "display related assemblies" do + expect(page).to have_content("RELATED ASSEMBLIES") + expect(page).to have_content(translated(published_assembly.title)) + expect(page).to have_content(translated(transparent_assembly.title)) + expect(page).to have_no_content(translated(unpublished_assembly.title)) + expect(page).to have_no_content(translated(private_assembly.title)) + end + end + end + end + end + end +end diff --git a/spec/system/admin_manages_organization_scopes_spec.rb b/spec/system/admin_manages_organization_scopes_spec.rb new file mode 100644 index 0000000000..c1e28bbc31 --- /dev/null +++ b/spec/system/admin_manages_organization_scopes_spec.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Organization scopes", type: :system do + include Decidim::SanitizeHelper + + let(:organization) { create :organization, default_locale: :en, available_locales: [:en, :es, :ca, :fr] } + let(:admin) { create :user, :admin, :confirmed, organization: organization } + let!(:attributes) { attributes_for(:scope) } + + before do + switch_to_host(organization.host) + end + + describe "Managing scopes" do + let!(:scope_type) { create(:scope_type, organization: admin.organization) } + + before do + login_as admin, scope: :user + visit decidim_admin.root_path + click_link "Settings" + click_link "Scopes" + end + + it "can create new scopes" do + click_link "Add" + + within ".new_scope" do + fill_in_i18n :scope_name, "#scope-name-tabs", **attributes[:name].except("machine_translations") + fill_in "Code", with: "MY-DISTRICT" + select scope_type.name["en"], from: :scope_scope_type_id + + find("*[type=submit]").click + end + + expect(page).to have_admin_callout("successfully") + + within ".draggable-list" do + expect(page).to have_content(translated(attributes[:name])) + end + + visit decidim_admin.root_path + expect(page).to have_content("created the #{translated(attributes[:name])} scope") + end + + context "with existing scopes" do + let!(:scope) { create(:scope, organization: organization) } + + before do + visit current_path + end + + it "can edit them" do + within find(".draggable-content", text: translated(scope.name)) do + click_link "Edit" + end + + within ".edit_scope" do + fill_in_i18n :scope_name, "#scope-name-tabs", **attributes[:name].except("machine_translations") + find("*[type=submit]").click + end + + expect(page).to have_admin_callout("successfully") + + within ".draggable-list" do + expect(page).to have_content(translated(attributes[:name])) + end + + visit decidim_admin.root_path + expect(page).to have_content("updated the #{translated(attributes[:name])} scope") + end + + it "can delete them" do + within find(".draggable-content", text: translated(scope.name)) do + accept_confirm { click_link "Destroy" } + end + + expect(page).to have_admin_callout("successfully") + + within ".card-section" do + expect(page).to have_no_content(translated(scope.name)) + end + end + + it "can create a new subcope" do + within find(".draggable-content", text: translated(scope.name)) do + find("a", text: translated(scope.name)).click + end + + click_link "Add" + + within ".new_scope" do + fill_in_i18n :scope_name, "#scope-name-tabs", en: "My nice subdistrict", + es: "Mi lindo subdistrito", + ca: "El meu bonic subbarri" + fill_in "Code", with: "MY-SUBDISTRICT" + select scope_type.name["en"], from: :scope_scope_type_id + + find("*[type=submit]").click + end + + expect(page).to have_admin_callout("successfully") + + within ".draggable-list" do + expect(page).to have_content("My nice subdistrict") + end + end + end + end +end diff --git a/spec/system/participatory_processes_spec.rb b/spec/system/participatory_processes_spec.rb index cacfdff136..f635bb873b 100644 --- a/spec/system/participatory_processes_spec.rb +++ b/spec/system/participatory_processes_spec.rb @@ -94,6 +94,41 @@ it_behaves_like "accessible page" + context "when emitter is defined", :slow do + context "when no emitter" do + it "doesn't displays logo or text" do + within "#participatory_process_#{promoted_process.id}" do + expect(page).not_to have_css(".emitter-header") + end + end + end + + context "when emitter" do + let(:base_process) do + create( + :participatory_process, + :active, + :with_emitter, + organization: organization, + description: { en: "Description", ca: "Descripció", es: "Descripción" }, + short_description: { en: "Short description", ca: "Descripció curta", es: "Descripción corta" }, + show_metrics: show_metrics, + show_statistics: show_statistics, + developer_group: { en: "Developer group" } + ) + end + + it "displays logo and text" do + within "#participatory_process_#{base_process.id}" do + within ".emitter-header" do + expect(page).to have_css("img", count: 1) + expect(page).to have_content("Consultation published by Developer group") + end + end + end + end + end + context "and accessing from the homepage" do it "the menu link is not shown" do visit decidim.root_path @@ -116,6 +151,41 @@ it_behaves_like "accessible page" + context "when emitter is defined", :slow do + context "when no emitter" do + it "doesn't displays logo or text" do + within "#participatory_process_#{promoted_process.id}" do + expect(page).not_to have_css(".emitter-header") + end + end + end + + context "when emitter" do + let(:base_process) do + create( + :participatory_process, + :active, + :with_emitter, + organization: organization, + description: { en: "Description", ca: "Descripció", es: "Descripción" }, + short_description: { en: "Short description", ca: "Descripció curta", es: "Descripción corta" }, + show_metrics: show_metrics, + show_statistics: show_statistics, + developer_group: { en: "Developer group" } + ) + end + + it "displays logo and text" do + within "#participatory_process_#{base_process.id}" do + within ".emitter-header" do + expect(page).to have_css("img", count: 1) + expect(page).to have_content("Consultation published by Developer group") + end + end + end + end + end + it "lists all the highlighted processes" do within "#highlighted-processes" do expect(page).to have_content(translated(promoted_process.title, locale: :en))