From d7115a8c5deaa7516fd77115fcd4b66086769e3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Verg=C3=A9s?= Date: Fri, 7 Oct 2022 11:14:22 +0200 Subject: [PATCH 01/18] New feature: customizable etiquette rules (#1) * Add configuration options * erb lint * set validations * fix summary test * fixes specs * fix config merger * bump version. add docs --- CHANGELOG.md | 11 ++ Gemfile.lock | 2 +- README.md | 13 ++ .../decidim_awesome/admin/config_form.rb | 14 ++ ...oposal_wizard_create_step_form_override.rb | 32 +++- .../admin/config_constraints_helpers.rb | 6 +- .../etiquette_validator_override.rb | 41 +++++ .../admin/config/_form_proposals.html.erb | 84 +++++++++- config/locales/en.yml | 32 ++++ examples/custom_validations.png | Bin 0 -> 107367 bytes lib/decidim/decidim_awesome/awesome.rb | 35 ++++ lib/decidim/decidim_awesome/checksums.yml | 2 + lib/decidim/decidim_awesome/config.rb | 12 +- lib/decidim/decidim_awesome/engine.rb | 45 ++++- .../decidim_awesome/test/initializer.rb | 10 +- .../test/shared_examples/summary_examples.rb | 2 + lib/decidim/decidim_awesome/version.rb | 2 +- spec/awesome_summary_spec.rb | 9 + .../admin/config_controller_spec.rb | 12 +- spec/forms/admin/config_form_spec.rb | 136 ++++++++++++++- .../proposal_wizard_create_step_form_spec.rb | 157 ++++++++++++++++++ spec/lib/system_checker_spec.rb | 2 +- spec/system/admin/admin_spec.rb | 32 +++- 23 files changed, 655 insertions(+), 36 deletions(-) create mode 100644 app/validators/concerns/decidim/decidim_awesome/etiquette_validator_override.rb create mode 100644 examples/custom_validations.png diff --git a/CHANGELOG.md b/CHANGELOG.md index c0dd5ce4e..66a11145f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ CHANGELOG ========= +v0.8.4 +------ + +Compatibility: + - Decidim v0.26.x + - Decidim v0.25.x + +Features: + - Feature: Override validation rules for title and body in proposals, with constrains available + - Improve loading process to facilitate development + v0.8.3 ------ diff --git a/Gemfile.lock b/Gemfile.lock index cbd37b769..c37c9f548 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - decidim-decidim_awesome (0.8.3) + decidim-decidim_awesome (0.8.4) decidim-admin (>= 0.25.0, < 0.27) decidim-core (>= 0.25.0, < 0.27) sassc (~> 2.3) diff --git a/README.md b/README.md index 6851642be..55718732e 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,19 @@ Using a link with a query string (ie: `/take-me-somewhere?locale=es`) that will ![Custom redirections screenshot](examples/custom-redirections.png) +#### 14. Custom validation rules for title and body in proposals + +Configure as you wish how the fields "title" and "body" are validated in proposals creation. + +Rules available: + +* Minimum title and body length (defaults to 15 chars). +* Maximum percentage of capital letters for title and body (defaults to 25%). +* Maximum number of "marks" (aka: exclamation and interrogation signs) that can be consective in the title or the body (defaults to 1). +* Enable/disable forcing to start the title or the body with a capital letter (defaults to "enabled"). + +![Custom validations](examples/custom_validations.png) + #### To be continued... We're not done! Please check the [issues](/Platoniq/decidim-module-decidim_awesome/issues) (and participate) to see what's on our mind diff --git a/app/forms/decidim/decidim_awesome/admin/config_form.rb b/app/forms/decidim/decidim_awesome/admin/config_form.rb index c5b962aff..57ed59307 100644 --- a/app/forms/decidim/decidim_awesome/admin/config_form.rb +++ b/app/forms/decidim/decidim_awesome/admin/config_form.rb @@ -22,12 +22,26 @@ class ConfigForm < Decidim::Form attribute :intergram_for_admins_settings, IntergramForm attribute :intergram_for_public, Boolean attribute :intergram_for_public_settings, IntergramForm + attribute :validate_title_min_length, Integer, default: 15 + attribute :validate_title_max_caps_percent, Integer, default: 25 + attribute :validate_title_max_marks_together, Integer, default: 1 + attribute :validate_title_start_with_caps, Boolean, default: true + attribute :validate_body_min_length, Integer, default: 15 + attribute :validate_body_max_caps_percent, Integer, default: 25 + attribute :validate_body_max_marks_together, Integer, default: 1 + attribute :validate_body_start_with_caps, Boolean, default: true # collect all keys anything not specified in the params (UpdateConfig command ignores it) attr_accessor :valid_keys validate :css_syntax, if: ->(form) { form.scoped_styles.present? } validate :json_syntax, if: ->(form) { form.proposal_custom_fields.present? } + validates :validate_title_min_length, presence: true, numericality: { greater_than_or_equal_to: 1, less_than_or_equal_to: 100 } + validates :validate_title_max_caps_percent, presence: true, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 } + validates :validate_title_max_marks_together, presence: true, numericality: { greater_than_or_equal_to: 1 } + validates :validate_body_min_length, presence: true, numericality: { greater_than_or_equal_to: 0 } + validates :validate_body_max_caps_percent, presence: true, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 } + validates :validate_body_max_marks_together, presence: true, numericality: { greater_than_or_equal_to: 1 } # TODO: validate non general admins are here diff --git a/app/forms/decidim/decidim_awesome/proposals/proposal_wizard_create_step_form_override.rb b/app/forms/decidim/decidim_awesome/proposals/proposal_wizard_create_step_form_override.rb index 98b70a627..91bf5113c 100644 --- a/app/forms/decidim/decidim_awesome/proposals/proposal_wizard_create_step_form_override.rb +++ b/app/forms/decidim/decidim_awesome/proposals/proposal_wizard_create_step_form_override.rb @@ -11,11 +11,15 @@ module ProposalWizardCreateStepFormOverride clear_validators! validates :title, presence: true, etiquette: true - validates :title, length: { in: 15..150 } - validates :body, presence: true, etiquette: true, unless: ->(form) { form.override_validations? } + validates :title, proposal_length: { + minimum: ->(form) { form.minimum_title_length }, + maximum: 150 + } + validates :body, presence: true, unless: ->(form) { form.override_validations? || form.minimum_body_length.zero? } + validates :body, etiquette: true, unless: ->(form) { form.override_validations? } validates :body, proposal_length: { - minimum: 15, - maximum: ->(record) { record.override_validations? ? 0 : record.component.settings.proposal_length } + minimum: ->(form) { form.minimum_body_length }, + maximum: ->(form) { form.override_validations? ? 0 : form.component.settings.proposal_length } } validate :body_is_not_bare_template, unless: ->(form) { form.override_validations? } @@ -26,10 +30,24 @@ def override_validations? custom_fields.present? end + def minimum_title_length + awesome_config.config[:validate_title_min_length].to_i + end + + def minimum_body_length + awesome_config.config[:validate_body_min_length].to_i + end + def custom_fields - awesome_config = Decidim::DecidimAwesome::Config.new(context.current_organization) - awesome_config.context_from_component(context.current_component) - awesome_config.collect_sub_configs_values("proposal_custom_field") + @custom_fields ||= awesome_config.collect_sub_configs_values("proposal_custom_field") + end + + def awesome_config + @awesome_config ||= begin + conf = Decidim::DecidimAwesome::Config.new(context.current_organization) + conf.context_from_component(context.current_component) + conf + end end end end diff --git a/app/helpers/decidim/decidim_awesome/admin/config_constraints_helpers.rb b/app/helpers/decidim/decidim_awesome/admin/config_constraints_helpers.rb index b3bda30c4..8b514e9d2 100644 --- a/app/helpers/decidim/decidim_awesome/admin/config_constraints_helpers.rb +++ b/app/helpers/decidim/decidim_awesome/admin/config_constraints_helpers.rb @@ -15,7 +15,11 @@ def check(status) def menus @menus ||= { editors: config_enabled?([:allow_images_in_full_editor, :allow_images_in_small_editor, :use_markdown_editor, :allow_images_in_markdown_editor]), - proposals: config_enabled?(:allow_images_in_proposals), + proposals: config_enabled?([:allow_images_in_proposals, + :validate_title_min_length, :validate_title_max_caps_percent, + :validate_title_max_marks_together, :validate_title_start_with_caps, + :validate_body_min_length, :validate_body_max_caps_percent, + :validate_body_max_marks_together, :validate_body_start_with_caps]), surveys: config_enabled?(:auto_save_forms), styles: config_enabled?(:scoped_styles), proposal_custom_fields: config_enabled?(:proposal_custom_fields), diff --git a/app/validators/concerns/decidim/decidim_awesome/etiquette_validator_override.rb b/app/validators/concerns/decidim/decidim_awesome/etiquette_validator_override.rb new file mode 100644 index 000000000..4e276892e --- /dev/null +++ b/app/validators/concerns/decidim/decidim_awesome/etiquette_validator_override.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Decidim + module DecidimAwesome + module EtiquetteValidatorOverride + extend ActiveSupport::Concern + + included do + private + + def validate_caps(record, attribute, value) + percent = awesome_config(record, "validate_#{attribute}_max_caps_percent").to_f + return if value.scan(/[[:upper:]]/).length < value.length * percent / 100 + + record.errors.add(attribute, options[:message] || I18n.t("too_much_caps", scope: "decidim.decidim_awesome.validators", percent: percent.round)) + end + + def validate_marks(record, attribute, value) + marks = awesome_config(record, "validate_#{attribute}_max_marks_together").to_i + 1 + return if value.scan(/[!?¡¿]{#{marks},}/).empty? + + record.errors.add(attribute, options[:message] || :too_many_marks) + end + + def validate_caps_first(record, attribute, value) + return unless awesome_config(record, "validate_#{attribute}_start_with_caps") + return if value.scan(/\A[[:lower:]]{1}/).empty? + + record.errors.add(attribute, options[:message] || :must_start_with_caps) + end + + def awesome_config(record, var) + config = record.try(:awesome_config)&.config + return unless config.is_a?(Hash) + + config[var.to_sym] + end + end + end + end +end diff --git a/app/views/decidim/decidim_awesome/admin/config/_form_proposals.html.erb b/app/views/decidim/decidim_awesome/admin/config/_form_proposals.html.erb index 61ad49b8f..b563aca1f 100644 --- a/app/views/decidim/decidim_awesome/admin/config/_form_proposals.html.erb +++ b/app/views/decidim/decidim_awesome/admin/config/_form_proposals.html.erb @@ -2,9 +2,89 @@ <% if config_enabled? :allow_images_in_proposals %>

<%= t("rich_text_editor_in_public_views", scope: "decidim.decidim_awesome.admin.config") if current_organization.rich_text_editor_in_public_views %>

- <%= form.check_box :allow_images_in_proposals %> + <%= form.check_box :allow_images_in_proposals, disabled: current_organization.rich_text_editor_in_public_views %>

<%= t("help.allow_images_in_proposals", scope: "decidim.decidim_awesome.admin.config.form") %>

- <%= render(partial: "decidim/decidim_awesome/admin/config/constraints", locals: { key: :allow_images_in_proposals, constraints: constraints_for(:allow_images_in_proposals) }) %> + <% unless current_organization.rich_text_editor_in_public_views %> + <%= render(partial: "decidim/decidim_awesome/admin/config/constraints", locals: { key: :allow_images_in_proposals, constraints: constraints_for(:allow_images_in_proposals) }) %> + <% end %> <% end %> + +<% if config_enabled? %i(validate_title_min_length validate_title_max_caps_percent validate_title_max_marks_together validate_title_start_with_caps) %> + + + +
+
+

<%= t("validators.title", scope: "decidim.decidim_awesome.admin.config.form") %>

+
+
+ +
+ <% if config_enabled? :validate_title_start_with_caps %> + <%= form.check_box :validate_title_start_with_caps %> + + <%= render(partial: "decidim/decidim_awesome/admin/config/constraints", locals: { key: :validate_title_start_with_caps, constraints: constraints_for(:validate_title_start_with_caps) }) %> + <% end %> + + <% if config_enabled? :validate_title_min_length %> + <%= form.number_field :validate_title_min_length %> +

<%= t("help.validate_title_min_length", scope: "decidim.decidim_awesome.admin.config.form") %>

+ + <%= render(partial: "decidim/decidim_awesome/admin/config/constraints", locals: { key: :validate_title_min_length, constraints: constraints_for(:validate_title_min_length) }) %> + <% end %> + + <% if config_enabled? :validate_title_max_caps_percent %> + <%= form.number_field :validate_title_max_caps_percent %> +

<%= t("help.validate_title_max_caps_percent", scope: "decidim.decidim_awesome.admin.config.form") %>

+ + <%= render(partial: "decidim/decidim_awesome/admin/config/constraints", locals: { key: :validate_title_max_caps_percent, constraints: constraints_for(:validate_title_max_caps_percent) }) %> + <% end %> + + <% if config_enabled? :validate_title_max_marks_together %> + <%= form.number_field :validate_title_max_marks_together %> +

<%= t("help.validate_title_max_marks_together", scope: "decidim.decidim_awesome.admin.config.form") %>

+ + <%= render(partial: "decidim/decidim_awesome/admin/config/constraints", locals: { key: :validate_title_max_marks_together, constraints: constraints_for(:validate_title_max_marks_together) }) %> + <% end %> +
+<% end %> + +<% if config_enabled? %i(validate_body_min_length validate_body_max_caps_percent validate_body_max_marks_together validate_body_start_with_caps) %> +
+
+ +
+
+

<%= t("validators.body", scope: "decidim.decidim_awesome.admin.config.form") %>

+
+
+ <% if config_enabled? :validate_body_start_with_caps %> + <%= form.check_box :validate_body_start_with_caps %> + + <%= render(partial: "decidim/decidim_awesome/admin/config/constraints", locals: { key: :validate_body_start_with_caps, constraints: constraints_for(:validate_body_start_with_caps) }) %> + <% end %> + + <% if config_enabled? :validate_body_min_length %> + <%= form.number_field :validate_body_min_length %> +

<%= t("help.validate_body_min_length", scope: "decidim.decidim_awesome.admin.config.form") %>

+ + <%= render(partial: "decidim/decidim_awesome/admin/config/constraints", locals: { key: :validate_body_min_length, constraints: constraints_for(:validate_body_min_length) }) %> + <% end %> + + <% if config_enabled? :validate_body_max_caps_percent %> + <%= form.number_field :validate_body_max_caps_percent %> +

<%= t("help.validate_body_max_caps_percent", scope: "decidim.decidim_awesome.admin.config.form") %>

+ + <%= render(partial: "decidim/decidim_awesome/admin/config/constraints", locals: { key: :validate_body_max_caps_percent, constraints: constraints_for(:validate_body_max_caps_percent) }) %> + <% end %> + + <% if config_enabled? :validate_body_max_marks_together %> + <%= form.number_field :validate_body_max_marks_together %> +

<%= t("help.validate_body_max_marks_together", scope: "decidim.decidim_awesome.admin.config.form") %>

+ + <%= render(partial: "decidim/decidim_awesome/admin/config/constraints", locals: { key: :validate_body_max_marks_together, constraints: constraints_for(:validate_body_max_marks_together) }) %> + <% end %> +
+<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 7dbab1f18..faa6a3d56 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -26,6 +26,18 @@ en: scoped_admins: Scoped admins group %{id} scoped_styles: Custom styles %{id} use_markdown_editor: Use a Markdown editor instead of the HTML editor + validate_body_max_caps_percent: Maximum allowed percentage of capital letters + for the body + validate_body_max_marks_together: Maximum consecutive marks symbols allowed + in the body + validate_body_min_length: Minimum required characters for the body + validate_body_start_with_caps: Force the body to start with a capital letter + validate_title_max_caps_percent: Maximum allowed percentage of capital letters + for the title + validate_title_max_marks_together: Maximum consecutive marks symbols allowed + in the title + validate_title_min_length: Minimum required characters for the title + validate_title_start_with_caps: Force the title to start with a capital letter constraint: component_id: or specifically in component_manifest: Only in components of type @@ -199,6 +211,23 @@ en: use_markdown_editor: This will substitute the Quill WYSIWYG editor, to use a Markdown editor instead. Text will be converted and saved as HTML in the database. + validate_body_max_caps_percent: Zero won't allow any capital letter, + 100 will force to write everything in capital letters + validate_body_max_marks_together: 'Limit the number of question and + exclamation marks that can be written together. Ie: if it is 2, then + ''!!!'' won''t be allowed in the text' + validate_body_min_length: This number can be zero, this will effectively + make this field non-mandatory + validate_title_max_caps_percent: Zero won't allow any capital letter, + 100 will force to write everything in capital letters + validate_title_max_marks_together: 'Limit the number of question and + exclamation marks that can be written together. Ie: if it is 2, then + ''!!!'' won''t be allowed in the text' + validate_title_min_length: Title is always mandatory and this number + cannot be zero + validators: + body: User input validations for the "body" field + title: User input validations for the "title" field form_proposal_custom_fields: new: Add a new "custom fields" box remove: Remove this "custom fields" box @@ -388,6 +417,9 @@ en: show: view_meeting: View meeting view_proposal: View proposal + validators: + too_much_caps: Is using too many capital letters (over %{percent}% of the + text) layouts: decidim: admin: diff --git a/examples/custom_validations.png b/examples/custom_validations.png new file mode 100644 index 0000000000000000000000000000000000000000..c32b3d527e23654d9e3ba0331f2040af102362c4 GIT binary patch literal 107367 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 T&NRpcjv3vtG=zE?&mA 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⪙^;qnb%+CEokh4??}~E69f$#{3^c{b*#^ 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 0.25.0", "< 0.27"].freeze end end diff --git a/spec/awesome_summary_spec.rb b/spec/awesome_summary_spec.rb index 8906801ac..3eaee713f 100644 --- a/spec/awesome_summary_spec.rb +++ b/spec/awesome_summary_spec.rb @@ -35,6 +35,15 @@ let!(:intergram_for_public) { create(:awesome_config, organization: organization, var: :intergram_for_public, value: true) } let!(:config_public_settings) { create(:awesome_config, organization: organization, var: :intergram_for_public_settings, value: intergram) } let!(:config_admins_settings) { create(:awesome_config, organization: organization, var: :intergram_for_admins_settings, value: intergram) } + let!(:validate_title_min_length) { create(:awesome_config, organization: organization, var: :validate_title_min_length, value: 10) } + let!(:validate_title_max_caps_percent) { create(:awesome_config, organization: organization, var: :validate_title_max_caps_percent, value: 10) } + let!(:validate_title_max_marks_together) { create(:awesome_config, organization: organization, var: :validate_title_max_marks_together, value: 10) } + let!(:validate_title_start_with_caps) { create(:awesome_config, organization: organization, var: :validate_title_start_with_caps, value: true) } + let!(:validate_body_min_length) { create(:awesome_config, organization: organization, var: :validate_body_min_length, value: 10) } + let!(:validate_body_max_caps_percent) { create(:awesome_config, organization: organization, var: :validate_body_max_caps_percent, value: 10) } + let!(:validate_body_max_marks_together) { create(:awesome_config, organization: organization, var: :validate_body_max_marks_together, value: 10) } + let!(:validate_body_start_with_caps) { create(:awesome_config, organization: organization, var: :validate_body_start_with_caps, value: true) } + let(:styles) { "body {background: red;}" } let(:intergram) do { chat_id: "some-id" } diff --git a/spec/controllers/admin/config_controller_spec.rb b/spec/controllers/admin/config_controller_spec.rb index 918e3c7af..5f0edc4f4 100644 --- a/spec/controllers/admin/config_controller_spec.rb +++ b/spec/controllers/admin/config_controller_spec.rb @@ -62,7 +62,17 @@ module Admin end context "and proposals is disabled" do - let(:disabled) { editors + [:allow_images_in_proposals] } + let(:disabled) do + editors + [:allow_images_in_proposals, + :validate_title_min_length, + :validate_title_max_caps_percent, + :validate_title_max_marks_together, + :validate_title_start_with_caps, + :validate_body_min_length, + :validate_body_max_caps_percent, + :validate_body_max_marks_together, + :validate_body_start_with_caps] + end it "returns surveys" do expect(controller.helpers.config_var).to eq(:surveys) diff --git a/spec/forms/admin/config_form_spec.rb b/spec/forms/admin/config_form_spec.rb index 45be4fde6..06aea9a42 100644 --- a/spec/forms/admin/config_form_spec.rb +++ b/spec/forms/admin/config_form_spec.rb @@ -29,11 +29,20 @@ module Admin let(:valid_fields) { '[{"foo":"bar"}]' } let(:invalid_fields) { '[{"foo":"bar"}]{"baz":"zet"}' } + let(:validate_title_min_length) { 15 } + let(:validate_title_max_caps_percent) { 25 } + let(:validate_title_max_marks_together) { 2 } + let(:validate_title_start_with_caps) { true } + let(:validate_body_min_length) { 15 } + let(:validate_body_max_caps_percent) { 25 } + let(:validate_body_max_marks_together) { 2 } + let(:validate_body_start_with_caps) { true } + context "when everything is OK" do it { is_expected.to be_valid } end - context "when custom styles" do + describe "custom styles" do let(:attributes) do { scoped_styles: custom_styles @@ -49,11 +58,11 @@ module Admin } end - it { is_expected.not_to be_valid } + it { is_expected.to be_invalid } end end - context "when proposal custom fields" do + describe "proposal custom fields" do let(:attributes) do { proposal_custom_fields: custom_fields @@ -69,7 +78,7 @@ module Admin } end - it { is_expected.not_to be_valid } + it { is_expected.to be_invalid } end context "and sending labels with html" do @@ -81,6 +90,125 @@ module Admin end end end + + describe "validators" do + let(:attributes) do + { + validate_title_min_length: validate_title_min_length, + validate_title_max_caps_percent: validate_title_max_caps_percent, + validate_title_max_marks_together: validate_title_max_marks_together, + validate_title_start_with_caps: validate_title_start_with_caps, + validate_body_min_length: validate_body_min_length, + validate_body_max_caps_percent: validate_body_max_caps_percent, + validate_body_max_marks_together: validate_body_max_marks_together, + validate_body_start_with_caps: validate_body_start_with_caps + } + end + + it { is_expected.to be_valid } + + context "and title start with caps is false" do + let(:validate_title_start_with_caps) { false } + + it { is_expected.to be_valid } + end + + context "and title min length is empty" do + let(:validate_title_min_length) { nil } + + it { is_expected.to be_invalid } + end + + context "and title min length is zero" do + let(:validate_title_min_length) { 0 } + + it { is_expected.to be_invalid } + end + + context "and title min length greater than 100" do + let(:validate_title_min_length) { 101 } + + it { is_expected.to be_invalid } + end + + context "and body min length is empty" do + let(:validate_body_min_length) { nil } + + it { is_expected.to be_invalid } + end + + context "and body min length is zero" do + let(:validate_body_min_length) { 0 } + + it { is_expected.to be_valid } + end + + context "and title max caps percent empty" do + let(:validate_title_max_caps_percent) { nil } + + it { is_expected.to be_invalid } + end + + context "and title max caps percent is zero" do + let(:validate_title_max_caps_percent) { 0 } + + it { is_expected.to be_valid } + end + + context "and title max caps percent is bigger than 100" do + let(:validate_title_max_caps_percent) { 101 } + + it { is_expected.to be_invalid } + end + + context "and body start with caps is false" do + let(:validate_body_start_with_caps) { false } + + it { is_expected.to be_valid } + end + + context "and body max caps percent empty" do + let(:validate_body_max_caps_percent) { nil } + + it { is_expected.to be_invalid } + end + + context "and body max caps percent is zero" do + let(:validate_body_max_caps_percent) { 0 } + + it { is_expected.to be_valid } + end + + context "and body max caps percent is bigger than 100" do + let(:validate_body_max_caps_percent) { 101 } + + it { is_expected.to be_invalid } + end + + context "and title max marks together is empty" do + let(:validate_title_max_marks_together) { nil } + + it { is_expected.to be_invalid } + end + + context "and title max marks together is zero" do + let(:validate_title_max_marks_together) { 0 } + + it { is_expected.to be_invalid } + end + + context "and body max marks together is empty" do + let(:validate_body_max_marks_together) { nil } + + it { is_expected.to be_invalid } + end + + context "and body max marks together is zero" do + let(:validate_body_max_marks_together) { 0 } + + it { is_expected.to be_invalid } + end + end end end end diff --git a/spec/forms/proposal_wizard_create_step_form_spec.rb b/spec/forms/proposal_wizard_create_step_form_spec.rb index 2cef5543c..a5b6020b4 100644 --- a/spec/forms/proposal_wizard_create_step_form_spec.rb +++ b/spec/forms/proposal_wizard_create_step_form_spec.rb @@ -106,5 +106,162 @@ module Decidim::Proposals it { is_expected.to be_valid } end end + + shared_examples "starts with caps" do |prop| + let!(:config) { create :awesome_config, organization: organization, var: "validate_#{prop}_start_with_caps", value: enabled } + let!(:constraint) { create(:config_constraint, awesome_config: config, settings: { "participatory_space_manifest" => "participatory_processes", "participatory_space_slug" => slug }) } + + let(:enabled) { false } + let(prop.to_sym) { "í don't start with caps" } + + it { is_expected.to be_valid } + + context "when scoped under different context" do + let(:slug) { "another-slug" } + + it { is_expected.to be_invalid } + + context "when starts with caps" do + let(prop.to_sym) { "Í start with caps" } + + it { is_expected.to be_valid } + end + end + + context "when enabled" do + let(:enabled) { true } + + it { is_expected.to be_invalid } + + context "when starts with caps" do + let(prop.to_sym) { "Í start with caps" } + + it { is_expected.to be_valid } + end + end + end + + shared_examples "minimum length" do |prop| + let!(:config) { create :awesome_config, organization: organization, var: "validate_#{prop}_min_length", value: min_length } + let!(:constraint) { create(:config_constraint, awesome_config: config, settings: { "participatory_space_manifest" => "participatory_processes", "participatory_space_slug" => slug }) } + + let(:min_length) { 10 } + let(prop.to_sym) { "I am 10 yo" } + + it { is_expected.to be_valid } + + context "when scoped under different context" do + let(:slug) { "another-slug" } + + it { is_expected.to be_invalid } + + context "when has more than 15 chars" do + let(prop.to_sym) { "I am 17 years old" } + + it { is_expected.to be_valid } + end + end + + context "when less than allowed" do + let(:min_length) { 11 } + + it { is_expected.to be_invalid } + end + + # rubocop:disable RSpec/EmptyExampleGroup + context "when min_length is zero" do + let(:min_length) { 0 } + let(prop.to_sym) { "" } + + if prop == :body + it { is_expected.to be_valid } + else + it { is_expected.to be_invalid } + end + end + # rubocop:enable RSpec/EmptyExampleGroup + end + + shared_examples "max caps percent" do |prop| + let!(:config) { create :awesome_config, organization: organization, var: "validate_#{prop}_max_caps_percent", value: percent } + let!(:constraint) { create(:config_constraint, awesome_config: config, settings: { "participatory_space_manifest" => "participatory_processes", "participatory_space_slug" => slug }) } + + let(:percent) { 90 } + let(prop.to_sym) { "Í ÁM A SÈMI-CÁPS text" } + + it { is_expected.to be_valid } + + shared_examples "invalid percentage" do |per| + it "error message returns percentage" do + expect(form).to be_invalid + expect(form.errors.messages.values.flatten.first).to include("over #{per}% of the text") + end + end + + context "when scoped under different context" do + let(:slug) { "another-slug" } + + it_behaves_like "invalid percentage", 25 + + context "when has less than 25% caps" do + let(prop.to_sym) { "Í only have some CÁPS" } + + it { is_expected.to be_valid } + end + end + + context "when less than allowed" do + let(:percent) { 11 } + + it_behaves_like "invalid percentage", 11 + end + end + + shared_examples "max marks together" do |prop| + let!(:config) { create :awesome_config, organization: organization, var: "validate_#{prop}_max_marks_together", value: max_marks } + let!(:constraint) { create(:config_constraint, awesome_config: config, settings: { "participatory_space_manifest" => "participatory_processes", "participatory_space_slug" => slug }) } + + let(:max_marks) { 5 } + let(prop.to_sym) { "Am I a little bit noisy??!!!" } + + it { is_expected.to be_valid } + + context "when scoped under different context" do + let(:slug) { "another-slug" } + + it { is_expected.to be_invalid } + + context "when has only 1 mark" do + let(prop.to_sym) { "I am not noisy!" } + + it { is_expected.to be_valid } + end + + context "when has 2 marks" do + let(prop.to_sym) { "I am not noisy!?" } + + it { is_expected.to be_invalid } + end + end + + context "when less than allowed" do + let(:max_marks) { 4 } + + it { is_expected.to be_invalid } + end + end + + describe "etiquette validations" do + let(:body) { "A body longer than the permitted" } + + it_behaves_like "minimum length", :title + it_behaves_like "minimum length", :body + it_behaves_like "starts with caps", :title + it_behaves_like "starts with caps", :body + it_behaves_like "max caps percent", :title + it_behaves_like "max caps percent", :body + it_behaves_like "max marks together", :title + it_behaves_like "max marks together", :body + end end end diff --git a/spec/lib/system_checker_spec.rb b/spec/lib/system_checker_spec.rb index c5c332195..4f767799d 100644 --- a/spec/lib/system_checker_spec.rb +++ b/spec/lib/system_checker_spec.rb @@ -17,7 +17,7 @@ module Decidim::DecidimAwesome end it "has 5 modified files in core" do - expect(subject.overrides["decidim-core"].files.length).to eq(5) + expect(subject.overrides["decidim-core"].files.length).to eq(6) end it "has 5 modified files in proposals" do diff --git a/spec/system/admin/admin_spec.rb b/spec/system/admin/admin_spec.rb index 9e2e40206..2781ea6cd 100644 --- a/spec/system/admin/admin_spec.rb +++ b/spec/system/admin/admin_spec.rb @@ -105,6 +105,8 @@ it "renders the page" do expect(page).to have_content(/Tweaks for proposals/i) expect(page).to have_content("\"Rich text editor for participants\" is enabled") + expect(page).to have_content("User input validations for the \"title\" field") + expect(page).to have_content("User input validations for the \"body\" field") end context "and rich text editor for participants is disabled" do @@ -115,10 +117,36 @@ expect(page).not_to have_content("\"Rich text editor for participants\" is enabled") end end + + context "when all title validators are disabled" do + let(:disabled_features) { [:validate_title_min_length, :validate_title_max_caps_percent, :validate_title_max_marks_together, :validate_title_start_with_caps] } + + it "does not show title options" do + expect(page).not_to have_content("User input validations for the \"title\" field") + expect(page).to have_content("User input validations for the \"body\" field") + end + end + + context "when all body validators are disabled" do + let(:disabled_features) { [:validate_body_min_length, :validate_body_max_caps_percent, :validate_body_max_marks_together, :validate_body_start_with_caps] } + + it "does not show body options" do + expect(page).to have_content("User input validations for the \"title\" field") + expect(page).not_to have_content("User input validations for the \"body\" field") + end + end + end + + context "when some proposals hacks are disabled" do + [:allow_images_in_proposals, :validate_title_min_length, :validate_title_max_caps_percent, :validate_title_max_marks_together, :validate_title_start_with_caps, :validate_body_min_length, :validate_body_max_caps_percent, :validate_body_max_marks_together, :validate_body_start_with_caps].each do |var| + let(:disabled_features) { [var] } + + it_behaves_like "has menu link", "proposals" + end end - context "when proposal hacks are disabled" do - let(:disabled_features) { [:allow_images_in_proposals] } + context "when all proposals hacks are disabled" do + let(:disabled_features) { [:allow_images_in_proposals, :validate_title_min_length, :validate_title_max_caps_percent, :validate_title_max_marks_together, :validate_title_start_with_caps, :validate_body_min_length, :validate_body_max_caps_percent, :validate_body_max_marks_together, :validate_body_start_with_caps] } it_behaves_like "do not have menu link", "proposals" end From 743fc4fccccebc7c7c000d5fc68ca79da4468ed9 Mon Sep 17 00:00:00 2001 From: Anna Topalidi <60363870+antopalidi@users.noreply.github.com> Date: Mon, 16 Jan 2023 17:05:32 +0100 Subject: [PATCH 02/18] Setup admin accountablity (#6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add new section, add new controller, add view index * add permissions to index action * add rspec tests * add var to config and rspec * fix initializer * fix lint * fix permissions * refactor permissions handling * fix test Co-authored-by: Ivan Vergés --- .../admin/admin_actions_controller.rb | 22 ++++++++++ .../admin/admin_actions/index.html.erb | 8 ++++ config/locales/en.yml | 1 + lib/decidim/decidim_awesome/admin_engine.rb | 13 ++++++ lib/decidim/decidim_awesome/awesome.rb | 6 +++ .../admin/admin_actions_controller_spec.rb | 40 +++++++++++++++++++ spec/permissions/admin/permissions_spec.rb | 17 ++++++++ .../system/admin/admin_accountability_spec.rb | 35 ++++++++++++++++ 8 files changed, 142 insertions(+) create mode 100644 app/controllers/decidim/decidim_awesome/admin/admin_actions_controller.rb create mode 100644 app/views/decidim/decidim_awesome/admin/admin_actions/index.html.erb create mode 100644 spec/controllers/admin/admin_actions_controller_spec.rb create mode 100644 spec/system/admin/admin_accountability_spec.rb diff --git a/app/controllers/decidim/decidim_awesome/admin/admin_actions_controller.rb b/app/controllers/decidim/decidim_awesome/admin/admin_actions_controller.rb new file mode 100644 index 000000000..62d813531 --- /dev/null +++ b/app/controllers/decidim/decidim_awesome/admin/admin_actions_controller.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Decidim + module DecidimAwesome + module Admin + class AdminActionsController < DecidimAwesome::Admin::ApplicationController + include NeedsAwesomeConfig + + layout "decidim/admin/users" + before_action do + enforce_permission_to :edit_config, :allow_admin_accountability + end + + def index; end + + def export_xls + # TODO: export to xls + end + end + end + end +end diff --git a/app/views/decidim/decidim_awesome/admin/admin_actions/index.html.erb b/app/views/decidim/decidim_awesome/admin/admin_actions/index.html.erb new file mode 100644 index 000000000..076d9cc31 --- /dev/null +++ b/app/views/decidim/decidim_awesome/admin/admin_actions/index.html.erb @@ -0,0 +1,8 @@ +
+
+

