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")