diff --git a/.env-example b/.env-example index c77a6034e8..842af62c1f 100644 --- a/.env-example +++ b/.env-example @@ -105,4 +105,7 @@ SMS_GATEWAY_MB_ACCOUNT_ID= # Automatically save AH metadata to user extended data # Format : comma separated list of auhtorization handler names -# AUTO_EXPORT_AUTHORIZATIONS_DATA_TO_USER_DATA_ENABLED_FOR="authorization1,authorization2" \ No newline at end of file +# AUTO_EXPORT_AUTHORIZATIONS_DATA_TO_USER_DATA_ENABLED_FOR="authorization1,authorization2" + +# Sort participatory processes by date +SORT_PROCESSES_BY_DATE=false diff --git a/OVERLOADS.md b/OVERLOADS.md index 95d0041245..7e9b14d431 100644 --- a/OVERLOADS.md +++ b/OVERLOADS.md @@ -2,6 +2,19 @@ * `app/cells/decidim/version_cell.rb` This override the default `VersionCell` from `decidim-core`, by adding sanitization for `version_number` to prevent XSS attacks. +* `app/controllers/decidim/assemblies/assemblies_controller.rb` +This override the default `AssembliesController` from `decidim-assemblies`, by adding custom sort for assembly_participatory_processes + +* `app/helpers/decidim/assemblies/assemblies_helper.rb` +This override the default `AssembliesHelpler` from `decidim-assemblies`, by adding custom html for sorted assembly_participatory_processes + +* `app/controllers/decidim/participatory_processes/participatory_processes_controller.rb` +This override the default `ParticipatoryProcessesController` from `decidim-participatory_processes`, by adding custom sort for participatory_processes + +## Initiative form +* `lib/extends/forms/decidim/initiatives/initiative_form_extends.rb` +This adds a validation to form's description. + ## Proposal's draft (Decidim awesome overrides 0.26.7) * `app/views/decidim/proposals/collaborative_drafts/_edit_form_fields.html.erb` diff --git a/app/controllers/decidim/assemblies/assemblies_controller.rb b/app/controllers/decidim/assemblies/assemblies_controller.rb new file mode 100644 index 0000000000..6fa700f10d --- /dev/null +++ b/app/controllers/decidim/assemblies/assemblies_controller.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +module Decidim + module Assemblies + # A controller that holds the logic to show Assemblies in a public layout. + class AssembliesController < Decidim::Assemblies::ApplicationController + include ParticipatorySpaceContext + participatory_space_layout only: :show + include FilterResource + + helper_method :parent_assemblies, :promoted_assemblies, :stats, :assembly_participatory_processes, :current_assemblies_settings + + def index + enforce_permission_to :list, :assembly + + respond_to do |format| + format.html do + raise ActionController::RoutingError, "Not Found" if published_assemblies.none? + + render "index" + end + + format.js do + raise ActionController::RoutingError, "Not Found" if published_assemblies.none? + + render "index" + end + + format.json do + render json: published_assemblies.query.includes(:children).where(parent: nil).collect { |assembly| + { + name: assembly.title[I18n.locale.to_s], + children: assembly.children.collect do |child| + { + name: child.title[I18n.locale.to_s], + children: child.children.collect { |child_of_child| { name: child_of_child.title[I18n.locale.to_s] } } + } + end + } + } + end + end + end + + def show + enforce_permission_to :read, :assembly, assembly: current_participatory_space + end + + private + + def search_collection + Assembly.where(organization: current_organization).published.visible_for(current_user) + end + + def default_filter_params + { + with_scope: nil, + with_area: nil, + type_id_eq: nil + } + end + + def current_participatory_space + return unless params[:slug] + + @current_participatory_space ||= OrganizationAssemblies.new(current_organization).query.where(slug: params[:slug]).or( + OrganizationAssemblies.new(current_organization).query.where(id: params[:slug]) + ).first! + end + + def published_assemblies + @published_assemblies ||= OrganizationPublishedAssemblies.new(current_organization, current_user) + end + + def promoted_assemblies + @promoted_assemblies ||= published_assemblies | PromotedAssemblies.new + end + + def parent_assemblies + search.result.parent_assemblies.order(weight: :asc, promoted: :desc) + end + + def stats + @stats ||= AssemblyStatsPresenter.new(assembly: current_participatory_space) + end + + def assembly_participatory_processes + if Rails.application.secrets.dig(:decidim, :participatory_processes, :sort_by_date) == false + @assembly_participatory_processes ||= @current_participatory_space.linked_participatory_space_resources(:participatory_processes, "included_participatory_processes") + else + @assembly_participatory_processes = @current_participatory_space.linked_participatory_space_resources(:participatory_processes, "included_participatory_processes") + sorted_by_date = { + active: @assembly_participatory_processes.active_spaces.sort_by(&:end_date), + future: @assembly_participatory_processes.future_spaces.sort_by(&:start_date), + past: @assembly_participatory_processes.past_spaces.sort_by(&:end_date).reverse + } + @assembly_participatory_processes = sorted_by_date + end + end + + def current_assemblies_settings + @current_assemblies_settings ||= Decidim::AssembliesSetting.find_or_create_by(decidim_organization_id: current_organization.id) + end + end + end +end diff --git a/app/controllers/decidim/initiatives/admin/initiatives_controller.rb b/app/controllers/decidim/initiatives/admin/initiatives_controller.rb index 06fbfba3e2..2f4a2a6ec8 100644 --- a/app/controllers/decidim/initiatives/admin/initiatives_controller.rb +++ b/app/controllers/decidim/initiatives/admin/initiatives_controller.rb @@ -34,6 +34,8 @@ def edit initiative: current_initiative ) @form.attachment = form_attachment_model + # "sanitize" the translated description, if the value is a hash (for machine_translation key) we don't modify it + @form.description.transform_values! { |v| v.instance_of?(String) ? v.gsub(/on\w+=("|')/, "nothing") : v } render layout: "decidim/admin/initiative" end diff --git a/app/controllers/decidim/participatory_processes/participatory_processes_controller.rb b/app/controllers/decidim/participatory_processes/participatory_processes_controller.rb new file mode 100644 index 0000000000..e1d755b7a3 --- /dev/null +++ b/app/controllers/decidim/participatory_processes/participatory_processes_controller.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +module Decidim + module ParticipatoryProcesses + # A controller that holds the logic to show ParticipatoryProcesses in a + # public layout. + class ParticipatoryProcessesController < Decidim::ParticipatoryProcesses::ApplicationController + include ParticipatorySpaceContext + participatory_space_layout only: [:show, :all_metrics] + include FilterResource + + helper_method :collection, + :promoted_collection, + :participatory_processes, + :stats, + :metrics, + :participatory_process_group, + :default_date_filter, + :related_processes, + :linked_assemblies + + def index + raise ActionController::RoutingError, "Not Found" if published_processes.none? + + enforce_permission_to :list, :process + enforce_permission_to :list, :process_group + end + + def show + enforce_permission_to :read, :process, process: current_participatory_space + end + + def all_metrics + if current_participatory_space.show_statistics + enforce_permission_to :read, :process, process: current_participatory_space + else + render status: :not_found + end + end + + private + + def search_collection + ParticipatoryProcess.where(organization: current_organization).published.visible_for(current_user).includes(:area) + end + + def default_filter_params + { + with_scope: nil, + with_area: nil, + with_type: nil, + with_date: default_date_filter + } + end + + def organization_participatory_processes + @organization_participatory_processes ||= OrganizationParticipatoryProcesses.new(current_organization).query + end + + def current_participatory_space + return unless params["slug"] + + @current_participatory_space ||= organization_participatory_processes.where(slug: params["slug"]).or( + organization_participatory_processes.where(id: params["slug"]) + ).first! + end + + def published_processes + @published_processes ||= OrganizationPublishedParticipatoryProcesses.new(current_organization, current_user) + end + + def promoted_participatory_processes + @promoted_participatory_processes ||= published_processes | PromotedParticipatoryProcesses.new + end + + def promoted_participatory_process_groups + @promoted_participatory_process_groups ||= OrganizationPromotedParticipatoryProcessGroups.new(current_organization) + end + + def promoted_collection + @promoted_collection ||= promoted_participatory_processes.query + promoted_participatory_process_groups.query + end + + def collection + @collection ||= participatory_processes + participatory_process_groups + end + + def filtered_processes + search.result + end + + def participatory_processes + @participatory_processes ||= filtered_processes.groupless.includes(attachments: :file_attachment) + return @participatory_processes if Rails.application.secrets.dig(:decidim, :participatory_processes, :sort_by_date) == false + + custom_sort(search.with_date) + end + + def participatory_process_groups + @participatory_process_groups ||= OrganizationParticipatoryProcessGroups.new(current_organization).query + .where(id: filtered_processes.grouped.group_ids) + end + + def stats + @stats ||= ParticipatoryProcessStatsPresenter.new(participatory_process: current_participatory_space) + end + + def metrics + @metrics ||= ParticipatoryProcessMetricChartsPresenter.new(participatory_process: current_participatory_space, view_context: view_context) + end + + def participatory_process_group + @participatory_process_group ||= current_participatory_space.participatory_process_group + end + + def default_date_filter + return "active" if published_processes.any?(&:active?) + return "upcoming" if published_processes.any?(&:upcoming?) + return "past" if published_processes.any?(&:past?) + + "all" + end + + def related_processes + @related_processes ||= + current_participatory_space + .linked_participatory_space_resources(:participatory_processes, "related_processes") + .published + .all + end + + def linked_assemblies + @linked_assemblies ||= current_participatory_space.linked_participatory_space_resources(:assembly, "included_participatory_processes").public_spaces + end + + def custom_sort(date) + case date + when "active" + @participatory_processes.sort_by(&:end_date) + when "past" + @participatory_processes.sort_by(&:end_date).reverse + when "upcoming" + @participatory_processes.sort_by(&:start_date) + when "all" + @participatory_processes = sort_all_processes + else + @participatory_processes + end + end + + def sort_all_processes + actives = @participatory_processes.select(&:active?).sort_by(&:end_date) + pasts = @participatory_processes.select(&:past?).sort_by(&:end_date).reverse + upcomings = @participatory_processes.select(&:upcoming?).sort_by(&:start_date) + (actives + upcomings + pasts) + end + end + end +end diff --git a/app/helpers/decidim/assemblies/assemblies_helper.rb b/app/helpers/decidim/assemblies/assemblies_helper.rb new file mode 100644 index 0000000000..4da2bb6aa9 --- /dev/null +++ b/app/helpers/decidim/assemblies/assemblies_helper.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +module Decidim + module Assemblies + # Helpers related to the Assemblies layout. + module AssembliesHelper + include Decidim::ResourceHelper + include Decidim::AttachmentsHelper + include Decidim::IconHelper + include Decidim::WidgetUrlsHelper + include Decidim::SanitizeHelper + include Decidim::ResourceReferenceHelper + include Decidim::FiltersHelper + include FilterAssembliesHelper + + # Public: Returns the characteristics of an assembly in a readable format like + # "title: close, no public, no transparent and is restricted to the members of the assembly" + def participatory_processes_for_assembly(assembly_participatory_processes) + if Rails.application.secrets.dig(:decidim, :participatory_processes, :sort_by_date) == true + sorted_participatory_processes_for_assembly(assembly_participatory_processes) + else + html = "" + html += %(
Participatory process d'essai
" }, + short_description: { fr: "Participatory process d'essai
" }, + published_at: Time.current, + organization: org, + slug: process[:slug], + weight: process[:weight], + start_date: process[:start_date], + end_date: process[:end_date] + ) +end diff --git a/lib/extends/forms/decidim/initiatives/initiative_form_extends.rb b/lib/extends/forms/decidim/initiatives/initiative_form_extends.rb new file mode 100644 index 0000000000..9918c015a2 --- /dev/null +++ b/lib/extends/forms/decidim/initiatives/initiative_form_extends.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "active_support/concern" + +module NoAdminInitiativeFormExtends + extend ActiveSupport::Concern + + included do + validate :no_javascript_event_in_description + + private + + def no_javascript_event_in_description + errors.add :description, :invalid if description =~ /on\w+=/ + end + end +end + +Decidim::Initiatives::InitiativeForm.include(NoAdminInitiativeFormExtends) diff --git a/spec/controllers/assemblies_controller_spec.rb b/spec/controllers/assemblies_controller_spec.rb new file mode 100644 index 0000000000..f28fd0e2c6 --- /dev/null +++ b/spec/controllers/assemblies_controller_spec.rb @@ -0,0 +1,210 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Assemblies + describe AssembliesController, type: :controller do + routes { Decidim::Assemblies::Engine.routes } + + let(:organization) { create(:organization) } + + let!(:unpublished_assembly) do + create( + :assembly, + :unpublished, + organization: organization + ) + end + + let!(:published) do + create( + :assembly, + :published, + organization: organization + ) + end + + let!(:promoted) do + create( + :assembly, + :published, + :promoted, + organization: organization + ) + end + + before do + request.env["decidim.current_organization"] = organization + end + + describe "published_assemblies" do + context "when there are no published assemblies" do + before do + published.unpublish! + promoted.unpublish! + end + + it "redirects to 404" do + expect { get :index }.to raise_error(ActionController::RoutingError) + end + end + end + + describe "GET assemblies in json format" do + let!(:first_level) { create(:assembly, :published, :with_parent, parent: published, organization: organization) } + let!(:second_level) { create(:assembly, :published, :with_parent, parent: first_level, organization: organization) } + let!(:third_level) { create(:assembly, :published, :with_parent, parent: second_level, organization: organization) } + + let(:parsed_response) { JSON.parse(response.body, symbolize_names: true) } + + it "includes only published assemblies with their children (two levels)" do + get :index, format: :json + expect(parsed_response).to match_array( + [ + { + name: translated(promoted.title), + children: [] + }, + { + name: translated(published.title), + children: [ + { + name: translated(first_level.title), + children: [{ name: translated(second_level.title) }] + } + ] + } + ] + ) + end + end + + describe "promoted_assemblies" do + it "includes only promoted" do + expect(controller.helpers.promoted_assemblies).to contain_exactly(promoted) + end + end + + describe "parent_assemblies" do + let!(:child_assembly) { create(:assembly, parent: published, organization: organization) } + + it "includes only parent assemblies, with promoted listed first" do + expect(controller.helpers.parent_assemblies.first).to eq(promoted) + expect(controller.helpers.parent_assemblies.second).to eq(published) + end + end + + describe "assembly_participatory_processes" do + let!(:organization) { create(:organization) } + let!(:past_processes) do + 5.times.map do |i| + create( + :participatory_process, + :published, + organization: organization, + start_date: Time.zone.now - (i + 10).days, + end_date: Time.zone.now - (i + 5).days + ) + end + end + + let!(:active_processes) do + 5.times.map do |i| + create( + :participatory_process, + :published, + organization: organization, + start_date: Time.zone.now - (i + 5).days, + end_date: Time.zone.now + (i + 5).days + ) + end + end + + let!(:upcoming_processes) do + 5.times.map do |i| + create( + :participatory_process, + :published, + organization: organization, + start_date: Time.zone.now + (i + 5).days, + end_date: Time.zone.now + (i + 10).days + ) + end + end + + let(:participatory_processes) do + past_processes + active_processes + upcoming_processes + end + + before do + published.link_participatory_space_resources(participatory_processes, "included_participatory_processes") + current_participatory_space = published + controller.instance_variable_set(:@current_participatory_space, current_participatory_space) + end + + context "when sort_by_date variable is true" do + before do + allow(Rails.application.secrets).to receive(:dig).with(:decidim, :participatory_processes, :sort_by_date).and_return(true) + end + + it "includes only participatory processes related to the assembly, actives one by end_date then upcoming ones by start_date then past ones by end_date reversed" do + sorted_participatory_processes = { + active: participatory_processes.select(&:active?).sort_by(&:end_date), + future: participatory_processes.select(&:upcoming?).sort_by(&:start_date), + past: participatory_processes.select(&:past?).sort_by(&:end_date).reverse + } + expect(controller.helpers.assembly_participatory_processes).to eq(sorted_participatory_processes) + end + + it "includes only active participatory processes" do + expect(controller.helpers.assembly_participatory_processes[:active].all?(&:active?)).to be true + end + + it "includes only upcoming participatory processes" do + expect(controller.helpers.assembly_participatory_processes[:future].all?(&:upcoming?)).to be true + end + + it "includes only past participatory processes" do + expect(controller.helpers.assembly_participatory_processes[:past].all?(&:past?)).to be true + end + end + + context "when sort_by_date variable is false" do + before do + allow(Rails.application.secrets).to receive(:dig).with(:decidim, :participatory_processes, :sort_by_date).and_return(false) + end + + it "includes only participatory processes related to the assembly, in random order" do + allow(Rails.application.secrets).to receive(:dig).with(:decidim, :participatory_processes, :sort_by_date).and_return(false) + expect(controller.helpers.assembly_participatory_processes).to eq(published.linked_participatory_space_resources(:participatory_processes, "included_participatory_processes")) + end + end + end + + describe "GET show" do + context "when the assembly is unpublished" do + it "redirects to sign in path" do + get :show, params: { slug: unpublished_assembly.slug } + + expect(response).to redirect_to("/users/sign_in") + end + + context "with signed in user" do + let!(:user) { create(:user, :confirmed, organization: organization) } + + before do + sign_in user, scope: :user + end + + it "redirects to root path" do + get :show, params: { slug: unpublished_assembly.slug } + + expect(response).to redirect_to("/") + end + end + end + end + end + end +end diff --git a/spec/controllers/decidim/initiatives/admin/initiatives_controller_spec.rb b/spec/controllers/decidim/initiatives/admin/initiatives_controller_spec.rb new file mode 100644 index 0000000000..f76c91dd4d --- /dev/null +++ b/spec/controllers/decidim/initiatives/admin/initiatives_controller_spec.rb @@ -0,0 +1,632 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Decidim::Initiatives::Admin::InitiativesController do + routes { Decidim::Initiatives::AdminEngine.routes } + render_views + + let(:user) { create(:user, :confirmed, :admin_terms_accepted, organization: organization) } + let(:admin_user) { create(:user, :admin, :confirmed, organization: organization) } + let(:organization) { create(:organization) } + let!(:initiative) { create(:initiative, organization: organization) } + let!(:created_initiative) { create(:initiative, :created, organization: organization) } + + before do + request.env["decidim.current_organization"] = organization + initiative.author.update(admin_terms_accepted_at: Time.current) + initiative.committee_members.approved.first.user.update(admin_terms_accepted_at: Time.current) + created_initiative.author.update(admin_terms_accepted_at: Time.current) + end + + context "when index" do + context "and Users without initiatives" do + before do + sign_in user, scope: :user + end + + it "initiative list is not allowed" do + get :index + expect(flash[:alert]).not_to be_empty + expect(response).to have_http_status(:found) + end + end + + context "and anonymous users do" do + it "initiative list is not allowed" do + get :index + expect(flash[:alert]).not_to be_empty + expect(response).to have_http_status(:found) + end + end + + context "and admin users" do + before do + sign_in admin_user, scope: :user + end + + it "initiative list is allowed" do + get :index + expect(flash[:alert]).to be_nil + expect(response).to have_http_status(:ok) + end + end + + context "and initiative author" do + before do + sign_in initiative.author, scope: :user + end + + it "initiative list is allowed" do + get :index + expect(flash[:alert]).to be_nil + expect(response).to have_http_status(:ok) + end + end + + describe "and promotal committee members" do + before do + sign_in initiative.committee_members.approved.first.user, scope: :user + end + + it "initiative list is allowed" do + get :index + expect(flash[:alert]).to be_nil + expect(response).to have_http_status(:ok) + end + end + end + + context "when edit" do + context "and Users without initiatives" do + before do + sign_in user, scope: :user + end + + it "are not allowed" do + get :edit, params: { slug: initiative.to_param } + expect(flash[:alert]).not_to be_empty + expect(response).to have_http_status(:found) + end + end + + context "and anonymous users" do + it "are not allowed" do + get :edit, params: { slug: initiative.to_param } + expect(flash[:alert]).not_to be_empty + expect(response).to have_http_status(:found) + end + end + + context "and admin users" do + before do + sign_in admin_user, scope: :user + end + + it "are allowed" do + get :edit, params: { slug: initiative.to_param } + expect(flash[:alert]).to be_nil + expect(response).to have_http_status(:ok) + end + + context "and initiative description contains a javascript event" do + let!(:my_initiative) { create(:initiative, organization: organization, description: { en: 'img src="invalid.jpg" onerror="alert();">' }) } + + it "modifies description value" do + get :edit, params: { slug: my_initiative.to_param } + expect(response.body).to include("nothingalert()") + end + end + end + + context "and initiative author" do + before do + sign_in initiative.author, scope: :user + end + + it "are allowed" do + get :edit, params: { slug: initiative.to_param } + expect(flash[:alert]).to be_nil + expect(response).to have_http_status(:ok) + end + end + + context "and promotal committee members" do + before do + sign_in initiative.committee_members.approved.first.user, scope: :user + end + + it "are allowed" do + get :edit, params: { slug: initiative.to_param } + expect(flash[:alert]).to be_nil + expect(response).to have_http_status(:ok) + end + end + end + + context "when update" do + let(:valid_attributes) do + attrs = attributes_for(:initiative, organization: organization) + attrs[:signature_end_date] = I18n.l(attrs[:signature_end_date], format: :decidim_short) + attrs[:signature_start_date] = I18n.l(attrs[:signature_start_date], format: :decidim_short) + attrs + end + + context "and Users without initiatives" do + before do + sign_in user, scope: :user + end + + it "are not allowed" do + put :update, + params: { + slug: initiative.to_param, + initiative: valid_attributes + } + expect(flash[:alert]).not_to be_empty + expect(response).to have_http_status(:found) + end + end + + context "and anonymous users do" do + it "are not allowed" do + put :update, + params: { + slug: initiative.to_param, + initiative: valid_attributes + } + expect(flash[:alert]).not_to be_empty + expect(response).to have_http_status(:found) + end + end + + context "and admin users" do + before do + sign_in admin_user, scope: :user + end + + it "are allowed" do + put :update, + params: { + slug: initiative.to_param, + initiative: valid_attributes + } + expect(flash[:alert]).to be_nil + expect(response).to have_http_status(:found) + end + end + + context "and initiative author" do + context "and initiative published" do + before do + sign_in initiative.author, scope: :user + end + + it "are not allowed" do + put :update, + params: { + slug: initiative.to_param, + initiative: valid_attributes + } + expect(flash[:alert]).not_to be_nil + expect(response).to have_http_status(:found) + end + end + + context "and initiative created" do + let(:initiative) { create(:initiative, :created, organization: organization) } + + before do + sign_in initiative.author, scope: :user + end + + it "are allowed" do + put :update, + params: { + slug: initiative.to_param, + initiative: valid_attributes + } + expect(flash[:alert]).to be_nil + expect(response).to have_http_status(:found) + end + end + end + + context "and promotal committee members" do + context "and initiative published" do + before do + sign_in initiative.committee_members.approved.first.user, scope: :user + end + + it "are not allowed" do + put :update, + params: { + slug: initiative.to_param, + initiative: valid_attributes + } + expect(flash[:alert]).not_to be_nil + expect(response).to have_http_status(:found) + end + end + + context "and initiative created" do + let(:initiative) { create(:initiative, :created, organization: organization) } + + before do + sign_in initiative.committee_members.approved.first.user, scope: :user + end + + it "are allowed" do + put :update, + params: { + slug: initiative.to_param, + initiative: valid_attributes + } + expect(flash[:alert]).to be_nil + expect(response).to have_http_status(:found) + end + end + end + end + + context "when GET send_to_technical_validation" do + context "and Initiative in created state" do + context "and has not enough committee members" do + before do + created_initiative.author.confirm + sign_in created_initiative.author, scope: :user + end + + it "does not pass to technical validation phase" do + created_initiative.type.update(minimum_committee_members: 4) + get :send_to_technical_validation, params: { slug: created_initiative.to_param } + + created_initiative.reload + expect(created_initiative).not_to be_validating + end + + it "does pass to technical validation phase" do + created_initiative.type.update(minimum_committee_members: 3) + get :send_to_technical_validation, params: { slug: created_initiative.to_param } + + created_initiative.reload + expect(created_initiative).to be_validating + end + end + + context "and User is not the owner of the initiative" do + let(:other_user) { create(:user, organization: organization) } + + before do + sign_in other_user, scope: :user + end + + it "Raises an error" do + get :send_to_technical_validation, params: { slug: created_initiative.to_param } + expect(flash[:alert]).not_to be_empty + expect(response).to have_http_status(:found) + end + end + + context "and User is the owner of the initiative. It is in created state" do + before do + created_initiative.author.confirm + sign_in created_initiative.author, scope: :user + end + + it "Passes to technical validation phase" do + get :send_to_technical_validation, params: { slug: created_initiative.to_param } + + created_initiative.reload + expect(created_initiative).to be_validating + end + end + end + + context "and Initiative in discarded state" do + let!(:discarded_initiative) { create(:initiative, :discarded, organization: organization) } + + before do + discarded_initiative.author.update(admin_terms_accepted_at: Time.current) + sign_in discarded_initiative.author, scope: :user + end + + it "Passes to technical validation phase" do + get :send_to_technical_validation, params: { slug: discarded_initiative.to_param } + + discarded_initiative.reload + expect(discarded_initiative).to be_validating + end + end + + context "and Initiative not in created or discarded state (published)" do + before do + sign_in initiative.author, scope: :user + end + + it "Raises an error" do + get :send_to_technical_validation, params: { slug: initiative.to_param } + expect(flash[:alert]).not_to be_empty + expect(response).to have_http_status(:found) + end + end + end + + context "when POST publish" do + let!(:initiative) { create(:initiative, :validating, organization: organization) } + + context "and Initiative owner" do + before do + sign_in initiative.author, scope: :user + end + + it "Raises an error" do + post :publish, params: { slug: initiative.to_param } + expect(flash[:alert]).not_to be_empty + expect(response).to have_http_status(:found) + end + end + + context "and Administrator" do + let!(:admin) { create(:user, :confirmed, :admin, organization: organization) } + + before do + sign_in admin, scope: :user + end + + it "initiative gets published" do + post :publish, params: { slug: initiative.to_param } + expect(response).to have_http_status(:found) + + initiative.reload + expect(initiative).to be_published + expect(initiative.published_at).not_to be_nil + expect(initiative.signature_start_date).not_to be_nil + expect(initiative.signature_end_date).not_to be_nil + end + end + end + + context "when DELETE unpublish" do + context "and Initiative owner" do + before do + sign_in initiative.author, scope: :user + end + + it "Raises an error" do + delete :unpublish, params: { slug: initiative.to_param } + expect(flash[:alert]).not_to be_empty + expect(response).to have_http_status(:found) + end + end + + context "and Administrator" do + let(:admin) { create(:user, :confirmed, :admin, organization: organization) } + + before do + sign_in admin, scope: :user + end + + it "initiative gets unpublished" do + delete :unpublish, params: { slug: initiative.to_param } + expect(response).to have_http_status(:found) + + initiative.reload + expect(initiative).not_to be_published + expect(initiative).to be_discarded + expect(initiative.published_at).to be_nil + end + end + end + + context "when DELETE discard" do + let(:initiative) { create(:initiative, :validating, organization: organization) } + + context "and Initiative owner" do + before do + sign_in initiative.author, scope: :user + end + + it "Raises an error" do + delete :discard, params: { slug: initiative.to_param } + expect(flash[:alert]).not_to be_empty + expect(response).to have_http_status(:found) + end + end + + context "and Administrator" do + let(:admin) { create(:user, :confirmed, :admin, organization: organization) } + + before do + sign_in admin, scope: :user + end + + it "initiative gets discarded" do + delete :discard, params: { slug: initiative.to_param } + expect(response).to have_http_status(:found) + + initiative.reload + expect(initiative).to be_discarded + expect(initiative.published_at).to be_nil + end + end + end + + context "when POST accept" do + let!(:initiative) { create(:initiative, :acceptable, signature_type: "any", organization: organization) } + + context "and Initiative owner" do + before do + sign_in initiative.author, scope: :user + end + + it "Raises an error" do + post :accept, params: { slug: initiative.to_param } + expect(flash[:alert]).not_to be_empty + expect(response).to have_http_status(:found) + end + end + + context "when Administrator" do + let!(:admin) { create(:user, :confirmed, :admin, organization: organization) } + + before do + sign_in admin, scope: :user + end + + it "initiative gets published" do + post :accept, params: { slug: initiative.to_param } + expect(response).to have_http_status(:found) + + initiative.reload + expect(initiative).to be_accepted + end + end + end + + context "when DELETE reject" do + let!(:initiative) { create(:initiative, :rejectable, signature_type: "any", organization: organization) } + + context "and Initiative owner" do + before do + sign_in initiative.author, scope: :user + end + + it "Raises an error" do + delete :reject, params: { slug: initiative.to_param } + expect(flash[:alert]).not_to be_empty + expect(response).to have_http_status(:found) + end + end + + context "when Administrator" do + let!(:admin) { create(:user, :confirmed, :admin, organization: organization) } + + before do + sign_in admin, scope: :user + end + + it "initiative gets rejected" do + delete :reject, params: { slug: initiative.to_param } + expect(response).to have_http_status(:found) + expect(flash[:alert]).to be_nil + + initiative.reload + expect(initiative).to be_rejected + end + end + end + + context "when GET export_votes" do + let(:initiative) { create(:initiative, organization: organization, signature_type: "any") } + + context "and author" do + before do + sign_in initiative.author, scope: :user + end + + it "is not allowed" do + get :export_votes, params: { slug: initiative.to_param, format: :csv } + expect(flash[:alert]).not_to be_empty + expect(response).to have_http_status(:found) + end + end + + context "and promotal committee" do + before do + sign_in initiative.committee_members.approved.first.user, scope: :user + end + + it "is not allowed" do + get :export_votes, params: { slug: initiative.to_param, format: :csv } + expect(flash[:alert]).not_to be_empty + expect(response).to have_http_status(:found) + end + end + + context "and admin user" do + let!(:vote) { create(:initiative_user_vote, initiative: initiative) } + + before do + sign_in admin_user, scope: :user + end + + it "is allowed" do + get :export_votes, params: { slug: initiative.to_param, format: :csv } + expect(flash[:alert]).to be_nil + expect(response).to have_http_status(:ok) + end + end + end + + context "when GET export_pdf_signatures" do + let(:initiative) { create(:initiative, :with_user_extra_fields_collection, organization: organization) } + + context "and author" do + before do + sign_in initiative.author, scope: :user + end + + it "is not allowed" do + get :export_pdf_signatures, params: { slug: initiative.to_param, format: :pdf } + expect(flash[:alert]).not_to be_empty + expect(response).to have_http_status(:found) + end + end + + context "and admin" do + before do + sign_in admin_user, scope: :user + end + + it "is allowed" do + get :export_pdf_signatures, params: { slug: initiative.to_param, format: :pdf } + expect(flash[:alert]).to be_nil + expect(response).to have_http_status(:ok) + end + end + end + + context "when GET export" do + context "and user" do + before do + sign_in user, scope: :user + end + + it "is not allowed" do + expect(Decidim::Initiatives::ExportInitiativesJob).not_to receive(:perform_later).with(user, "CSV", nil) + + get :export, params: { format: :csv } + expect(flash[:alert]).not_to be_empty + expect(response).to have_http_status(:found) + end + end + + context "and admin" do + before do + sign_in admin_user, scope: :user + end + + it "is allowed" do + expect(Decidim::Initiatives::ExportInitiativesJob).to receive(:perform_later).with(admin_user, organization, "csv", nil) + + get :export, params: { format: :csv } + expect(flash[:alert]).to be_nil + expect(response).to have_http_status(:found) + end + + context "when a collection of ids is passed as a parameter" do + let!(:initiatives) { create_list(:initiative, 3, organization: organization) } + let(:collection_ids) { initiatives.map(&:id) } + + it "enqueues the job" do + expect(Decidim::Initiatives::ExportInitiativesJob).to receive(:perform_later).with(admin_user, organization, "csv", collection_ids) + + get :export, params: { format: :csv, collection_ids: collection_ids } + expect(flash[:alert]).to be_nil + expect(response).to have_http_status(:found) + end + end + end + end +end diff --git a/spec/controllers/participatory_processes_controller_spec.rb b/spec/controllers/participatory_processes_controller_spec.rb index b5257c2fc1..52d34d206b 100644 --- a/spec/controllers/participatory_processes_controller_spec.rb +++ b/spec/controllers/participatory_processes_controller_spec.rb @@ -110,6 +110,113 @@ module ParticipatoryProcesses end end + describe "participatory_processes" do + context "when there are active processes" do + let!(:active_processes) do + 5.times.map do |i| + create( + :participatory_process, + :published, + organization: organization, + start_date: Time.zone.now - (i + 5).days, + end_date: Time.zone.now + (i + 5).days + ) + end + end + + context "and sort_by_date is false" do + before do + allow(Rails.application.secrets).to receive(:dig).with(:decidim, :participatory_processes, :sort_by_date).and_return(false) + end + + it "includes active processes without ordering" do + expect(controller.helpers.participatory_processes.to_a).to eq(active_processes) + end + end + + context "and sort_by_date is true" do + before do + allow(Rails.application.secrets).to receive(:dig).with(:decidim, :participatory_processes, :sort_by_date).and_return(true) + end + # search.with_date will default to "active" + + it "orders active processes by end date" do + expect(controller.helpers.participatory_processes).to eq(active_processes.sort_by(&:end_date)) + end + end + end + + context "when there are upcoming processes" do + let!(:upcoming_processes) do + 5.times.map do |i| + create( + :participatory_process, + :published, + organization: organization, + start_date: Time.zone.now + (i + 2).days, + end_date: Time.zone.now + (i + 5).days + ) + end + end + + context "and sort_by_date is false" do + before do + allow(Rails.application.secrets).to receive(:dig).with(:decidim, :participatory_processes, :sort_by_date).and_return(false) + end + + it "includes upcoming processes without ordering" do + expect(controller.helpers.participatory_processes.to_a).to eq(upcoming_processes) + end + end + + context "and sort_by_date is true" do + before do + allow(Rails.application.secrets).to receive(:dig).with(:decidim, :participatory_processes, :sort_by_date).and_return(true) + end + # search.with_date will default to "upcoming" + + it "orders upcoming processes by start_date" do + expect(controller.helpers.participatory_processes).to eq(upcoming_processes.sort_by(&:start_date)) + end + end + end + + context "when there are past processes" do + let!(:past_processes) do + 5.times.map do |i| + create( + :participatory_process, + :published, + organization: organization, + start_date: Time.zone.now - (i + 10).days, + end_date: Time.zone.now - (i + 5).days + ) + end + end + + context "and sort_by_date is false" do + before do + allow(Rails.application.secrets).to receive(:dig).with(:decidim, :participatory_processes, :sort_by_date).and_return(false) + end + + it "includes past processes without ordering" do + expect(controller.helpers.participatory_processes.to_a).to eq(past_processes) + end + end + + context "and sort_by_date is true" do + before do + allow(Rails.application.secrets).to receive(:dig).with(:decidim, :participatory_processes, :sort_by_date).and_return(true) + end + # search.with_date will default to "past" + + it "orders past processes by reverse end_date" do + expect(controller.helpers.participatory_processes).to eq(past_processes.sort_by(&:end_date).reverse) + end + end + end + end + describe "GET show" do context "when the process is unpublished" do it "redirects to sign in path" do diff --git a/spec/forms/initiative_form_spec.rb b/spec/forms/initiative_form_spec.rb new file mode 100644 index 0000000000..cc412c774e --- /dev/null +++ b/spec/forms/initiative_form_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Initiatives + describe InitiativeForm do + subject { described_class.from_params(attributes).with_context(context) } + + let(:organization) { create(:organization) } + let(:initiatives_type) { create(:initiatives_type, organization: organization) } + let(:scope) { create(:initiatives_type_scope, type: initiatives_type) } + let(:attachment_params) { nil } + + let(:title) { ::Faker::Lorem.sentence(word_count: 5) } + let(:my_description) { "" } + let(:attributes) do + { + title: title, + description: my_description, + type_id: initiatives_type.id, + scope_id: scope&.scope&.id, + signature_type: "offline", + attachment: attachment_params + }.merge(custom_signature_end_date).merge(area) + end + let(:custom_signature_end_date) { {} } + let(:area) { {} } + let(:context) do + { + current_organization: organization, + current_component: nil, + initiative_type: initiatives_type + } + end + let(:state) { "validating" } + let(:initiative) { create(:initiative, organization: organization, state: state, scoped_type: scope) } + + context "when everything is OK" do + let(:my_description) { ::Faker::Lorem.sentence(word_count: 25) } + + it { is_expected.to be_valid } + end + + context "when description contains a javascript event" do + let(:my_description) { 'description<img src=\"invalid.jpg\" onerror=\"alert();\">
' } + + it { is_expected.to be_invalid } + end + end + end +end