<%= t("menu.admin_accountability", scope: "decidim.admin", default: "Admin accountability") %>

+
+
+

List of admin actions

+
+
diff --git a/config/locales/en.yml b/config/locales/en.yml index 32f4a5124..750189738 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -60,6 +60,7 @@ en: decidim: admin: menu: + admin_accountability: Admin accountability decidim_awesome: Decidim awesome components: awesome_iframe: diff --git a/lib/decidim/decidim_awesome/admin_engine.rb b/lib/decidim/decidim_awesome/admin_engine.rb index d9c9f5818..6ddc91fa6 100644 --- a/lib/decidim/decidim_awesome/admin_engine.rb +++ b/lib/decidim/decidim_awesome/admin_engine.rb @@ -22,6 +22,7 @@ class AdminEngine < ::Rails::Engine resources :scoped_styles, param: :var, only: [:create, :destroy] resources :proposal_custom_fields, param: :var, only: [:create, :destroy] resources :scoped_admins, param: :var, only: [:create, :destroy] + resources :admin_actions, only: [:index, :export_xls] get :users, to: "config#users" post :rename_scope_label, to: "config#rename_scope_label" get :checks, to: "checks#index" @@ -47,6 +48,18 @@ class AdminEngine < ::Rails::Engine end end + initializer "decidim_awesome.admin_menu" do + Decidim.menu :admin_user_menu do |menu| + if DecidimAwesome.enabled? :allow_admin_accountability + menu.add_item :admin_accountability, + I18n.t("menu.admin_accountability", scope: "decidim.admin", default: "Admin accountability"), + decidim_admin_decidim_awesome.admin_actions_path, + active: is_active_link?(decidim_admin_decidim_awesome.admin_actions_path, :inclusive), + position: 7 + end + end + end + def load_seed nil end diff --git a/lib/decidim/decidim_awesome/awesome.rb b/lib/decidim/decidim_awesome/awesome.rb index 10219e02c..b993285df 100644 --- a/lib/decidim/decidim_awesome/awesome.rb +++ b/lib/decidim/decidim_awesome/awesome.rb @@ -221,6 +221,12 @@ module DecidimAwesome } end + # If true, enables a new section in "Participants" where to audit all the admin roles that have been enabled/disabled historically in Decidim + # Set to :disabled to completly remove this feature + config_accessor :allow_admin_accountability do + true + end + # # HELPERS # diff --git a/spec/controllers/admin/admin_actions_controller_spec.rb b/spec/controllers/admin/admin_actions_controller_spec.rb new file mode 100644 index 000000000..776bb9d67 --- /dev/null +++ b/spec/controllers/admin/admin_actions_controller_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::DecidimAwesome + module Admin + describe AdminActionsController, type: :controller do + routes { Decidim::DecidimAwesome::AdminEngine.routes } + + let(:user) { create(:user, :confirmed, :admin, organization: organization) } + let(:organization) { create(:organization) } + let(:allow_admin_accountability) { true } + + before do + request.env["decidim.current_organization"] = user.organization + sign_in user, scope: :user + + allow(Decidim::DecidimAwesome.config).to receive(:allow_admin_accountability).and_return(allow_admin_accountability) + end + + describe "GET #index" do + context "when admin accountability is enabled" do + it "returns http success" do + get :index, params: {} + expect(response).to have_http_status(:success) + end + end + + context "when admin accountability is disabled" do + let!(:allow_admin_accountability) { :disabled } + + it "returns http success" do + get :index, params: {} + expect(response).to have_http_status(:found) + end + end + end + end + end +end diff --git a/spec/permissions/admin/permissions_spec.rb b/spec/permissions/admin/permissions_spec.rb index eeb219417..391cba3bc 100644 --- a/spec/permissions/admin/permissions_spec.rb +++ b/spec/permissions/admin/permissions_spec.rb @@ -63,5 +63,22 @@ module Decidim::DecidimAwesome::Admin it_behaves_like "permission is not set" end + + context "when accessing admin_accountability" do + let(:feature) { :allow_admin_accountability } + let(:status) { true } + + before do + allow(Decidim::DecidimAwesome.config).to receive(feature).and_return(status) + end + + it { is_expected.to eq true } + + context "when admin_accountability is disabled" do + let(:status) { :disabled } + + it { is_expected.to eq false } + end + end end end diff --git a/spec/system/admin/admin_accountability_spec.rb b/spec/system/admin/admin_accountability_spec.rb new file mode 100644 index 000000000..6cc450b50 --- /dev/null +++ b/spec/system/admin/admin_accountability_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Admin accountability", type: :system do + let(:organization) { create :organization } + let!(:user) { create :user, :admin, :confirmed, organization: organization } + let(:status) { true } + + before do + allow(Decidim::DecidimAwesome.config).to receive(:allow_admin_accountability).and_return(status) + switch_to_host(organization.host) + login_as user, scope: :user + + visit decidim_admin.root_path + end + + context "when admin accountability is enabled" do + it "shows the admin accountability link" do + click_link "Participants" + + expect(page).to have_content("Admin accountability") + end + end + + context "when admin accountability is disabled" do + let(:status) { :disabled } + + it "does not show the admin accountability link" do + click_link "Participants" + + expect(page).not_to have_content("Admin accountability") + end + end +end From 0666824a726b64073b5d76216c0a49d905669f24 Mon Sep 17 00:00:00 2001 From: Anna Topalidi <60363870+antopalidi@users.noreply.github.com> Date: Wed, 1 Feb 2023 13:21:21 +0100 Subject: [PATCH 03/18] View and controllers (#7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * improve the controller and add a table to the view * logs list * add PaperTrail instead ActionLog * add removal date to table * change datetime format * move remove instance var from helper * add styles to table, add role * add pagination * fix pagination * add rspec * change controller * change controller * fix lint * add rspec * add participatory_space_type to table * fix lint * refactoring * change method's name * improve the controller and add a table to the view * logs list * add PaperTrail instead ActionLog * add removal date to table * change datetime format * move remove instance var from helper * add styles to table, add role * add pagination * fix pagination * add rspec * change controller * change controller * fix lint * add rspec * add participatory_space_type to table * fix lint * refactoring * change method's name * add config for types_user_roles * change view and helper, move building html to view from helper * fix lint * fix lint * add presenter, remove helper * fix pagination * add link to participatory space, add presenter spec * extract the i18n version of the role * add rspec PaperTrailVersion * rspec PaperTrailRolePresenter * Fix presenter * fix tests * fix presenter * handle deleted users * fix checksums --------- Co-authored-by: Ivan Vergés --- Gemfile | 4 +- Gemfile.lock | 294 +++++++++--------- .../admin/admin_accountability_controller.rb | 32 ++ .../admin/admin_actions_controller.rb | 22 -- .../decidim_awesome/paper_trail_version.rb | 18 ++ .../paper_trail_role_presenter.rb | 131 ++++++++ .../admin/admin_accountability/index.html.erb | 42 +++ .../admin/admin_actions/index.html.erb | 8 - config/locales/en.yml | 23 ++ lib/decidim/decidim_awesome/admin_engine.rb | 11 +- lib/decidim/decidim_awesome/awesome.rb | 5 + lib/decidim/decidim_awesome/checksums.yml | 1 + lib/decidim/decidim_awesome/test/factories.rb | 7 + ...> admin_accountability_controller_spec.rb} | 2 +- spec/models/parer_trail_version_spec.rb | 34 ++ spec/permissions/admin/permissions_spec.rb | 4 +- .../paper_trail_role_presenter_spec.rb | 204 ++++++++++++ .../system/admin/admin_accountability_spec.rb | 155 ++++++++- spec/system/admin/scoped_admins_spec.rb | 4 +- 19 files changed, 809 insertions(+), 192 deletions(-) create mode 100644 app/controllers/decidim/decidim_awesome/admin/admin_accountability_controller.rb delete mode 100644 app/controllers/decidim/decidim_awesome/admin/admin_actions_controller.rb create mode 100644 app/models/decidim/decidim_awesome/paper_trail_version.rb create mode 100644 app/presenters/decidim/decidim_awesome/paper_trail_role_presenter.rb create mode 100644 app/views/decidim/decidim_awesome/admin/admin_accountability/index.html.erb delete mode 100644 app/views/decidim/decidim_awesome/admin/admin_actions/index.html.erb rename spec/controllers/admin/{admin_actions_controller_spec.rb => admin_accountability_controller_spec.rb} (94%) create mode 100644 spec/models/parer_trail_version_spec.rb create mode 100644 spec/presenters/paper_trail_role_presenter_spec.rb diff --git a/Gemfile b/Gemfile index 45a51a5d6..3f6877fe3 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ source "https://rubygems.org" ruby RUBY_VERSION -DECIDIM_VERSION = "0.27.0" +DECIDIM_VERSION = "0.27.1" gem "decidim", DECIDIM_VERSION gem "decidim-decidim_awesome", path: "." @@ -28,7 +28,7 @@ group :development do gem "rubocop-faker" gem "spring", "~> 2.0" gem "spring-watcher-listen", "~> 2.0.0" - gem "web-console", "~> 3.5" + gem "web-console" end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 7859237ba..2a5dca9f0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,40 +9,40 @@ PATH GEM remote: https://rubygems.org/ specs: - actioncable (6.1.7) - actionpack (= 6.1.7) - activesupport (= 6.1.7) + actioncable (6.1.7.2) + actionpack (= 6.1.7.2) + activesupport (= 6.1.7.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.7) - actionpack (= 6.1.7) - activejob (= 6.1.7) - activerecord (= 6.1.7) - activestorage (= 6.1.7) - activesupport (= 6.1.7) + actionmailbox (6.1.7.2) + actionpack (= 6.1.7.2) + activejob (= 6.1.7.2) + activerecord (= 6.1.7.2) + activestorage (= 6.1.7.2) + activesupport (= 6.1.7.2) mail (>= 2.7.1) - actionmailer (6.1.7) - actionpack (= 6.1.7) - actionview (= 6.1.7) - activejob (= 6.1.7) - activesupport (= 6.1.7) + actionmailer (6.1.7.2) + actionpack (= 6.1.7.2) + actionview (= 6.1.7.2) + activejob (= 6.1.7.2) + activesupport (= 6.1.7.2) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.7) - actionview (= 6.1.7) - activesupport (= 6.1.7) + actionpack (6.1.7.2) + actionview (= 6.1.7.2) + activesupport (= 6.1.7.2) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.7) - actionpack (= 6.1.7) - activerecord (= 6.1.7) - activestorage (= 6.1.7) - activesupport (= 6.1.7) + actiontext (6.1.7.2) + actionpack (= 6.1.7.2) + activerecord (= 6.1.7.2) + activestorage (= 6.1.7.2) + activesupport (= 6.1.7.2) nokogiri (>= 1.8.5) - actionview (6.1.7) - activesupport (= 6.1.7) + actionview (6.1.7.2) + activesupport (= 6.1.7.2) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -50,22 +50,22 @@ GEM active_link_to (1.0.5) actionpack addressable - activejob (6.1.7) - activesupport (= 6.1.7) + activejob (6.1.7.2) + activesupport (= 6.1.7.2) globalid (>= 0.3.6) - activemodel (6.1.7) - activesupport (= 6.1.7) - activerecord (6.1.7) - activemodel (= 6.1.7) - activesupport (= 6.1.7) - activestorage (6.1.7) - actionpack (= 6.1.7) - activejob (= 6.1.7) - activerecord (= 6.1.7) - activesupport (= 6.1.7) + activemodel (6.1.7.2) + activesupport (= 6.1.7.2) + activerecord (6.1.7.2) + activemodel (= 6.1.7.2) + activesupport (= 6.1.7.2) + activestorage (6.1.7.2) + actionpack (= 6.1.7.2) + activejob (= 6.1.7.2) + activerecord (= 6.1.7.2) + activesupport (= 6.1.7.2) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.7) + activesupport (6.1.7.2) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -146,8 +146,8 @@ GEM coffee-script-source execjs coffee-script-source (1.12.2) - commonmarker (0.23.6) - concurrent-ruby (1.1.10) + commonmarker (0.23.7) + concurrent-ruby (1.2.0) crack (0.4.5) rexml crass (1.0.6) @@ -160,53 +160,53 @@ GEM db-query-matchers (0.10.0) activesupport (>= 4.0, < 7) rspec (~> 3.0) - decidim (0.27.0) - decidim-accountability (= 0.27.0) - decidim-admin (= 0.27.0) - decidim-api (= 0.27.0) - decidim-assemblies (= 0.27.0) - decidim-blogs (= 0.27.0) - decidim-budgets (= 0.27.0) - decidim-comments (= 0.27.0) - decidim-core (= 0.27.0) - decidim-debates (= 0.27.0) - decidim-forms (= 0.27.0) - decidim-generators (= 0.27.0) - decidim-meetings (= 0.27.0) - decidim-pages (= 0.27.0) - decidim-participatory_processes (= 0.27.0) - decidim-proposals (= 0.27.0) - decidim-sortitions (= 0.27.0) - decidim-surveys (= 0.27.0) - decidim-system (= 0.27.0) - decidim-templates (= 0.27.0) - decidim-verifications (= 0.27.0) - decidim-accountability (0.27.0) - decidim-comments (= 0.27.0) - decidim-core (= 0.27.0) - decidim-admin (0.27.0) + decidim (0.27.1) + decidim-accountability (= 0.27.1) + decidim-admin (= 0.27.1) + decidim-api (= 0.27.1) + decidim-assemblies (= 0.27.1) + decidim-blogs (= 0.27.1) + decidim-budgets (= 0.27.1) + decidim-comments (= 0.27.1) + decidim-core (= 0.27.1) + decidim-debates (= 0.27.1) + decidim-forms (= 0.27.1) + decidim-generators (= 0.27.1) + decidim-meetings (= 0.27.1) + decidim-pages (= 0.27.1) + decidim-participatory_processes (= 0.27.1) + decidim-proposals (= 0.27.1) + decidim-sortitions (= 0.27.1) + decidim-surveys (= 0.27.1) + decidim-system (= 0.27.1) + decidim-templates (= 0.27.1) + decidim-verifications (= 0.27.1) + decidim-accountability (0.27.1) + decidim-comments (= 0.27.1) + decidim-core (= 0.27.1) + decidim-admin (0.27.1) active_link_to (~> 1.0) - decidim-core (= 0.27.0) + decidim-core (= 0.27.1) devise (~> 4.7) devise-i18n (~> 1.2) devise_invitable (~> 2.0) - decidim-api (0.27.0) + decidim-api (0.27.1) graphql (~> 1.12, < 1.13) graphql-docs (~> 2.1.0) rack-cors (~> 1.0) - decidim-assemblies (0.27.0) - decidim-core (= 0.27.0) - decidim-blogs (0.27.0) - decidim-admin (= 0.27.0) - decidim-comments (= 0.27.0) - decidim-core (= 0.27.0) - decidim-budgets (0.27.0) - decidim-comments (= 0.27.0) - decidim-core (= 0.27.0) - decidim-comments (0.27.0) - decidim-core (= 0.27.0) + decidim-assemblies (0.27.1) + decidim-core (= 0.27.1) + decidim-blogs (0.27.1) + decidim-admin (= 0.27.1) + decidim-comments (= 0.27.1) + decidim-core (= 0.27.1) + decidim-budgets (0.27.1) + decidim-comments (= 0.27.1) + decidim-core (= 0.27.1) + decidim-comments (0.27.1) + decidim-core (= 0.27.1) redcarpet (~> 3.5, >= 3.5.1) - decidim-core (0.27.0) + decidim-core (0.27.1) active_link_to (~> 1.0) acts_as_list (~> 0.9) batch-loader (~> 1.2) @@ -216,7 +216,7 @@ GEM cells-rails (~> 0.1.3) charlock_holmes (~> 0.7) date_validator (~> 0.12.0) - decidim-api (= 0.27.0) + decidim-api (= 0.27.1) devise (~> 4.7) devise-i18n (~> 1.2) diffy (~> 3.3) @@ -257,15 +257,15 @@ GEM webpacker (= 6.0.0.rc.5) webpush (~> 1.1) wisper (~> 2.0) - decidim-debates (0.27.0) - decidim-comments (= 0.27.0) - decidim-core (= 0.27.0) - decidim-dev (0.27.0) + decidim-debates (0.27.1) + decidim-comments (= 0.27.1) + decidim-core (= 0.27.1) + decidim-dev (0.27.1) axe-core-rspec (~> 4.1.0) byebug (~> 11.0) capybara (~> 3.24) db-query-matchers (~> 0.10.0) - decidim (= 0.27.0) + decidim (= 0.27.1) erb_lint (~> 0.0.35) factory_bot_rails (~> 4.8) i18n-tasks (~> 0.9.18) @@ -288,45 +288,45 @@ GEM w3c_rspec_validators (~> 0.3.0) webmock (~> 3.6) wisper-rspec (~> 1.0) - decidim-forms (0.27.0) - decidim-core (= 0.27.0) + decidim-forms (0.27.1) + decidim-core (= 0.27.1) wicked_pdf (~> 2.1) wkhtmltopdf-binary (~> 0.12) - decidim-generators (0.27.0) - decidim-core (= 0.27.0) - decidim-meetings (0.27.0) - decidim-core (= 0.27.0) - decidim-forms (= 0.27.0) + decidim-generators (0.27.1) + decidim-core (= 0.27.1) + decidim-meetings (0.27.1) + decidim-core (= 0.27.1) + decidim-forms (= 0.27.1) icalendar (~> 2.5) - decidim-pages (0.27.0) - decidim-core (= 0.27.0) - decidim-participatory_processes (0.27.0) - decidim-core (= 0.27.0) - decidim-proposals (0.27.0) - decidim-comments (= 0.27.0) - decidim-core (= 0.27.0) + decidim-pages (0.27.1) + decidim-core (= 0.27.1) + decidim-participatory_processes (0.27.1) + decidim-core (= 0.27.1) + decidim-proposals (0.27.1) + decidim-comments (= 0.27.1) + decidim-core (= 0.27.1) doc2text (~> 0.4.5) redcarpet (~> 3.5, >= 3.5.1) - decidim-sortitions (0.27.0) - decidim-admin (= 0.27.0) - decidim-comments (= 0.27.0) - decidim-core (= 0.27.0) - decidim-proposals (= 0.27.0) - decidim-surveys (0.27.0) - decidim-core (= 0.27.0) - decidim-forms (= 0.27.0) - decidim-templates (= 0.27.0) - decidim-system (0.27.0) + decidim-sortitions (0.27.1) + decidim-admin (= 0.27.1) + decidim-comments (= 0.27.1) + decidim-core (= 0.27.1) + decidim-proposals (= 0.27.1) + decidim-surveys (0.27.1) + decidim-core (= 0.27.1) + decidim-forms (= 0.27.1) + decidim-templates (= 0.27.1) + decidim-system (0.27.1) active_link_to (~> 1.0) - decidim-core (= 0.27.0) + decidim-core (= 0.27.1) devise (~> 4.7) devise-i18n (~> 1.2) devise_invitable (~> 2.0) - decidim-templates (0.27.0) - decidim-core (= 0.27.0) - decidim-forms (= 0.27.0) - decidim-verifications (0.27.0) - decidim-core (= 0.27.0) + decidim-templates (0.27.1) + decidim-core (= 0.27.1) + decidim-forms (= 0.27.1) + decidim-verifications (0.27.1) + decidim-core (= 0.27.1) declarative-builder (0.1.0) declarative-option (< 0.2.0) declarative-option (0.1.0) @@ -365,7 +365,7 @@ GEM temple erubi (1.12.0) escape_utils (1.3.0) - excon (0.97.1) + excon (0.97.2) execjs (2.8.1) extended-markdown-filter (0.7.0) html-pipeline (~> 2.9) @@ -376,7 +376,7 @@ GEM railties (>= 3.0.0) faker (2.23.0) i18n (>= 1.8.11, < 2) - faraday (2.7.3) + faraday (2.7.4) faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) faraday-net_http (3.0.2) @@ -399,7 +399,7 @@ GEM railties (>= 4.1, < 7.1) gemoji (3.0.1) geocoder (1.8.1) - globalid (1.0.0) + globalid (1.0.1) activesupport (>= 5.0) graphql (1.12.24) graphql-docs (2.1.0) @@ -525,7 +525,7 @@ GEM rack (>= 1.2, < 4) snaky_hash (~> 2.0) version_gem (~> 1.1) - omniauth (2.1.0) + omniauth (2.1.1) hashie (>= 3.4.6) rack (>= 2.2.3) rack-protection @@ -561,7 +561,7 @@ GEM pg_search (2.3.6) activerecord (>= 5.2) activesupport (>= 5.2) - premailer (1.18.0) + premailer (1.19.0) addressable css_parser (>= 1.12.0) htmlentities (>= 4.0.0) @@ -573,31 +573,31 @@ GEM puma (5.6.5) nio4r (~> 2.0) racc (1.6.2) - rack (2.2.6) + rack (2.2.6.2) rack-attack (6.6.1) rack (>= 1.0, < 3) rack-cors (1.1.1) rack (>= 2.0.0) rack-protection (3.0.5) rack - rack-proxy (0.7.4) + rack-proxy (0.7.6) rack rack-test (2.0.2) rack (>= 1.3) - rails (6.1.7) - actioncable (= 6.1.7) - actionmailbox (= 6.1.7) - actionmailer (= 6.1.7) - actionpack (= 6.1.7) - actiontext (= 6.1.7) - actionview (= 6.1.7) - activejob (= 6.1.7) - activemodel (= 6.1.7) - activerecord (= 6.1.7) - activestorage (= 6.1.7) - activesupport (= 6.1.7) + rails (6.1.7.2) + actioncable (= 6.1.7.2) + actionmailbox (= 6.1.7.2) + actionmailer (= 6.1.7.2) + actionpack (= 6.1.7.2) + actiontext (= 6.1.7.2) + actionview (= 6.1.7.2) + activejob (= 6.1.7.2) + activemodel (= 6.1.7.2) + activerecord (= 6.1.7.2) + activestorage (= 6.1.7.2) + activesupport (= 6.1.7.2) bundler (>= 1.15.0) - railties (= 6.1.7) + railties (= 6.1.7.2) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -611,9 +611,9 @@ GEM rails-i18n (6.0.0) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 7) - railties (6.1.7) - actionpack (= 6.1.7) - activesupport (= 6.1.7) + railties (6.1.7.2) + actionpack (= 6.1.7.2) + activesupport (= 6.1.7.2) method_source rake (>= 12.2) thor (~> 1.0) @@ -628,7 +628,7 @@ GEM ffi (~> 1.0) redcarpet (3.5.1) redis (4.8.0) - regexp_parser (2.6.1) + regexp_parser (2.6.2) request_store (1.5.1) rack (>= 1.4) responders (3.0.1) @@ -734,7 +734,7 @@ GEM activesupport (>= 5.2) sprockets (>= 3.0.0) ssrf_filter (1.1.1) - temple (0.9.1) + temple (0.10.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) thor (1.2.1) @@ -766,11 +766,11 @@ GEM rexml (~> 3.2) warden (1.2.9) rack (>= 2.0.9) - web-console (3.7.0) - actionview (>= 5.0) - activemodel (>= 5.0) + web-console (4.2.0) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) bindex (>= 0.4.0) - railties (>= 5.0) + railties (>= 6.0.0) webmock (3.18.1) addressable (>= 2.8.0) crack (>= 0.3.2) @@ -802,9 +802,9 @@ DEPENDENCIES bootsnap (~> 1.4) byebug (~> 11.0) codecov - decidim (= 0.27.0) + decidim (= 0.27.1) decidim-decidim_awesome! - decidim-dev (= 0.27.0) + decidim-dev (= 0.27.1) faker (~> 2.14) letter_opener_web (~> 1.3) listen (~> 3.1) @@ -813,10 +813,10 @@ DEPENDENCIES spring (~> 2.0) spring-watcher-listen (~> 2.0.0) uglifier (~> 4.1) - web-console (~> 3.5) + web-console RUBY VERSION ruby 3.0.5p211 BUNDLED WITH - 2.2.33 + 2.3.20 diff --git a/app/controllers/decidim/decidim_awesome/admin/admin_accountability_controller.rb b/app/controllers/decidim/decidim_awesome/admin/admin_accountability_controller.rb new file mode 100644 index 000000000..0c19b9ec6 --- /dev/null +++ b/app/controllers/decidim/decidim_awesome/admin/admin_accountability_controller.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Decidim + module DecidimAwesome + module Admin + class AdminAccountabilityController < DecidimAwesome::Admin::ApplicationController + include NeedsAwesomeConfig + include Decidim::Admin::Filterable + + helper_method :admin_actions + + layout "decidim/admin/users" + + before_action do + enforce_permission_to :edit_config, :allow_admin_accountability + end + + def index; end + + def export + # TODO: export to xls, csv + end + + private + + def admin_actions + @admin_actions ||= paginate(PaperTrailVersion.role_actions) + end + end + end + end +end diff --git a/app/controllers/decidim/decidim_awesome/admin/admin_actions_controller.rb b/app/controllers/decidim/decidim_awesome/admin/admin_actions_controller.rb deleted file mode 100644 index 62d813531..000000000 --- a/app/controllers/decidim/decidim_awesome/admin/admin_actions_controller.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module DecidimAwesome - module Admin - class AdminActionsController < DecidimAwesome::Admin::ApplicationController - include NeedsAwesomeConfig - - layout "decidim/admin/users" - before_action do - enforce_permission_to :edit_config, :allow_admin_accountability - end - - def index; end - - def export_xls - # TODO: export to xls - end - end - end - end -end diff --git a/app/models/decidim/decidim_awesome/paper_trail_version.rb b/app/models/decidim/decidim_awesome/paper_trail_version.rb new file mode 100644 index 000000000..b8a438931 --- /dev/null +++ b/app/models/decidim/decidim_awesome/paper_trail_version.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Decidim + module DecidimAwesome + class PaperTrailVersion < PaperTrail::Version + default_scope { order("created_at DESC") } + scope :role_actions, -> { where(item_type: ::Decidim::DecidimAwesome.admin_user_roles, event: "create") } + + def present + @present ||= if item_type.in?(Decidim::DecidimAwesome.admin_user_roles) + PaperTrailRolePresenter.new(self) + else + self + end + end + end + end +end diff --git a/app/presenters/decidim/decidim_awesome/paper_trail_role_presenter.rb b/app/presenters/decidim/decidim_awesome/paper_trail_role_presenter.rb new file mode 100644 index 000000000..e0014a733 --- /dev/null +++ b/app/presenters/decidim/decidim_awesome/paper_trail_role_presenter.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +module Decidim + module DecidimAwesome + class PaperTrailRolePresenter < Decidim::Log::BasePresenter + include TranslatableAttributes + + attr_reader :entry, :html + + def initialize(entry, html: true) + @entry = entry + @html = html + end + + # try to use the object in the database if exists + # Note that "reify" does not work on "create" events + def item + @item ||= entry&.item + end + + # Finds the destroyed entry if exists + def destroy_entry + @destroy_entry ||= PaperTrail::Version.find_by(item_type: entry.item_type, event: "destroy", item_id: entry.item_id) + end + + alias destroyed? destroy_entry + + # try to reconstruct a destroyed event + def destroy_item + @destroy_item ||= destroy_entry&.reify + end + + # participatory spaces is in the normal entry if the role hasn't been removed + # otherwise is in the removed role log entry + def participatory_space + item&.participatory_space || destroy_item&.participatory_space + end + + # roles are in the destroyed event if the role has been removed + def role + @role ||= destroy_item&.role || item&.role + end + + def role_class + case role + when "admin" + "text-alert" + when "valuator" + "text-secondary" + end + end + + def role_name + I18n.t(role, scope: "decidim.decidim_awesome.admin.admin_accountability.roles", default: role) + end + + def participatory_space_name + "#{participatory_space_type} > #{translated_attribute participatory_space&.title}" + end + + def participatory_space_type + I18n.t(participatory_space&.manifest&.name, scope: "decidim.admin.menu", default: entry.changeset) + end + + # try to link to the user roles page or to the participatory space if not existing + def participatory_space_path + proxy.send("#{participatory_space.manifest.route_name}_user_roles_path") + rescue NoMethodError + begin + proxy.send("#{participatory_space.manifest.route_name}_path", participatory_space) + rescue NoMethodError + "" + end + end + + def user + @user ||= Decidim::User.find_by(id: entry.changeset["decidim_user_id"]&.last) + end + + def user_name + return I18n.t("missing_user", scope: "decidim.decidim_awesome.admin.admin_accountability") unless user + return I18n.t("deleted_user", scope: "decidim.decidim_awesome.admin.admin_accountability") if user.deleted? + + user&.name + end + + def user_email + user&.email + end + + def created_at + entry.changeset["created_at"]&.last || entry&.created_at + end + + def created_date + I18n.l(created_at, format: :short) + rescue I18n::ArgumentError + "" + end + + def destroyed_at + destroy_entry&.created_at + end + + def removal_date + I18n.l(destroyed_at, format: :short) + rescue I18n::ArgumentError + info_text("currently_active", klass: "text-success") + end + + def last_sign_in_date + I18n.l(user&.last_sign_in_at, format: :short) + rescue I18n::ArgumentError + info_text("never_logged") + end + + private + + def info_text(key, klass: :muted) + text = I18n.t(key, scope: "decidim.decidim_awesome.admin.admin_accountability") + return text unless html + + "#{text}".html_safe + end + + def proxy + @proxy ||= Decidim::EngineRouter.admin_proxy(participatory_space) + end + end + end +end diff --git a/app/views/decidim/decidim_awesome/admin/admin_accountability/index.html.erb b/app/views/decidim/decidim_awesome/admin/admin_accountability/index.html.erb new file mode 100644 index 000000000..61f12cebd --- /dev/null +++ b/app/views/decidim/decidim_awesome/admin/admin_accountability/index.html.erb @@ -0,0 +1,42 @@ +
+
+

<%= t(".title") %>

+
+
+
+ <%= admin_filters_pagination %> +
+
+
+

<%= t(".description") %>

+
+ + + + + + + + + + + + + + <% admin_actions.each do |log| %> + "> + + + + + + + + + <% end %> + +
<%= t("role", scope: "decidim.decidim_awesome.admin.admin_accountability") %><%= t("name", scope: "decidim.decidim_awesome.admin.admin_accountability") %><%= t("email", scope: "decidim.decidim_awesome.admin.admin_accountability") %><%= t("participatory_space", scope: "decidim.decidim_awesome.admin.admin_accountability") %><%= t("last_login", scope: "decidim.decidim_awesome.admin.admin_accountability") %><%= t("created_at", scope: "decidim.decidim_awesome.admin.admin_accountability") %><%= t("removal_date", scope: "decidim.decidim_awesome.admin.admin_accountability") %>
<%= log.present.role_name %><%= log.present.user_name %><%= link_to(log.present.user_email, "mailto:#{log.present.user_email}") if log.present.user_email %><%= link_to log.present.participatory_space_name, log.present.participatory_space_path %><%= log.present.last_sign_in_date %><%= log.present.created_date %><%= log.present.removal_date %>
+ <%= paginate admin_actions, theme: "decidim" %> +
+
+
diff --git a/app/views/decidim/decidim_awesome/admin/admin_actions/index.html.erb b/app/views/decidim/decidim_awesome/admin/admin_actions/index.html.erb deleted file mode 100644 index 076d9cc31..000000000 --- a/app/views/decidim/decidim_awesome/admin/admin_actions/index.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -
-
-

<%= t("menu.admin_accountability", scope: "decidim.admin", default: "Admin accountability") %>

-
-
-

List of admin actions

-
-
diff --git a/config/locales/en.yml b/config/locales/en.yml index a40d4d9a8..e2bcf85c3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -105,6 +105,29 @@ en: show_withdrawn: Show withdrawn proposals decidim_awesome: admin: + admin_accountability: + created_at: Role created at + currently_active: Currently active + deleted_user: Deleted user + email: Email + index: + description: Listed here you'll find all users that have had some role + in the administration of a participatory space. Normal admins are not + listed. + title: Admin accountability + last_login: Last sign in date + missing_info: "(missing information)" + missing_user: User not in the database + name: Name + never_logged: Never logged yet + participatory_space: Participatory space + removal_date: Role removed at + role: Role + roles: + admin: Administrator + collaborator: Collaborator + moderator: Moderator + valuator: Valuator checks: index: admin_head_tags: Awesome tags included in the admin application header diff --git a/lib/decidim/decidim_awesome/admin_engine.rb b/lib/decidim/decidim_awesome/admin_engine.rb index 6ddc91fa6..3deb5d36c 100644 --- a/lib/decidim/decidim_awesome/admin_engine.rb +++ b/lib/decidim/decidim_awesome/admin_engine.rb @@ -22,7 +22,8 @@ class AdminEngine < ::Rails::Engine resources :scoped_styles, param: :var, only: [:create, :destroy] resources :proposal_custom_fields, param: :var, only: [:create, :destroy] resources :scoped_admins, param: :var, only: [:create, :destroy] - resources :admin_actions, only: [:index, :export_xls] + get :admin_accountability, to: "admin_accountability#index", as: "admin_accountability" + post :export_admin_accountability, to: "admin_accountability#export", as: "export_admin_accountability" get :users, to: "config#users" post :rename_scope_label, to: "config#rename_scope_label" get :checks, to: "checks#index" @@ -39,7 +40,7 @@ class AdminEngine < ::Rails::Engine initializer "decidim_awesome.admin_menu" do Decidim.menu :admin_menu do |menu| menu.add_item :awesome_menu, - I18n.t("menu.decidim_awesome", scope: "decidim.admin", default: "Decidim Awesome"), + I18n.t("menu.decidim_awesome", scope: "decidim.admin"), decidim_admin_decidim_awesome.config_path(:editors), icon_name: "fire", position: 7.5, @@ -52,9 +53,9 @@ class AdminEngine < ::Rails::Engine Decidim.menu :admin_user_menu do |menu| if DecidimAwesome.enabled? :allow_admin_accountability menu.add_item :admin_accountability, - I18n.t("menu.admin_accountability", scope: "decidim.admin", default: "Admin accountability"), - decidim_admin_decidim_awesome.admin_actions_path, - active: is_active_link?(decidim_admin_decidim_awesome.admin_actions_path, :inclusive), + I18n.t("menu.admin_accountability", scope: "decidim.admin"), + decidim_admin_decidim_awesome.admin_accountability_path, + active: is_active_link?(decidim_admin_decidim_awesome.admin_accountability_path, :inclusive), position: 7 end end diff --git a/lib/decidim/decidim_awesome/awesome.rb b/lib/decidim/decidim_awesome/awesome.rb index c22b70c9c..b02248765 100644 --- a/lib/decidim/decidim_awesome/awesome.rb +++ b/lib/decidim/decidim_awesome/awesome.rb @@ -227,6 +227,11 @@ module DecidimAwesome true end + # Roles for which it is necessary to show admin_accountability + config_accessor :admin_user_roles do + %w(Decidim::AssemblyUserRole Decidim::ParticipatoryProcessUserRole Decidim::ConferencesUserRole) + end + # # HELPERS # diff --git a/lib/decidim/decidim_awesome/checksums.yml b/lib/decidim/decidim_awesome/checksums.yml index e996653bb..763d15630 100644 --- a/lib/decidim/decidim_awesome/checksums.yml +++ b/lib/decidim/decidim_awesome/checksums.yml @@ -29,6 +29,7 @@ decidim-proposals: decidim-0.26: 216c974bc425393c18b01bfc4eed4f0b decidim-0.26.4: 2e673d2aabe66a80a971d7ff80ebdbb8 decidim-0.27: c0ebeac39ebe4926bf0e5fc585a384d7 + decidim-0.27.1: a4f902d1c4829a7f7f62299686f8604e /app/views/decidim/proposals/collaborative_drafts/show.html.erb: decidim-0.24: 2a7e0a4c65361f238fd1b917f39c8642 /app/views/decidim/proposals/collaborative_drafts/_edit_form_fields.html.erb: diff --git a/lib/decidim/decidim_awesome/test/factories.rb b/lib/decidim/decidim_awesome/test/factories.rb index f6dd9e896..542586375 100644 --- a/lib/decidim/decidim_awesome/test/factories.rb +++ b/lib/decidim/decidim_awesome/test/factories.rb @@ -23,6 +23,13 @@ organization { create :organization } end + factory :paper_trail_version, class: Decidim::DecidimAwesome::PaperTrailVersion do + item_id { user.id } + item_type { "Decidim::ParticipatoryProcessUserRole" } + event { "create" } + created_at { 1.hour.ago } + end + factory :map_component, parent: :component do name { Decidim::Components::Namer.new(participatory_space.organization.available_locales, :proposals).i18n_name } manifest_name { :awesome_map } diff --git a/spec/controllers/admin/admin_actions_controller_spec.rb b/spec/controllers/admin/admin_accountability_controller_spec.rb similarity index 94% rename from spec/controllers/admin/admin_actions_controller_spec.rb rename to spec/controllers/admin/admin_accountability_controller_spec.rb index 776bb9d67..4da26b204 100644 --- a/spec/controllers/admin/admin_actions_controller_spec.rb +++ b/spec/controllers/admin/admin_accountability_controller_spec.rb @@ -4,7 +4,7 @@ module Decidim::DecidimAwesome module Admin - describe AdminActionsController, type: :controller do + describe AdminAccountabilityController, type: :controller do routes { Decidim::DecidimAwesome::AdminEngine.routes } let(:user) { create(:user, :confirmed, :admin, organization: organization) } diff --git a/spec/models/parer_trail_version_spec.rb b/spec/models/parer_trail_version_spec.rb new file mode 100644 index 000000000..2c3a1079c --- /dev/null +++ b/spec/models/parer_trail_version_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "spec_helper" +module Decidim::DecidimAwesome + describe PaperTrailVersion, type: :model do + subject { paper_trail_version } + + let(:organization) { create(:organization) } + let(:user) { create(:user) } + let(:participatory_process_user_role) { create(:participatory_process_user_role, participatory_process: participatory_process, user: administrator, role: "admin", created_at: 1.day.ago) } + let(:paper_trail_version) { create(:paper_trail_version, item_type: "Decidim::AssemblyUserRole", item_id: participatory_process_user_role.id, whodunnit: user.id, event: "create") } + let(:administrator) { create(:user, organization: organization, last_sign_in_at: 1.day.ago) } + let(:participatory_process) { create(:participatory_process, organization: organization) } + + it { is_expected.to be_valid } + + it "paper_trail_version is associated with user" do + expect(subject).to eq(paper_trail_version) + expect(subject.whodunnit).to eq(user.id.to_s) + end + + it "returns default_scope ordered by created_at" do + expect(PaperTrailVersion.all).to eq([paper_trail_version]) + end + + it "returns role_actions scope correctly" do + expect(PaperTrailVersion.role_actions).to include(paper_trail_version) + end + + it "present method returns a PaperTrailRolePresenter object" do + expect(subject.present).to be_a(PaperTrailRolePresenter) + end + end +end diff --git a/spec/permissions/admin/permissions_spec.rb b/spec/permissions/admin/permissions_spec.rb index 8c0fc2c2f..5831c41d0 100644 --- a/spec/permissions/admin/permissions_spec.rb +++ b/spec/permissions/admin/permissions_spec.rb @@ -72,12 +72,12 @@ module Decidim::DecidimAwesome::Admin allow(Decidim::DecidimAwesome.config).to receive(feature).and_return(status) end - it { is_expected.to eq true } + it { is_expected.to be true } context "when admin_accountability is disabled" do let(:status) { :disabled } - it { is_expected.to eq false } + it { is_expected.to be false } end end end diff --git a/spec/presenters/paper_trail_role_presenter_spec.rb b/spec/presenters/paper_trail_role_presenter_spec.rb new file mode 100644 index 000000000..92bde63ff --- /dev/null +++ b/spec/presenters/paper_trail_role_presenter_spec.rb @@ -0,0 +1,204 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::DecidimAwesome + describe PaperTrailRolePresenter, type: :helper do + let(:user) { create :user, organization: organization } + let(:organization) { create :organization } + let(:participatory_space) { create(:participatory_process, organization: organization) } + let(:role) { "admin" } + let(:participatory_process_user_role) { create(:participatory_process_user_role, role: role, participatory_process: participatory_space, user: user) } + let(:destroyed_at) { 2.days.ago } + let(:changes_create) do + { + "decidim_user_id" => [nil, user.id], + "decidim_participatory_process_id" => [nil, participatory_space.id], + "role" => [nil, role] + } + end + let(:changes_destroy) do + { + "decidim_user_id" => [user.id, nil], + "decidim_participatory_process_id" => [participatory_space.id, nil], + "role" => [role, nil] + } + end + let!(:entry) do + create(:paper_trail_version, item: participatory_process_user_role, + created_at: 1.week.ago, + event: "create") + end + + let(:action_log) { create(:action_log, organization: organization, resource_id: entry.reload.changeset["decidim_user_id"].last, action: "create") } + let(:html) { true } + + subject { described_class.new(entry, html: html) } + + before do + allow(entry).to receive(:changeset).and_return(changes_create) + end + + shared_context "with role destroyed" do + let!(:destroy_entry) do + create(:paper_trail_version, item: participatory_process_user_role, + created_at: destroyed_at, + event: "destroy") + end + + before do + allow(destroy_entry).to receive(:changeset).and_return(changes_destroy) + end + end + + describe "#role" do + it "returns the role" do + expect(subject.role).to eq("admin") + end + + it "returns the role name" do + expect(subject.role_name).to eq("Administrator") + end + + it "returns the role class" do + expect(subject.role_class).to eq("text-alert") + end + + context "when role is a valuator" do + let(:role) { "valuator" } + + it "returns the role name" do + expect(subject.role_name).to eq("Valuator") + end + + it "returns the role class" do + expect(subject.role_class).to eq("text-secondary") + end + end + + context "when role is a collaborator" do + let(:role) { "collaborator" } + + it "returns the role name" do + expect(subject.role_name).to eq("Collaborator") + end + + it "returns the role class" do + expect(subject.role_class).to be_blank + end + end + end + + describe "#removal_date" do + it "returns currently active" do + expect(subject.removal_date).to eq("Currently active") + end + + context "when html is disabled" do + let(:html) { false } + + it "returns currently active" do + expect(subject.removal_date).to eq("Currently active") + end + end + + context "when the role was removed" do + include_context "with role destroyed" + + it "returns the removal date" do + expect(subject.removal_date).to eq(destroyed_at.strftime("%d/%m/%Y %H:%M")) + end + end + end + + describe "#participatory_space_name" do + it "returns the participatory space name" do + expect(subject.participatory_space_name).to include("Processes") + end + end + + describe "#participatory_space_type" do + it "returns the participatory space type" do + expect(subject.participatory_space_type).to eq("Processes") + end + end + + describe "#participatory_space_path" do + it "returns the path to user roles" do + expect(subject.participatory_space_path).to eq("/admin/participatory_processes/#{participatory_space.slug}/user_roles") + end + + context "when role is destroyed" do + include_context "with role destroyed" + + it "returns the path to user roles" do + expect(subject.participatory_space_path).to eq("/admin/participatory_processes/#{participatory_space.slug}/user_roles") + end + end + + # rubocop:disable RSpec/AnyInstance + context "when no user roles route exist" do + before do + allow_any_instance_of(Decidim::EngineRouter).to receive(:participatory_process_user_roles_path).and_raise(NoMethodError) + end + + it "returns the path to user roles" do + expect(subject.participatory_space_path.split("?").first).to eq("/admin/participatory_processes/#{participatory_space.slug}") + end + + context "when no participatory_space route exist" do + before do + allow_any_instance_of(Decidim::EngineRouter).to receive(:participatory_process_path).and_raise(NoMethodError) + end + + it "returns empty" do + expect(subject.participatory_space_path).to be_blank + end + end + end + # rubocop:enable RSpec/AnyInstance + end + + describe "#created_date" do + it "returns the creation date" do + expect(subject.created_date).to eq(entry.created_at.strftime("%d/%m/%Y %H:%M")) + end + + context "when date is missing" do + let(:entry) { nil } + + it "returns the creation date" do + expect(subject.created_date).to eq("") + end + end + end + + describe "#last_sign_in_date" do + it "returns never logged in yet" do + expect(subject.last_sign_in_date).to eq("Never logged yet") + end + + context "when no html" do + let(:html) { false } + + it "returns never logged in yet" do + expect(subject.last_sign_in_date).to eq("Never logged yet") + end + end + + context "when user has logged before" do + let(:user) { create :user, organization: organization, last_sign_in_at: 1.day.ago } + + it "returns the last sign in date" do + expect(subject.last_sign_in_date).to eq(1.day.ago.strftime("%d/%m/%Y %H:%M")) + end + end + end + + describe "#user" do + it "returns the user" do + expect(subject.user).to eq(user) + end + end + end +end diff --git a/spec/system/admin/admin_accountability_spec.rb b/spec/system/admin/admin_accountability_spec.rb index 6cc450b50..44e9884a9 100644 --- a/spec/system/admin/admin_accountability_spec.rb +++ b/spec/system/admin/admin_accountability_spec.rb @@ -3,14 +3,23 @@ require "spec_helper" describe "Admin accountability", type: :system do + let(:user_creation_date) { 7.days.ago } + let(:login_date) { 6.days.ago } let(:organization) { create :organization } - let!(:user) { create :user, :admin, :confirmed, organization: organization } + let!(:admin) { create :user, :admin, :confirmed, organization: organization } + + let(:administrator) { create(:user, organization: organization, last_sign_in_at: login_date, created_at: user_creation_date) } + let(:valuator) { create(:user, organization: organization, created_at: user_creation_date) } + let(:collaborator) { create(:user, organization: organization, created_at: user_creation_date) } + let(:moderator) { create(:user, organization: organization, created_at: user_creation_date) } + let(:participatory_process) { create(:participatory_process, organization: organization) } + let(:status) { true } before do allow(Decidim::DecidimAwesome.config).to receive(:allow_admin_accountability).and_return(status) switch_to_host(organization.host) - login_as user, scope: :user + login_as admin, scope: :user visit decidim_admin.root_path end @@ -32,4 +41,146 @@ expect(page).not_to have_content("Admin accountability") end end + + describe "admin action list" do + context "when there are admin actions" do + before do + create(:participatory_process_user_role, user: administrator, participatory_process: participatory_process, role: "admin", created_at: 4.days.ago) + create(:participatory_process_user_role, user: valuator, participatory_process: participatory_process, role: "valuator", created_at: 3.days.ago) + create(:participatory_process_user_role, user: collaborator, participatory_process: participatory_process, role: "collaborator", created_at: 2.days.ago) + create(:participatory_process_user_role, user: moderator, participatory_process: participatory_process, role: "moderator", created_at: 1.day.ago) + + Decidim::ParticipatoryProcessUserRole.find_by(user: collaborator).destroy + + click_link "Participants" + click_link "Admin accountability" + end + + it "shows the correct information for each user", versioning: true do + expect(page).to have_link("Processes > #{participatory_process.title["en"]}", + href: "/admin/participatory_processes/#{participatory_process.slug}/user_roles", count: 4) + + within all("table tr")[1] do + expect(page).to have_content("Moderator") + expect(page).to have_content(moderator.name) + expect(page).to have_content(moderator.email) + expect(page).to have_content(1.day.ago.strftime("%d/%m/%Y %H:%M")) + expect(page).to have_content("Currently active") + expect(page).not_to have_content(login_date.strftime("%d/%m/%Y %H:%M")) + expect(page).to have_content("Never logged yet") + expect(page).not_to have_content(Time.current.strftime("%d/%m/%Y %H:%M")) + end + + within all("table tr")[2] do + expect(page).to have_content("Collaborator") + expect(page).to have_content(collaborator.name) + expect(page).to have_content(collaborator.email) + expect(page).to have_content(2.days.ago.strftime("%d/%m/%Y %H:%M")) + expect(page).not_to have_content(login_date.strftime("%d/%m/%Y %H:%M")) + expect(page).to have_content("Never logged yet") + expect(page).to have_content(Time.current.strftime("%d/%m/%Y %H:%M")) + expect(page).not_to have_content("Currently active") + end + + within all("table tr")[3] do + expect(page).to have_content("Valuator") + expect(page).to have_content(valuator.name) + expect(page).to have_content(valuator.email) + expect(page).to have_content(3.days.ago.strftime("%d/%m/%Y %H:%M")) + expect(page).to have_content("Currently active") + expect(page).not_to have_content(login_date.strftime("%d/%m/%Y %H:%M")) + expect(page).to have_content("Never logged yet") + expect(page).not_to have_content(Time.current.strftime("%d/%m/%Y %H:%M")) + end + + within all("table tr")[4] do + expect(page).to have_content("Administrator") + expect(page).to have_content(administrator.name) + expect(page).to have_content(administrator.email) + expect(page).to have_content(4.days.ago.strftime("%d/%m/%Y %H:%M")) + expect(page).to have_content("Currently active") + expect(page).to have_content(login_date.strftime("%d/%m/%Y %H:%M")) + expect(page).not_to have_content("Never logged yet") + expect(page).not_to have_content(Time.current.strftime("%d/%m/%Y %H:%M")) + end + end + end + + context "when there are multiple assignations for the same user" do + before do + create(:participatory_process_user_role, user: collaborator, participatory_process: participatory_process, role: "collaborator", created_at: 3.days.ago) + + Decidim::ParticipatoryProcessUserRole.find_by(user: collaborator).destroy + + create(:participatory_process_user_role, user: collaborator, participatory_process: participatory_process, role: "valuator", created_at: 2.days.ago) + + Decidim::ParticipatoryProcessUserRole.find_by(user: collaborator).destroy + + create(:participatory_process_user_role, user: collaborator, participatory_process: participatory_process, role: "collaborator", created_at: 1.day.ago) + + click_link "Participants" + click_link "Admin accountability" + end + + it "shows currently active", versioning: true do + within all("table tr")[1] do + expect(page).to have_content("Collaborator") + expect(page).to have_content(collaborator.name) + expect(page).to have_content(collaborator.email) + expect(page).to have_content(1.day.ago.strftime("%d/%m/%Y %H:%M")) + expect(page).not_to have_content(Time.current.strftime("%d/%m/%Y %H:%M")) + expect(page).to have_content("Currently active") + end + + within all("table tr")[2] do + expect(page).to have_content("Valuator") + expect(page).to have_content(collaborator.name) + expect(page).to have_content(collaborator.email) + expect(page).to have_content(2.days.ago.strftime("%d/%m/%Y %H:%M")) + expect(page).to have_content(Time.current.strftime("%d/%m/%Y %H:%M")) + expect(page).not_to have_content("Currently active") + end + + within all("table tr")[3] do + expect(page).to have_content("Collaborator") + expect(page).to have_content(collaborator.name) + expect(page).to have_content(collaborator.email) + expect(page).to have_content(3.days.ago.strftime("%d/%m/%Y %H:%M")) + expect(page).to have_content(Time.current.strftime("%d/%m/%Y %H:%M")) + expect(page).not_to have_content("Currently active") + end + end + end + + context "when user listed has been removed" do + let(:valuator) { create(:user, :deleted, organization: organization, created_at: user_creation_date) } + + before do + create(:participatory_process_user_role, user: collaborator, participatory_process: participatory_process, role: "collaborator", created_at: 3.days.ago) + collaborator.destroy + create(:participatory_process_user_role, user: valuator, participatory_process: participatory_process, role: "valuator", created_at: 2.days.ago) + + click_link "Participants" + click_link "Admin accountability" + end + + it "shows the user as removed", versioning: true do + within all("table tr")[1] do + expect(page).to have_content("Valuator") + expect(page).to have_content("Deleted user") + expect(page).to have_content(2.days.ago.strftime("%d/%m/%Y %H:%M")) + expect(page).not_to have_content(Time.current.strftime("%d/%m/%Y %H:%M")) + expect(page).to have_content("Currently active") + end + + within all("table tr")[2] do + expect(page).to have_content("Collaborator") + expect(page).to have_content("User not in the database") + expect(page).to have_content(3.days.ago.strftime("%d/%m/%Y %H:%M")) + expect(page).not_to have_content(Time.current.strftime("%d/%m/%Y %H:%M")) + expect(page).to have_content("Currently active") + end + end + end + end end diff --git a/spec/system/admin/scoped_admins_spec.rb b/spec/system/admin/scoped_admins_spec.rb index d5ce63aa5..0d11f2655 100644 --- a/spec/system/admin/scoped_admins_spec.rb +++ b/spec/system/admin/scoped_admins_spec.rb @@ -83,9 +83,7 @@ expect(page).to have_content("Agree to the terms and conditions of use") - agree_to_terms = "I agree with the following terms" - agree_to_terms = "I agree with the terms" if legacy_version? - click_button agree_to_terms + click_button "I agree with the terms" expect(page).to have_content(welcome_text) expect(page).not_to have_content("Review them now") From d5bccc9ccc267dc907bb36c9eb493f5fe7858662 Mon Sep 17 00:00:00 2001 From: Anna Topalidi <60363870+antopalidi@users.noreply.github.com> Date: Fri, 17 Feb 2023 14:31:02 +0100 Subject: [PATCH 04/18] Search and filtering (#8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add Filterable * fix searching and filtering * add filter by date * add rspec * change ransacker method name * change locale * fix test * fix filter by date examples --------- Co-authored-by: Ivan Vergés --- Gemfile.lock | 3 + .../admin_accountability/admin/filterable.rb | 69 ++++++ .../admin/admin_accountability_controller.rb | 18 +- .../decidim_awesome/paper_trail_version.rb | 56 +++++ .../decidim/admin/shared/_filters.html.erb | 58 +++++ .../admin/admin_accountability/index.html.erb | 1 + config/locales/en.yml | 13 ++ lib/decidim/decidim_awesome/awesome.rb | 2 +- spec/system/admin/filter_admin_action_spec.rb | 211 ++++++++++++++++++ 9 files changed, 426 insertions(+), 5 deletions(-) create mode 100644 app/controllers/concerns/decidim/decidim_awesome/admin_accountability/admin/filterable.rb create mode 100644 app/views/decidim/admin/shared/_filters.html.erb create mode 100644 spec/system/admin/filter_admin_action_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index 62f8543ce..3d780a97e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -510,6 +510,8 @@ GEM net-smtp (0.3.3) net-protocol nio4r (2.5.8) + nokogiri (1.13.10-x86_64-darwin) + racc (~> 1.4) nokogiri (1.13.10-x86_64-linux) racc (~> 1.4) oauth (1.1.0) @@ -796,6 +798,7 @@ GEM zeitwerk (2.6.6) PLATFORMS + x86_64-darwin-22 x86_64-linux DEPENDENCIES diff --git a/app/controllers/concerns/decidim/decidim_awesome/admin_accountability/admin/filterable.rb b/app/controllers/concerns/decidim/decidim_awesome/admin_accountability/admin/filterable.rb new file mode 100644 index 000000000..94a0df4a6 --- /dev/null +++ b/app/controllers/concerns/decidim/decidim_awesome/admin_accountability/admin/filterable.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require "active_support/concern" + +module Decidim + module DecidimAwesome + module AdminAccountability + module Admin + module Filterable + extend ActiveSupport::Concern + + included do + include Decidim::Admin::Filterable + + private + + def base_query + collection + end + + def filters + [ + :role_type_eq, + :participatory_space_type_eq + ] + end + + def filters_with_values + { + role_type_eq: role_types, + participatory_space_type_eq: participatory_space_types + } + end + + def dynamically_translated_filters + [:role_type_eq, :participatory_space_type_eq] + end + + def translated_role_type_eq(role) + admin_action = collection.find { |action| action.item && action.item[:role] == role } + admin_action ? translated_attribute(admin_action.item[:role]) : nil + end + + def translated_participatory_space_type_eq(item_type) + admin_action = collection.find { |action| action.item_type == item_type } + admin_action ? translated_attribute(admin_action.item_type.demodulize.gsub("UserRole", "")) : nil + end + + def search_field_predicate + :user_name_or_user_email_cont + end + + def extra_allowed_params + [:per_page] + end + + def participatory_space_types + collection.pluck(:item_type).uniq.sort + end + + def role_types + collection.map { |admin_action| admin_action.item&.[](:role) }.compact.uniq.sort + end + end + end + end + end + end +end diff --git a/app/controllers/decidim/decidim_awesome/admin/admin_accountability_controller.rb b/app/controllers/decidim/decidim_awesome/admin/admin_accountability_controller.rb index 0c19b9ec6..bc09ee0f5 100644 --- a/app/controllers/decidim/decidim_awesome/admin/admin_accountability_controller.rb +++ b/app/controllers/decidim/decidim_awesome/admin/admin_accountability_controller.rb @@ -5,9 +5,9 @@ module DecidimAwesome module Admin class AdminAccountabilityController < DecidimAwesome::Admin::ApplicationController include NeedsAwesomeConfig - include Decidim::Admin::Filterable + include Decidim::DecidimAwesome::AdminAccountability::Admin::Filterable - helper_method :admin_actions + helper_method :admin_actions, :admin_action, :collection layout "decidim/admin/users" @@ -15,7 +15,9 @@ class AdminAccountabilityController < DecidimAwesome::Admin::ApplicationControll enforce_permission_to :edit_config, :allow_admin_accountability end - def index; end + def index + @render_date_fields = true + end def export # TODO: export to xls, csv @@ -24,7 +26,15 @@ def export private def admin_actions - @admin_actions ||= paginate(PaperTrailVersion.role_actions) + @admin_actions ||= filtered_collection + end + + def collection + @collection ||= paginate(PaperTrailVersion.role_actions) + end + + def admin_action + @admin_action ||= collection.find(params[:id]) end end end diff --git a/app/models/decidim/decidim_awesome/paper_trail_version.rb b/app/models/decidim/decidim_awesome/paper_trail_version.rb index b8a438931..52f015ac1 100644 --- a/app/models/decidim/decidim_awesome/paper_trail_version.rb +++ b/app/models/decidim/decidim_awesome/paper_trail_version.rb @@ -13,6 +13,62 @@ def present self end end + + ransacker :role_type do + Arel.sql(%{ + ( + SELECT cast("decidim_assembly_user_roles"."role" as text) FROM "decidim_assembly_user_roles" + WHERE "decidim_assembly_user_roles"."id" = "versions"."item_id" + AND item_type = 'Decidim::AssemblyUserRole' + UNION + SELECT cast("decidim_participatory_process_user_roles"."role" as text) FROM decidim_participatory_process_user_roles + WHERE decidim_participatory_process_user_roles.id = versions.item_id + AND item_type = 'Decidim::ParticipatoryProcessUserRole' + ) + }) + end + + ransacker :participatory_space_type do + Arel.sql(%{ (cast("item_type" as text))}) + end + + ransacker :user_email do + query = <<-SQL.squish + ( + SELECT decidim_users.email FROM decidim_users + JOIN decidim_assembly_user_roles ON decidim_users.id = decidim_assembly_user_roles.decidim_user_id + WHERE decidim_assembly_user_roles.id = versions.item_id + AND item_type = 'Decidim::AssemblyUserRole' + UNION + SELECT decidim_users.email FROM decidim_users + JOIN decidim_participatory_process_user_roles ON decidim_users.id = decidim_participatory_process_user_roles.decidim_user_id + WHERE decidim_participatory_process_user_roles.id = versions.item_id + AND item_type = 'Decidim::ParticipatoryProcessUserRole' + ) + SQL + Arel.sql(query) + end + + ransacker :user_name do + query = <<-SQL.squish + ( + SELECT decidim_users.name FROM decidim_users + JOIN decidim_assembly_user_roles ON decidim_users.id = decidim_assembly_user_roles.decidim_user_id + WHERE decidim_assembly_user_roles.id = versions.item_id + AND item_type = 'Decidim::AssemblyUserRole' + UNION + SELECT decidim_users.name FROM decidim_users + JOIN decidim_participatory_process_user_roles ON decidim_users.id = decidim_participatory_process_user_roles.decidim_user_id + WHERE decidim_participatory_process_user_roles.id = versions.item_id + AND item_type = 'Decidim::ParticipatoryProcessUserRole' + ) + SQL + Arel.sql(query) + end + + ransacker :created_at, type: :date do + Arel.sql("date(created_at)") + end end end end diff --git a/app/views/decidim/admin/shared/_filters.html.erb b/app/views/decidim/admin/shared/_filters.html.erb new file mode 100644 index 000000000..bde77f996 --- /dev/null +++ b/app/views/decidim/admin/shared/_filters.html.erb @@ -0,0 +1,58 @@ +
+
+ +
+ +
+
+
+ <%= applied_filters_tags(i18n_ctx) %> +
+
+ <%= admin_filters_pagination %> +
+
diff --git a/app/views/decidim/decidim_awesome/admin/admin_accountability/index.html.erb b/app/views/decidim/decidim_awesome/admin/admin_accountability/index.html.erb index 61f12cebd..7d17f3a34 100644 --- a/app/views/decidim/decidim_awesome/admin/admin_accountability/index.html.erb +++ b/app/views/decidim/decidim_awesome/admin/admin_accountability/index.html.erb @@ -7,6 +7,7 @@ <%= admin_filters_pagination %>
+ <%= admin_filter_selector(:admin_actions) %>

<%= t(".description") %>

diff --git a/config/locales/en.yml b/config/locales/en.yml index e2bcf85c3..4fa878a41 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -59,6 +59,19 @@ en: proposal_custom_fields: Custom fields decidim: admin: + filters: + admin_actions: + participatory_space_type_eq: + label: Participatory space type + role: + label: Role + role_type_eq: + label: Role type + label: + search_field_end_date: "To date:" + search_field_start_date: "From date:" + search_placeholder: + user_name_or_user_email_cont: Search by user name, email menu: admin_accountability: Admin accountability decidim_awesome: Decidim awesome diff --git a/lib/decidim/decidim_awesome/awesome.rb b/lib/decidim/decidim_awesome/awesome.rb index b02248765..c5b3200da 100644 --- a/lib/decidim/decidim_awesome/awesome.rb +++ b/lib/decidim/decidim_awesome/awesome.rb @@ -229,7 +229,7 @@ module DecidimAwesome # Roles for which it is necessary to show admin_accountability config_accessor :admin_user_roles do - %w(Decidim::AssemblyUserRole Decidim::ParticipatoryProcessUserRole Decidim::ConferencesUserRole) + %w(Decidim::AssemblyUserRole Decidim::ParticipatoryProcessUserRole) end # diff --git a/spec/system/admin/filter_admin_action_spec.rb b/spec/system/admin/filter_admin_action_spec.rb new file mode 100644 index 000000000..522d3dc02 --- /dev/null +++ b/spec/system/admin/filter_admin_action_spec.rb @@ -0,0 +1,211 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Filter Admin actions", type: :system do + let(:user_creation_date) { 7.days.ago } + let(:login_date) { 6.days.ago } + let(:organization) { create :organization } + let!(:admin) { create :user, :admin, :confirmed, organization: organization } + let!(:resource_controller) { Decidim::DecidimAwesome::Admin::AdminAccountabilityController } + let(:administrator) { create(:user, organization: organization, last_sign_in_at: login_date, created_at: user_creation_date) } + let(:valuator) { create(:user, name: "Lorry", email: "test@example.org", organization: organization, created_at: user_creation_date) } + let(:collaborator) { create(:user, organization: organization, created_at: user_creation_date) } + let(:moderator) { create(:user, organization: organization, created_at: user_creation_date) } + let(:participatory_process) { create(:participatory_process, organization: organization) } + + let(:status) { true } + + include_context "with filterable context" + + before do + allow(Decidim::DecidimAwesome.config).to receive(:allow_admin_accountability).and_return(status) + switch_to_host(organization.host) + login_as admin, scope: :user + + visit decidim_admin.root_path + end + + describe "admin action list" do + context "when there are admin actions" do + let!(:participatory_process_user_role1) { create(:participatory_process_user_role, user: administrator, role: "admin", created_at: 4.days.ago) } + let!(:participatory_process_user_role2) { create(:participatory_process_user_role, user: valuator, role: "valuator", created_at: 3.days.ago) } + let!(:participatory_process_user_role3) { create(:participatory_process_user_role, user: collaborator, role: "collaborator", created_at: 2.days.ago) } + let!(:participatory_process_user_role4) { create(:participatory_process_user_role, user: moderator, role: "moderator", created_at: 1.day.ago) } + let!(:assembly_user_role1) { create(:assembly_user_role, user: administrator, role: "admin", created_at: 4.days.ago) } + let!(:assembly_user_role2) { create(:assembly_user_role, user: valuator, role: "valuator", created_at: 3.days.ago) } + let!(:assembly_user_role3) { create(:assembly_user_role, user: collaborator, role: "collaborator", created_at: 2.days.ago) } + let!(:assembly_user_role4) { create(:assembly_user_role, user: moderator, role: "moderator", created_at: 1.day.ago) } + + before do + # ensure papertrail has the same created_at date as the object being mocked + Decidim::DecidimAwesome::PaperTrailVersion.role_actions.map { |v| v.update(created_at: v.item.created_at) } + + click_link "Participants" + click_link "Admin accountability" + end + + it "shows filters", versioning: true do + expect(page).to have_content("Filter") + expect(page).to have_css("#q_user_name_or_user_email_cont") + expect(page).to have_css("#q_created_at_gteq") + expect(page).to have_css("#q_created_at_lteq") + end + + it "displays the filter labels", versioning: true do + find("a.dropdown").hover + expect(page).to have_content("Participatory space type") + expect(page).to have_content("Role type") + + find("a", text: "Participatory space type").hover + expect(page).to have_content("Process") + expect(page).to have_content("Assembly") + + find("a", text: "Role type").hover + expect(page).to have_content("admin") + expect(page).to have_content("collaborator") + expect(page).to have_content("moderator") + expect(page).to have_content("valuator") + end + + context "when filtering admin_actions by PARTICIPATORY SPACE" do + it "Assemblies space type", versioning: true do + apply_filter("Participatory space type", "Assembly") + + within "tbody" do + expect(page).to have_content("Assemblies >", count: 4) + end + end + + it "Processes space type", versioning: true do + apply_filter("Participatory space type", "Process") + + within "tbody" do + expect(page).to have_content("Processes >", count: 4) + end + end + end + + context "when filtering admin_actions by ROLE TYPE" do + it "Admin role type", versioning: true do + apply_filter("Role type", "admin") + + within "tbody" do + expect(page).to have_content("Administrator", count: 2) + end + end + + it "Collaborator role type", versioning: true do + apply_filter("Role type", "collaborator") + + within "tbody" do + expect(page).to have_content("Collaborator", count: 2) + end + end + + it "Moderator role type", versioning: true do + apply_filter("Role type", "moderator") + + within "tbody" do + expect(page).to have_content("Moderator", count: 2) + end + end + + it "Valuator role type", versioning: true do + apply_filter("Role type", "valuator") + + within "tbody" do + expect(page).to have_content("Valuator", count: 2) + end + end + end + + context "when searching by name or email" do + it "searches by name", versioning: true do + search_by_text("Lorry") + + within "tbody" do + expect(page).to have_content("Lorry", count: 2) + end + end + + it "searches by email", versioning: true do + search_by_text("test@example.org") + + within "tbody" do + expect(page).to have_content("test@example.org", count: 2) + end + end + end + + context "when searching by date" do + def search_by_date(start_date, end_date) + within(".filters__section") do + fill_in(:q_created_at_gteq, with: start_date) if start_date.present? + fill_in(:q_created_at_lteq, with: end_date) if end_date.present? + + find("*[type=submit]").click + end + end + + context "when the start date is earlier" do + it "displays all entries", versioning: true do + search_by_date(6.days.ago, "") + + within "tbody" do + expect(page).to have_css("tr", count: 8) + end + end + end + + context "when the start date is later" do + it "displays no entries", versioning: true do + search_by_date(1.day.from_now, "") + + within "tbody" do + expect(page).to have_css("tr", count: 0) + end + end + end + + context "when the end date is later" do + it "displays all entries", versioning: true do + search_by_date("", 5.days.from_now) + + within "tbody" do + expect(page).to have_css("tr", count: 8) + end + end + end + + context "when the end date is earlier" do + it "displays no entries", versioning: true do + search_by_date("", 6.days.ago) + + within "tbody" do + expect(page).to have_css("tr", count: 0) + end + end + end + + context "when searching in range" do + it "displays entries in range", versioning: true do + search_by_date(3.days.ago, 2.days.ago) + + within "tbody" do + expect(page).to have_css("tr", count: 4) + expect(page).to have_content("Collaborator", count: 2) + expect(page).to have_content("Valuator", count: 2) + expect(page).to have_content(collaborator.name, count: 2) + expect(page).to have_content(valuator.name, count: 2) + expect(page).to have_content(participatory_process_user_role2.participatory_space.title["en"]) + expect(page).to have_content(participatory_process_user_role3.participatory_space.title["en"]) + expect(page).to have_content(assembly_user_role2.participatory_space.title["en"]) + expect(page).to have_content(assembly_user_role3.participatory_space.title["en"]) + end + end + end + end + end + end +end From 61631134fcc95782bd50c5e388df313d78dc26cb Mon Sep 17 00:00:00 2001 From: Anna Topalidi <60363870+antopalidi@users.noreply.github.com> Date: Sat, 18 Feb 2023 14:01:26 +0100 Subject: [PATCH 05/18] Export excel/csv (#9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add export of admin_actions * restore Gemfile.lock * update gemfile * add reference decidim admin for locales * commit used Decidim styles * restore envs * remove default * add tests --------- Co-authored-by: Ivan Vergés --- .eslintrc.json | 248 +- .github/workflows/tests-legacy.yml | 2 + .github/workflows/tests.yml | 2 + Gemfile.lock | 30 +- .../admin/admin_accountability_controller.rb | 13 +- .../export_admin_actions_job.rb | 28 + .../decidim_awesome/paper_trail_version.rb | 4 +- .../decidim_admin_decidim_awesome.js | 5 +- .../decidim_awesome/admin/auto_edit.js | 14 +- .../admin/check_redirections.js | 4 +- .../decidim_awesome/admin/constraints.js | 10 +- .../admin/custom_fields_builder.js | 21 +- .../decidim_awesome/admin/form_exit_warn.js | 1 + .../decidim_awesome/admin/user_picker.js | 1 + .../awesome_map/api/fetcher.js | 26 +- .../awesome_map/awesome_map.js | 26 +- .../awesome_map/controllers/controller.js | 19 +- .../controllers/proposals_controller.js | 6 +- .../awesome_map/controls_ui.js | 51 +- .../decidim_awesome/awesome_map/load_map.js | 1 + .../decidim/decidim_awesome/editors/editor.js | 14 +- .../decidim/decidim_awesome/forms/autosave.js | 20 +- .../forms/custom_fields_renderer.js | 63 +- .../decidim_awesome/forms/rich_text_plugin.js | 10 +- .../proposals/custom_fields.js | 14 +- .../decidim_awesome/proposals/images.js | 4 +- .../paper_trail_version_serializer.rb | 37 + .../admin/admin_accountability/index.html.erb | 20 +- babel.config.json | 28 + config/i18n-tasks.yml | 1 + config/locales/en.yml | 13 +- package-lock.json | 12152 +++++++++++----- package.json | 177 +- spec/jobs/export_admin_actions_job_spec.rb | 91 + spec/system/admin/filter_admin_action_spec.rb | 264 +- 35 files changed, 9196 insertions(+), 4224 deletions(-) create mode 100644 app/jobs/decidim/decidim_awesome/export_admin_actions_job.rb create mode 100644 app/serializers/decidim/decidim_awesome/paper_trail_version_serializer.rb create mode 100644 babel.config.json create mode 100644 spec/jobs/export_admin_actions_job_spec.rb diff --git a/.eslintrc.json b/.eslintrc.json index 8b788aade..88007db2f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,247 +1,3 @@ { - "env": { - "browser": true, - "es2021": true - }, - "extends": [ - "eslint:recommended", - "prettier" - ], - "parserOptions": { - "ecmaVersion": 12, - "sourceType": "module" - }, - "globals": { - "require": false, - "$": false, - "jQuery": false, - "L": false, - "ApiFetcher": false, - "FormStorage": false, - "Quill": false, - "InscrybMDE": false, - "CodeMirror": false, - "Europa": false, - "DecidimAwesome": false, - "inlineAttachment": false, - "CustomFieldsBuilder": false - }, - "rules": { - "accessor-pairs": "error", - "array-bracket-spacing": "error", - "array-callback-return": "error", - "arrow-body-style": "off", - "arrow-parens": [ - "error", - "always" - ], - "arrow-spacing": [ - "error", - { - "after": true, - "before": true - } - ], - "block-scoped-var": "error", - "block-spacing": "off", - "callback-return": "error", - "class-methods-use-this": "off", - "comma-dangle": ["error", "never"], - "comma-spacing": [ - "error", - { - "after": true, - "before": false - } - ], - "comma-style": [ - "error", - "last" - ], - "complexity": "error", - "computed-property-spacing": [ - "error", - "never" - ], - "consistent-this": "error", - "curly": "error", - "default-case": "error", - "dot-location": "error", - "dot-notation": "error", - "eol-last": "error", - "func-call-spacing": "error", - "func-name-matching": "error", - "func-names": [ - "error", - "never" - ], - "generator-star-spacing": "error", - "global-require": "error", - "handle-callback-err": "error", - "id-blacklist": "error", - "id-match": "error", - "indent": ["error", 2, { "VariableDeclarator": 2 }], - "jsx-quotes": "error", - "key-spacing": "error", - "keyword-spacing": [ - "error", - { - "after": true, - "before": true - } - ], - "linebreak-style": [ - "error", - "unix" - ], - "lines-around-comment": "error", - "lines-around-directive": "error", - "max-len": "off", - "max-lines": "error", - "max-nested-callbacks": "error", - "max-params": "error", - "max-statements": "off", - "max-statements-per-line": "error", - "multiline-ternary": "error", - "new-cap": "error", - "new-parens": "error", - "newline-after-var": "off", - "newline-before-return": "off", - "newline-per-chained-call": "off", - "no-alert": "error", - "no-array-constructor": "error", - "no-bitwise": "error", - "no-caller": "error", - "no-catch-shadow": "error", - "no-confusing-arrow": "error", - "no-continue": "error", - "no-div-regex": "error", - "no-duplicate-imports": "error", - "no-else-return": "error", - "no-eq-null": "error", - "no-eval": "error", - "no-extend-native": "error", - "no-extra-bind": "error", - "no-extra-label": "error", - "no-extra-parens": "off", - "no-floating-decimal": "error", - "no-implicit-coercion": "error", - "no-implicit-globals": "error", - "no-implied-eval": "error", - "no-iterator": "error", - "no-label-var": "error", - "no-labels": "error", - "no-lone-blocks": "error", - "no-lonely-if": "error", - "no-magic-numbers": "off", - "no-mixed-requires": "error", - "no-multi-spaces": "off", - "no-multi-str": "error", - "no-multiple-empty-lines": "error", - "no-native-reassign": "error", - "no-negated-condition": "error", - "no-negated-in-lhs": "error", - "no-nested-ternary": "error", - "no-new": "error", - "no-new-func": "error", - "no-new-object": "error", - "no-new-require": "error", - "no-new-wrappers": "error", - "no-octal-escape": "error", - "no-path-concat": "error", - "no-plusplus": "error", - "no-process-env": "error", - "no-process-exit": "error", - "no-proto": "error", - "no-prototype-builtins": "error", - "no-restricted-globals": "error", - "no-restricted-imports": "error", - "no-restricted-modules": "error", - "no-restricted-properties": "error", - "no-restricted-syntax": "error", - "no-return-assign": "error", - "no-script-url": "error", - "no-self-compare": "error", - "no-sequences": "error", - "no-shadow": "error", - "no-shadow-restricted-names": "error", - "no-spaced-func": "error", - "no-sync": "error", - "no-tabs": "error", - "no-template-curly-in-string": "error", - "no-throw-literal": "error", - "no-trailing-spaces": "off", - "no-undef-init": "error", - "no-undefined": "error", - "no-underscore-dangle": "off", - "no-unmodified-loop-condition": "error", - "no-unneeded-ternary": "error", - "no-unused-expressions": "error", - "no-use-before-define": "error", - "no-useless-call": "error", - "no-useless-computed-key": "error", - "no-useless-concat": "error", - "no-useless-constructor": "error", - "no-useless-escape": "error", - "no-useless-rename": "error", - "no-var": "error", - "no-void": "error", - "no-warning-comments": "error", - "no-whitespace-before-property": "error", - "no-with": "error", - "object-curly-newline": "off", - "object-curly-spacing": "off", - "object-property-newline": [ - "error", - { - "allowMultiplePropertiesPerLine": true - } - ], - "object-shorthand": "off", - "one-var": "off", - "one-var-declaration-per-line": "error", - "operator-assignment": "error", - "operator-linebreak": "error", - "padded-blocks": "off", - "prefer-arrow-callback": "off", - "prefer-const": "off", - "prefer-numeric-literals": "error", - "prefer-rest-params": "error", - "prefer-spread": "error", - "prefer-template": "error", - "quote-props": "off", - "quotes": ["error", "double", { "avoidEscape": true }], - "require-jsdoc": "error", - "rest-spread-spacing": "error", - "semi": "off", - "semi-spacing": "error", - "sort-imports": "off", - "sort-keys": "off", - "space-before-blocks": "error", - "space-before-function-paren": "off", - "space-in-parens": [ - "error", - "never" - ], - "space-infix-ops": "error", - "space-unary-ops": "error", - "spaced-comment": [ - "error", - "always" - ], - "strict": "error", - "symbol-description": "error", - "template-curly-spacing": "error", - "unicode-bom": [ - "error", - "never" - ], - "vars-on-top": "error", - "wrap-iife": "error", - "wrap-regex": "error", - "yield-star-spacing": "error", - "yoda": "error", - "import/no-named-as-default": "off", - "import/no-extraneous-dependencies": "off" - } -} + "extends": "@decidim" +} \ No newline at end of file diff --git a/.github/workflows/tests-legacy.yml b/.github/workflows/tests-legacy.yml index b866ba1f0..efa888124 100644 --- a/.github/workflows/tests-legacy.yml +++ b/.github/workflows/tests-legacy.yml @@ -60,6 +60,8 @@ jobs: run: bundle exec rspec spec/${{ matrix.rspec }} env: FEATURES: ${{ matrix.features }} + SIMPLECOV: 1 + CODECOV: 1 - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9ccaf6fe8..f4f356cc1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -60,6 +60,8 @@ jobs: run: bundle exec rspec spec/${{ matrix.rspec }} env: FEATURES: ${{ matrix.features }} + SIMPLECOV: 1 + CODECOV: 1 - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 diff --git a/Gemfile.lock b/Gemfile.lock index 3d780a97e..21cb59032 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -332,13 +332,13 @@ GEM declarative-option (0.1.0) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) - devise (4.8.1) + devise (4.9.0) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-i18n (1.10.2) + devise-i18n (1.10.3) devise (>= 4.8.0) devise_invitable (2.0.7) actionmailer (>= 5.0) @@ -557,7 +557,7 @@ GEM parallel (1.22.1) parallel_tests (3.13.0) parallel - parser (3.2.0.0) + parser (3.2.1.0) ast (~> 2.4.1) pg (1.1.4) pg_search (2.3.6) @@ -629,8 +629,8 @@ GEM rb-inotify (0.10.1) ffi (~> 1.0) redcarpet (3.6.0) - redis (4.8.0) - regexp_parser (2.6.2) + redis (4.8.1) + regexp_parser (2.7.0) request_store (1.5.1) rack (>= 1.4) responders (3.1.0) @@ -677,8 +677,8 @@ GEM rubocop-ast (>= 1.17.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.24.1) - parser (>= 3.1.1.0) + rubocop-ast (1.26.0) + parser (>= 3.2.1.0) rubocop-faker (1.1.0) faker (>= 2.12.0) rubocop (>= 0.82.0) @@ -741,8 +741,8 @@ GEM unicode-display_width (>= 1.1.1, < 3) thor (1.2.1) thread_safe (0.3.6) - tilt (2.0.11) - timeout (0.3.1) + tilt (2.1.0) + timeout (0.3.2) tomlrb (2.0.3) tzinfo (2.0.6) concurrent-ruby (~> 1.0) @@ -768,11 +768,11 @@ GEM rexml (~> 3.2) warden (1.2.9) rack (>= 2.0.9) - web-console (3.7.0) - actionview (>= 5.0) - activemodel (>= 5.0) + web-console (4.2.0) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) bindex (>= 0.4.0) - railties (>= 5.0) + railties (>= 6.0.0) webmock (3.18.1) addressable (>= 2.8.0) crack (>= 0.3.2) @@ -795,7 +795,7 @@ GEM wkhtmltopdf-binary (0.12.6.6) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.6) + zeitwerk (2.6.7) PLATFORMS x86_64-darwin-22 @@ -816,7 +816,7 @@ DEPENDENCIES spring (~> 2.0) spring-watcher-listen (~> 2.0.0) uglifier (~> 4.1) - web-console (~> 3.5) + web-console RUBY VERSION ruby 3.0.5p211 diff --git a/app/controllers/decidim/decidim_awesome/admin/admin_accountability_controller.rb b/app/controllers/decidim/decidim_awesome/admin/admin_accountability_controller.rb index bc09ee0f5..02bc566a3 100644 --- a/app/controllers/decidim/decidim_awesome/admin/admin_accountability_controller.rb +++ b/app/controllers/decidim/decidim_awesome/admin/admin_accountability_controller.rb @@ -7,7 +7,7 @@ class AdminAccountabilityController < DecidimAwesome::Admin::ApplicationControll include NeedsAwesomeConfig include Decidim::DecidimAwesome::AdminAccountability::Admin::Filterable - helper_method :admin_actions, :admin_action, :collection + helper_method :admin_actions, :admin_action, :collection, :export_params layout "decidim/admin/users" @@ -20,7 +20,12 @@ def index end def export - # TODO: export to xls, csv + format = params[:format].to_s + filters = export_params[:q] + + Decidim::DecidimAwesome::ExportAdminActionsJob.perform_later(current_user, format, admin_actions.ransack(filters).result.ids) + + redirect_to decidim_admin_decidim_awesome.admin_accountability_path, notice: t("decidim.decidim_awesome.admin.admin_accountability.exports.notice") end private @@ -36,6 +41,10 @@ def collection def admin_action @admin_action ||= collection.find(params[:id]) end + + def export_params + params.permit(:format, q: [:role_type_eq, :user_name_or_user_email_cont, :created_at_gteq, :created_at_lteq]) + end end end end diff --git a/app/jobs/decidim/decidim_awesome/export_admin_actions_job.rb b/app/jobs/decidim/decidim_awesome/export_admin_actions_job.rb new file mode 100644 index 000000000..6f676556a --- /dev/null +++ b/app/jobs/decidim/decidim_awesome/export_admin_actions_job.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Decidim + module DecidimAwesome + class ExportAdminActionsJob < ApplicationJob + queue_as :default + + def perform(current_user, format, collection_ids) + collection = serialized_collection(collection_ids) + + export_data = Exporters.find_exporter(format).new(collection).export + + ExportMailer.export(current_user, "admin_actions", export_data).deliver_now + end + + private + + def serialized_collection(collection_ids) + @serialized_collection ||= begin + collection = PaperTrailVersion.role_actions.where(id: collection_ids) + collection.map do |item| + PaperTrailVersionSerializer.new(item).serialize + end + end + end + end + end +end diff --git a/app/models/decidim/decidim_awesome/paper_trail_version.rb b/app/models/decidim/decidim_awesome/paper_trail_version.rb index 52f015ac1..785c7d657 100644 --- a/app/models/decidim/decidim_awesome/paper_trail_version.rb +++ b/app/models/decidim/decidim_awesome/paper_trail_version.rb @@ -6,9 +6,9 @@ class PaperTrailVersion < PaperTrail::Version default_scope { order("created_at DESC") } scope :role_actions, -> { where(item_type: ::Decidim::DecidimAwesome.admin_user_roles, event: "create") } - def present + def present(html: true) @present ||= if item_type.in?(Decidim::DecidimAwesome.admin_user_roles) - PaperTrailRolePresenter.new(self) + PaperTrailRolePresenter.new(self, html: html) else self end diff --git a/app/packs/entrypoints/decidim_admin_decidim_awesome.js b/app/packs/entrypoints/decidim_admin_decidim_awesome.js index a14ad8f14..c88a74714 100644 --- a/app/packs/entrypoints/decidim_admin_decidim_awesome.js +++ b/app/packs/entrypoints/decidim_admin_decidim_awesome.js @@ -1,5 +1,6 @@ -import "src/decidim/decidim_awesome/awesome_admin" -import "jquery-ui/ui/widgets/sortable" // This is needed by custom fields builder but if loader there duplicates the jQuery inclusion +import "src/decidim/decidim_awesome/awesome_admin"; +// This is needed by custom fields builder but if loader there duplicates the jQuery inclusion +import "jquery-ui/ui/widgets/sortable"; // CSS import "entrypoints/decidim_admin_decidim_awesome.scss"; diff --git a/app/packs/src/decidim/decidim_awesome/admin/auto_edit.js b/app/packs/src/decidim/decidim_awesome/admin/auto_edit.js index 9824efa76..faa9fc668 100644 --- a/app/packs/src/decidim/decidim_awesome/admin/auto_edit.js +++ b/app/packs/src/decidim/decidim_awesome/admin/auto_edit.js @@ -1,13 +1,13 @@ $(() => { let CustomFieldsBuilders = window.CustomFieldsBuilders || []; - $("body").on("click", "a.awesome-auto-edit", (e) => { - e.preventDefault(); - const $link = $(e.currentTarget); + $("body").on("click", "a.awesome-auto-edit", (ev) => { + ev.preventDefault(); + const $link = $(ev.currentTarget); const scope = $link.data("scope"); const $target = $(`span.awesome-auto-edit[data-scope="${scope}"]`); const $constraints = $(`.constraints-editor[data-key="${scope}"]`); - if ($target.length == 0) { + if ($target.length === 0) { return; } @@ -41,7 +41,7 @@ $(() => { $container.attr("data-key", result.key); $delete.attr("href", $delete.attr("href").replace(`key=${key}`, `key=${result.key}`)) CustomFieldsBuilders.forEach((builder) => { - if (builder.key == key) { + if (builder.key === key) { builder.key = result.key; } }); @@ -52,12 +52,12 @@ $(() => { $link.hide(); $input.select(); $input.on("keypress", (evt) => { - if (evt.code == "Enter" || evt.code == "13" || evt.code == "10") { + if (evt.code === "Enter" || evt.code === "13" || evt.code === "10") { evt.preventDefault(); $.ajax( { type: "POST", - url: DecidimAwesome.rename_scope_label_path, + url: window.DecidimAwesome.rename_scope_label_path, dataType: "json", headers: { "X-CSRF-Token": $("meta[name=csrf-token]").attr("content") diff --git a/app/packs/src/decidim/decidim_awesome/admin/check_redirections.js b/app/packs/src/decidim/decidim_awesome/admin/check_redirections.js index 869c3df72..97a11b66a 100644 --- a/app/packs/src/decidim/decidim_awesome/admin/check_redirections.js +++ b/app/packs/src/decidim/decidim_awesome/admin/check_redirections.js @@ -14,13 +14,13 @@ $(() => { let type = response.type; let status = response.status; - if (response.type == "opaqueredirect") { + if (response.type === "opaqueredirect") { type = "redirect"; status = "302"; } if (item.active) { - if (type == "redirect") { + if (type === "redirect") { $td.addClass("success"); } else { $td.addClass("alert"); diff --git a/app/packs/src/decidim/decidim_awesome/admin/constraints.js b/app/packs/src/decidim/decidim_awesome/admin/constraints.js index 9d72ad1f3..d8c88ce05 100644 --- a/app/packs/src/decidim/decidim_awesome/admin/constraints.js +++ b/app/packs/src/decidim/decidim_awesome/admin/constraints.js @@ -4,9 +4,9 @@ $(() => { return; } - $(".decidim_awesome-form").on("click", ".constraints-editor .add-condition,.constraints-editor .edit-condition", (e) => { - e.preventDefault(); - const $this = $(e.target) + $(".decidim_awesome-form").on("click", ".constraints-editor .add-condition,.constraints-editor .edit-condition", (evt) => { + evt.preventDefault(); + const $this = $(evt.target) const url = $this.attr("href"); const $callout = $this.closest(".constraints-editor").find(".callout"); $callout.hide(); @@ -21,8 +21,8 @@ $(() => { }); // Custom event listener to reload the modal if needed - document.body.addEventListener("constraint:change", (e) => { - const vars = e.detail.map((setting) => `${setting.key}=${setting.value}`); + document.body.addEventListener("constraint:change", (evt) => { + const vars = evt.detail.map((setting) => `${setting.key}=${setting.value}`); const url = `${$modal.data("url")}&${vars.join("&")}`; // console.log("constraint:change vars:", vars, "url:", url) $modal.addClass("loading"); diff --git a/app/packs/src/decidim/decidim_awesome/admin/custom_fields_builder.js b/app/packs/src/decidim/decidim_awesome/admin/custom_fields_builder.js index 5c58ab37c..2356aeb9b 100644 --- a/app/packs/src/decidim/decidim_awesome/admin/custom_fields_builder.js +++ b/app/packs/src/decidim/decidim_awesome/admin/custom_fields_builder.js @@ -36,7 +36,8 @@ $(() => { "paragraph" ], disabledSubtypes: { - text: ["color"], // default color as it generate hashtags in decidim (TODO: fix hashtag generator with this) + // default color as it generate hashtags in decidim (TODO: fix hashtag generator with this) + text: ["color"], // disable default wysiwyg editors as they present problems textarea: ["tinymce", "quill"] } @@ -45,21 +46,21 @@ $(() => { }); }); - $(document).on("formBuilder.create", (_event, i, list) => { - if (!list[i]) { + $(document).on("formBuilder.create", (_event, idx, list) => { + if (!list[idx]) { return; } - $(list[i].el).formBuilder(list[i].config).promise.then(function(res) { - list[i].instance = res; + $(list[idx].el).formBuilder(list[idx].config).promise.then(function(res) { + list[idx].instance = res; // Attach to DOM - list[i].el.FormBuilder = res; + list[idx].el.FormBuilder = res; // remove spinner - $(list[i].el).find(".loading-spinner").remove(); + $(list[idx].el).find(".loading-spinner").remove(); // for external use - $(document).trigger("formBuilder.created", [list[i]]); - if (i < list.length) { - $(document).trigger("formBuilder.create", [i + 1, list]); + $(document).trigger("formBuilder.created", [list[idx]]); + if (idx < list.length) { + $(document).trigger("formBuilder.create", [idx + 1, list]); } }); }); diff --git a/app/packs/src/decidim/decidim_awesome/admin/form_exit_warn.js b/app/packs/src/decidim/decidim_awesome/admin/form_exit_warn.js index 7cef6a7ff..f8810f245 100644 --- a/app/packs/src/decidim/decidim_awesome/admin/form_exit_warn.js +++ b/app/packs/src/decidim/decidim_awesome/admin/form_exit_warn.js @@ -23,6 +23,7 @@ $(() => { } event.returnValue = true; + return true; }); } }); diff --git a/app/packs/src/decidim/decidim_awesome/admin/user_picker.js b/app/packs/src/decidim/decidim_awesome/admin/user_picker.js index b1eb356f1..9f3a1dfcd 100644 --- a/app/packs/src/decidim/decidim_awesome/admin/user_picker.js +++ b/app/packs/src/decidim/decidim_awesome/admin/user_picker.js @@ -1,3 +1,4 @@ +/* eslint-disable no-invalid-this */ import "select2" import "stylesheets/decidim/decidim_awesome/admin/user_picker.scss" diff --git a/app/packs/src/decidim/decidim_awesome/awesome_map/api/fetcher.js b/app/packs/src/decidim/decidim_awesome/awesome_map/api/fetcher.js index 9b75068cd..c73fbb05d 100644 --- a/app/packs/src/decidim/decidim_awesome/awesome_map/api/fetcher.js +++ b/app/packs/src/decidim/decidim_awesome/awesome_map/api/fetcher.js @@ -67,13 +67,13 @@ export default class Fetcher { } findTranslation(translations) { - let text, - lang = document.querySelector("html").getAttribute("lang"); + let lang = document.querySelector("html").getAttribute("lang"), + text = ""; - translations.forEach((t) => { - if (t.text) { - if (!text || t.locale == lang) { - text = t.text + translations.forEach((txt) => { + if (txt.text) { + if (!text || txt.locale === lang) { + text = txt.text } } }); @@ -85,12 +85,10 @@ export default class Fetcher { if (text) { const gids = text.match(/gid:\/\/[^\s<&]+/g) if (gids) { - tags = gids.filter((gid) => gid.indexOf("/Decidim::Hashtag/") != -1).map((gid) => { + tags = gids.filter((gid) => gid.indexOf("/Decidim::Hashtag/") !== -1).map((gid) => { const parts = gid.split("/"); - const fromSelector = parts[5].charAt(0) == "_"; - const tag = fromSelector - ? parts[5].substr(1) - : parts[5]; + const fromSelector = parts[5].charAt(0) === "_"; + const tag = fromSelector ? parts[5].substr(1) : parts[5]; // eslint-disable-line no-ternary, multiline-ternary const name = `#${tag}`; const html = `${name}`; const hashtag = { @@ -110,7 +108,8 @@ export default class Fetcher { return tags; } - replaceHashtags(text, hashtags) { + replaceHashtags(txt, hashtags) { + let text = txt; hashtags.forEach((tag) => { text = text.replace(tag.gid, tag.name) }); @@ -121,7 +120,8 @@ export default class Fetcher { return text.replace(/gid:\/\/[^\s<&]+/g, ""); } - appendHtmlHashtags(text, tags) { + appendHtmlHashtags(txt, tags) { + let text = txt; tags.forEach((tag) => { text += ` ${tag.html}`; }); diff --git a/app/packs/src/decidim/decidim_awesome/awesome_map/awesome_map.js b/app/packs/src/decidim/decidim_awesome/awesome_map/awesome_map.js index 62bc033c8..689874b36 100644 --- a/app/packs/src/decidim/decidim_awesome/awesome_map/awesome_map.js +++ b/app/packs/src/decidim/decidim_awesome/awesome_map/awesome_map.js @@ -1,8 +1,11 @@ import * as L from "leaflet"; -import "src/decidim/map/icon.js" // comes with Decidim +// comes with Decidim +import "src/decidim/map/icon.js" import "src/decidim/vendor/leaflet-tilelayer-here" -import "leaflet.markercluster"; // Comes with Decidim -import "leaflet.featuregroup.subgroup" // included in this package.json +// Comes with Decidim +import "leaflet.markercluster"; +// included in this package.json +import "leaflet.featuregroup.subgroup" import "src/vendor/jquery.truncate" import "jsrender" @@ -60,7 +63,7 @@ export default class AwesomeMap { this.loading.pop(); this.autoResize(); - if (this.loading.length == 0) { + if (this.loading.length === 0) { this.controls.$loading.hide(); // call trigger as all loads are finished this.onFinished(); @@ -90,13 +93,11 @@ export default class AwesomeMap { }; if (category) { - let id = category.id - ? parseInt(category.id, 10) - : parseInt(category, 10); - let cat = this.categories.find((c) => c.id == id); + let id = category.id ? parseInt(category.id, 10) : parseInt(category, 10); // eslint-disable-line no-ternary, multiline-ternary + let cat = this.categories.find((ct) => ct.id === id); if (cat) { cat.children = () => { - return this.categories.filter((c) => c.parent === cat.id); + return this.categories.filter((ct) => ct.parent === cat.id); } return cat; } @@ -105,12 +106,12 @@ export default class AwesomeMap { } _getController(component) { - let controller; + let controller = null; - if (component.type == "proposals") { + if (component.type === "proposals") { controller = new ProposalsController(this, component); } - if (component.type == "meetings" && this.config.menu.meetings) { + if (component.type === "meetings" && this.config.menu.meetings) { controller = new MeetingsController(this, component); } @@ -125,5 +126,6 @@ export default class AwesomeMap { this.controllers[component.type] = controller; return this.controllers[component.type] } + return null; } } diff --git a/app/packs/src/decidim/decidim_awesome/awesome_map/controllers/controller.js b/app/packs/src/decidim/decidim_awesome/awesome_map/controllers/controller.js index 44e9be785..55d120cdc 100644 --- a/app/packs/src/decidim/decidim_awesome/awesome_map/controllers/controller.js +++ b/app/packs/src/decidim/decidim_awesome/awesome_map/controllers/controller.js @@ -14,9 +14,10 @@ export default class Controller { } getLabel() { - let text = this.awesomeMap.config.menu.mergeComponents || !this.component.name - ? window.DecidimAwesome.texts[this.component.type] - : this.component.name; + let text = this.awesomeMap.config.menu.mergeComponents || this.component.name; + if (!text) { + text = window.DecidimAwesome.texts[this.component.type]; + } return `${text}` } @@ -32,8 +33,8 @@ export default class Controller { const collectionEdges = collection.edges.filter((item) => item.node.coordinates && item.node.coordinates.latitude && item.node.coordinates.longitude); try { this.awesomeMap.cluster.addLayers(collectionEdges.map((item) => item.node.marker)); - } catch (e) { - console.error("Failed marker collection assignation", collectionEdges, "error", e); + } catch (evt) { + console.error("Failed marker collection assignation", collectionEdges, "error", evt); } // subgroups don't have th addLayers utility collectionEdges.forEach((item) => { @@ -97,8 +98,8 @@ export default class Controller { try { this.awesomeMap.layers[cat.id].group.addLayer(marker); this.awesomeMap.controls.showCategory(cat); - } catch (e) { - console.error("Failed category marker assignation", marker, e.message); + } catch (evt) { + console.error("Failed category marker assignation", marker, evt.message); } } } @@ -108,8 +109,8 @@ export default class Controller { if (this.awesomeMap.config.menu.hashtags) { try { this.awesomeMap.controls.addHashtagsControls(hashtags, marker); - } catch (e) { - console.error("Failed hashtags marker assignation", marker, e.message); + } catch (evt) { + console.error("Failed hashtags marker assignation", marker, evt.message); } } } diff --git a/app/packs/src/decidim/decidim_awesome/awesome_map/controllers/proposals_controller.js b/app/packs/src/decidim/decidim_awesome/awesome_map/controllers/proposals_controller.js index c1d50388a..d1ae68b44 100644 --- a/app/packs/src/decidim/decidim_awesome/awesome_map/controllers/proposals_controller.js +++ b/app/packs/src/decidim/decidim_awesome/awesome_map/controllers/proposals_controller.js @@ -62,15 +62,15 @@ export default class ProposalsController extends Controller { // Process all amendments iterableAmendments.forEach((amendment) => { - const marker = this.allNodes.find((node) => node.id == amendment[0]); + const marker = this.allNodes.find((node) => node.id === amendment[0]); const parent = amendment[1]; // console.log("marker", marker, "parent proposal", parent) // add marker to amendments layers and remove it from proposals if (marker) { try { marker.marker.removeFrom(this.controls.group) - } catch (e) { - console.error("error removeFrom marker", marker, "layer", this.controls.group, e); + } catch (evt) { + console.error("error removeFrom marker", marker, "layer", this.controls.group, evt); } if (this.awesomeMap.config.menu.amendments) { marker.marker.addTo(this.awesomeMap.layers.amendments.group); diff --git a/app/packs/src/decidim/decidim_awesome/awesome_map/controls_ui.js b/app/packs/src/decidim/decidim_awesome/awesome_map/controls_ui.js index 8d3d0a3c6..d4d8a358c 100644 --- a/app/packs/src/decidim/decidim_awesome/awesome_map/controls_ui.js +++ b/app/packs/src/decidim/decidim_awesome/awesome_map/controls_ui.js @@ -1,3 +1,5 @@ +/* eslint-disable no-ternary, multiline-ternary */ + import * as L from "leaflet"; export default class ControlsUI { @@ -33,28 +35,28 @@ export default class ControlsUI { } // sub-layer hashtag title toggle - $("#awesome-map").on("click", ".awesome_map-title-control", (e) => { - e.preventDefault(); - e.stopPropagation(); + $("#awesome-map").on("click", ".awesome_map-title-control", (evt) => { + evt.preventDefault(); + evt.stopPropagation(); $("#awesome_map-categories-control").toggleClass("active"); $("#awesome_map-hashtags-control").toggleClass("active"); }); // hashtag events - $("#awesome-map").on("change", ".awesome_map-hashtags-selector", (e) => { - e.preventDefault(); - e.stopPropagation(); - const tag = $(e.target).closest("label").data("layer"); - // console.log("changed, layer", tag, "checked", e.target.checked, e); + $("#awesome-map").on("change", ".awesome_map-hashtags-selector", (evt) => { + evt.preventDefault(); + evt.stopPropagation(); + const tag = $(evt.target).closest("label").data("layer"); + // console.log("changed, layer", tag, "checked", evt.target.checked, e); if (tag) { this.updateHashtagLayers(); } }); // select/deselect all tags - $("#awesome-map").on("click", ".awesome_map-toggle_all_tags", (e) => { - e.preventDefault(); - e.stopPropagation(); + $("#awesome-map").on("click", ".awesome_map-toggle_all_tags", (evt) => { + evt.preventDefault(); + evt.stopPropagation(); $("#awesome-map .awesome_map-hashtags-selector").prop("checked", $("#awesome-map .awesome_map-hashtags-selector:checked").length < $("#awesome-map .awesome_map-hashtags-selector").length); this.updateHashtagLayers(); }); @@ -74,22 +76,20 @@ export default class ControlsUI { group: new L.FeatureGroup.SubGroup(this.awesomeMap.cluster) }; this.awesomeMap.layers[category.id].group.addTo(this.awesomeMap.map); - $("#awesome_map-categories-control .categories-container").append(``); + $("#awesome_map-categories-control .categories-container").append(``); }) // category events - $("#awesome-map").on("change", ".awesome_map-categories-selector", (e) => { - e.preventDefault(); - e.stopPropagation(); + $("#awesome-map").on("change", ".awesome_map-categories-selector", (evt) => { + evt.preventDefault(); + evt.stopPropagation(); - const id = $(e.target).closest("label").data("layer"); + const id = $(evt.target).closest("label").data("layer"); const cat = this.awesomeMap.getCategory(id); - // console.log("changed, layer", id, "cat", cat, "checked", e.target.checked, e); + // console.log("changed, layer", id, "cat", cat, "checked", evt.target.checked, e); if (cat) { const layer = this.awesomeMap.layers[cat.id]; - if (e.target.checked) { + if (evt.target.checked) { // show group of markers this.awesomeMap.map.addLayer(layer.group); } else { @@ -131,7 +131,7 @@ export default class ControlsUI { const $label = $(`label.awesome_map-hashtag-${hashtag.tag}`); // update number of items - $label.attr("title", `${parseInt($label.attr("title") || 0) + 1} ${window.DecidimAwesome.texts.items}`); + $label.attr("title", `${parseInt($label.attr("title") || 0, 10) + 1} ${window.DecidimAwesome.texts.items}`); }); } } @@ -143,10 +143,10 @@ export default class ControlsUI { const $parent = $(`label.awesome_map-category-${cat.parent}`); $label.show(); // update number of items - $label.attr("title", `${parseInt($label.attr("title") || 0) + 1} ${window.DecidimAwesome.texts.items}`); + $label.attr("title", `${parseInt($label.attr("title") || 0, 10) + 1} ${window.DecidimAwesome.texts.items}`); // show parent if apply $parent.show(); - $parent.attr("title", `${parseInt($parent.attr("title") || 0) + 1} ${window.DecidimAwesome.texts.items}`); + $parent.attr("title", `${parseInt($parent.attr("title") || 0, 10) + 1} ${window.DecidimAwesome.texts.items}`); } removeHiddenComponents() { @@ -200,8 +200,8 @@ export default class ControlsUI { if (cat.parent) { let $input = $(`.awesome_map-category-${cat.parent}`).contents("input"); let $subcats = $(`[class^="awesome_map-category-"][data-parent="${cat.parent}"]:visible`); - let num_checked = $subcats.contents("input:checked").length; - $input.prop("indeterminate", num_checked != $subcats.length && num_checked != 0); + let numChecked = $subcats.contents("input:checked").length; + $input.prop("indeterminate", numChecked !== $subcats.length && numChecked !== 0); } } @@ -213,7 +213,6 @@ export default class ControlsUI { $div.contents("label").each((_idx, el) => { if ($(el).text().localeCompare($last.text()) > 0) { $(el).before($last); - return false; } }); } diff --git a/app/packs/src/decidim/decidim_awesome/awesome_map/load_map.js b/app/packs/src/decidim/decidim_awesome/awesome_map/load_map.js index 86979ed76..a3fd7a1cc 100644 --- a/app/packs/src/decidim/decidim_awesome/awesome_map/load_map.js +++ b/app/packs/src/decidim/decidim_awesome/awesome_map/load_map.js @@ -12,6 +12,7 @@ $(() => { } } } + return null; }; const config = { diff --git a/app/packs/src/decidim/decidim_awesome/editors/editor.js b/app/packs/src/decidim/decidim_awesome/editors/editor.js index 5709888c8..866d4f1dd 100644 --- a/app/packs/src/decidim/decidim_awesome/editors/editor.js +++ b/app/packs/src/decidim/decidim_awesome/editors/editor.js @@ -1,4 +1,4 @@ -/* eslint-disable require-jsdoc */ +/* eslint-disable require-jsdoc, func-style */ /* * Since version 0.25 we follow a different strategy and opt to destroy and override completely the original editor @@ -26,8 +26,8 @@ export function destroyQuillEditor(container) { const content = $(container).find(".ql-editor").html(); $(container).html(content); $(container).siblings(".ql-toolbar").remove(); - $(container).find("*[class*='ql-']").removeClass((index, class_name) => (class_name.match(/(^|\s)ql-\S+/g) || []).join(" ")); - $(container).removeClass((index, class_name) => (class_name.match(/(^|\s)ql-\S+/g) || []).join(" ")); + $(container).find("*[class*='ql-']").removeClass((index, className) => (className.match(/(^|\s)ql-\S+/g) || []).join(" ")); + $(container).removeClass((index, className) => (className.match(/(^|\s)ql-\S+/g) || []).join(" ")); if ($(container).next().is("p.help-text")) { $(container).next().remove(); } @@ -107,7 +107,7 @@ export function createQuillEditor(container) { let msg = serverError && serverError.body; try { msg = JSON.parse(msg).message; - } catch (e) { console.error("Parsing error", e); } + } catch (evt) { console.error("Parsing error", evt); } console.error(`Image upload error: ${msg}`); let $p = $(`

${msg}

`); $(container).after($p) @@ -174,7 +174,7 @@ export function createQuillEditor(container) { } export function createMarkdownEditor(container) { - const t = DecidimAwesome.texts.drag_and_drop_image; + const text = DecidimAwesome.texts.drag_and_drop_image; const token = $('meta[name="csrf-token"]').attr("content"); const $input = $(container).siblings('input[type="hidden"]'); const $faker = $('