Skip to content

Commit

Permalink
Show superadmins in admin accountability (#12)
Browse files Browse the repository at this point in the history
* refactor initializer & presenter clasess

* complete space role test

* differentiate admins from space admins

* add user presenter specs

* fix filters

* add tests for super admins

* fix tests

* fix permissions

* Add readme instructions

* update version

* fix readme
  • Loading branch information
microstudi authored Feb 20, 2023
1 parent 1a97b89 commit 10958f9
Show file tree
Hide file tree
Showing 31 changed files with 905 additions and 178 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
CHANGELOG
=========

v0.9.1
------

Compatibility:
- Decidim v0.27.x
- Decidim v0.26.x

Features:
- Fixes for the Awesome Map
- Added Admin Accountability feature

v0.9.0
------

Expand Down
2 changes: 1 addition & 1 deletion Gemfile.legacy.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
decidim-decidim_awesome (0.9.0)
decidim-decidim_awesome (0.9.1)
decidim-admin (>= 0.26.0, < 0.28)
decidim-core (>= 0.26.0, < 0.28)
sassc (~> 2.3)
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
decidim-decidim_awesome (0.9.0)
decidim-decidim_awesome (0.9.1)
decidim-admin (>= 0.26.0, < 0.28)
decidim-core (>= 0.26.0, < 0.28)
sassc (~> 2.3)
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ Rules available:

![Custom validations](examples/custom_validations.png)

#### 15. Admin accountability

This feature allows you to list all the users that are, or have been at any point in time, admins, valuators, user managers or any other role in Decidim. Including global admin roles or private admins of a particular participatory space.

Results can be filtered by role and by time range and also exported as CSV or other formats.

![Admin accountability](examples/admin_accountability.png)

#### To be continued...

We're not done! Please check the [issues](/decidim-ice/decidim-module-decidim_awesome/issues) (and participate) to see what's on our mind
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,32 @@ module Filterable
included do
include Decidim::Admin::Filterable

helper Decidim::DecidimAwesome::AdminAccountability::Admin::FilterableHelper

private

def base_query
collection
end

def filters
[
:role_type_eq,
:participatory_space_type_eq
]
[:role_type_eq, :participatory_space_type_eq]
end

def filters_with_values
{
role_type_eq: role_types,
participatory_space_type_eq: participatory_space_types
}
return { admin_role_type: [] } if global?

{ 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 extra_allowed_params
[:per_page, :admins, :admin_role_type]
end

def translated_role_type_eq(role)
I18n.t(role, scope: "decidim.decidim_awesome.admin.admin_accountability.roles")
end
Expand All @@ -48,16 +50,14 @@ def search_field_predicate
:user_name_or_user_email_cont
end

def extra_allowed_params
[:per_page]
end

def participatory_space_types
@participatory_space_types ||= collection.pluck(:item_type).uniq.sort
end

def role_types
@role_types ||= collection.map { |admin_action| admin_action.item&.role }.compact.uniq.sort
@role_types ||= PaperTrailVersion.safe_user_roles.map do |role_class|
role_class.safe_constantize.select(:role).distinct.pluck(:role)
end.union.flatten.sort
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

module Decidim
module DecidimAwesome
module AdminAccountability
module Admin
module FilterableHelper
def extra_dropdown_submenu_options_items(_filter, _i18n_scope)
Decidim.user_roles.sort.map do |role_type|
link_to(I18n.t(role_type, scope: "decidim.decidim_awesome.admin.admin_accountability.admin_roles"),
url_for(export_params.merge({ admin_role_type: role_type })))
end
end

def applied_filters_tags(i18n_ctx)
if global? && params[:admin_role_type].present?
content_tag(:span, class: "label secondary") do
concat "#{i18n_filter_label(:admin_role_type, filterable_i18n_scope_from_ctx(i18n_ctx))}: "
concat t("decidim.decidim_awesome.admin.admin_accountability.admin_roles.#{params[:admin_role_type]}", default: params[:admin_role_type])
concat icon_link_to(
"circle-x",
url_for(export_params.except(:admin_role_type)),
t("decidim.admin.actions.cancel"),
class: "action-icon--remove"
)
end
else
ransack_params.slice(*filters).map do |filter, value|
applied_filter_tag(filter, value, filterable_i18n_scope_from_ctx(i18n_ctx))
end.join.html_safe
end
end
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,25 @@ class AdminAccountabilityController < DecidimAwesome::Admin::ApplicationControll
include NeedsAwesomeConfig
include Decidim::DecidimAwesome::AdminAccountability::Admin::Filterable

helper_method :admin_actions, :admin_action, :collection, :export_params
helper_method :admin_actions, :collection, :export_params, :global?

layout "decidim/admin/users"

before_action do
enforce_permission_to :edit_config, :allow_admin_accountability
enforce_permission_to :edit_config, :admin_accountability, global: global?
end

def index; end

def export
format = params[:format].to_s
filters = export_params[:q]

Decidim::DecidimAwesome::ExportAdminActionsJob.perform_later(current_user, format, admin_actions.ransack(filters).result.ids)
Decidim::DecidimAwesome::ExportAdminActionsJob.perform_later(current_user,
params[:format].to_s,
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")
redirect_back fallback_location: decidim_admin_decidim_awesome.admin_accountability_path,
notice: t("decidim.decidim_awesome.admin.admin_accountability.exports.notice")
end

private
Expand All @@ -33,15 +35,15 @@ def admin_actions
end

def collection
@collection ||= paginate(PaperTrailVersion.role_actions)
@collection ||= paginate(global? ? PaperTrailVersion.admin_role_actions(params[:admin_role_type]) : PaperTrailVersion.space_role_actions)
end

def admin_action
@admin_action ||= collection.find(params[:id])
def export_params
params.permit(:format, :admins, :admin_role_type, q: [:role_type_eq, :user_name_or_user_email_cont, :created_at_gteq, :created_at_lteq])
end

def export_params
params.permit(:format, q: [:role_type_eq, :user_name_or_user_email_cont, :created_at_gteq, :created_at_lteq])
def global?
params[:admins] == "true"
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def perform(current_user, format, collection_ids)

def serialized_collection(collection_ids)
@serialized_collection ||= begin
collection = PaperTrailVersion.role_actions.where(id: collection_ids)
collection = PaperTrailVersion.where(id: collection_ids)
collection.map do |item|
PaperTrailVersionSerializer.new(item).serialize
end
Expand Down
32 changes: 28 additions & 4 deletions app/models/decidim/decidim_awesome/paper_trail_version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,28 @@ class PaperTrailVersion < PaperTrail::Version
default_scope { order("created_at DESC") }

def self.safe_user_roles
DecidimAwesome.admin_user_roles.filter(&:safe_constantize)
DecidimAwesome.participatory_space_roles.filter(&:safe_constantize)
end

scope :role_actions, -> { where(item_type: PaperTrailVersion.safe_user_roles, event: "create") }
scope :space_role_actions, -> { where(item_type: PaperTrailVersion.safe_user_roles, event: "create") }

def self.admin_role_actions(filter = nil)
base = where(item_type: "Decidim::UserBaseEntity", event: %w(create update))
case filter
when nil
base.where("object_changes LIKE '%\nroles:\n%' OR object_changes LIKE '%\nadmin:\n- false\n%'")
when "admin"
base.where("object_changes LIKE '%\nadmin:\n- false\n%'")
else
base.where(Arel.sql("object_changes LIKE '%\nroles:\n%\n- - #{filter}\n%'"))
end
end

def present(html: true)
@present ||= if item_type.in?(PaperTrailVersion.safe_user_roles)
PaperTrailRolePresenter.new(self, html: html)
@present ||= if item_type == "Decidim::UserBaseEntity"
UserEntityPresenter.new(self, html: html)
elsif item_type.in?(PaperTrailVersion.safe_user_roles)
ParticipatorySpaceRolePresenter.new(self, html: html)
else
self
end
Expand Down Expand Up @@ -48,6 +62,11 @@ def present(html: true)
AND item_type = '#{role_class}'
)
end
queries << %(
SELECT decidim_users.email FROM decidim_users
WHERE decidim_users.id = versions.item_id
AND item_type = 'Decidim::UserBaseEntity'
)
Arel.sql("(#{queries.join(" UNION ")})")
end
end
Expand All @@ -63,6 +82,11 @@ def present(html: true)
AND item_type = '#{role_class}'
)
end
queries << %(
SELECT decidim_users.name FROM decidim_users
WHERE decidim_users.id = versions.item_id
AND item_type = 'Decidim::UserBaseEntity'
)
Arel.sql("(#{queries.join(" UNION ")})")
end
end
Expand Down
16 changes: 15 additions & 1 deletion app/permissions/decidim/decidim_awesome/admin/permissions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,24 @@ def permissions
return permission_action unless user
return permission_action if user.read_attribute("admin").blank?

toggle_allow(config_enabled?(permission_action.subject)) if permission_action.action == :edit_config
if permission_action.subject == :admin_accountability && DecidimAwesome.admin_accountability.respond_to?(:include?)
if global?
toggle_allow(DecidimAwesome.admin_accountability.include?(:admin_roles))
else
toggle_allow(DecidimAwesome.admin_accountability.include?(:participatory_space_roles))
end
elsif permission_action.action == :edit_config
toggle_allow(config_enabled?(permission_action.subject))
end

permission_action
end

private

def global?
context.fetch(:global)
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

module Decidim
module DecidimAwesome
class PaperTrailBasePresenter
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

def item_type
@item_type ||= entry&.item_type
end

def item_id
@item_id ||= entry&.item_id
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

module Decidim
module DecidimAwesome
class ParticipatorySpaceRolePresenter < RoleBasePresenter
# Finds the destroyed entry if exists
def destroy_entry
@destroy_entry ||= PaperTrail::Version.find_by(item_type: item_type, event: "destroy", item_id: item_id)
end

# roles are in the destroyed event if the role has been removed
def role
@role ||= destroy_item&.role || item&.role
end

def role_name
type = I18n.t(role, scope: "decidim.decidim_awesome.admin.admin_accountability.roles", default: role)
return type unless html && role_class

"<span class=\"#{role_class}\">#{type}</span>".html_safe
end

def user
@user ||= Decidim::User.find_by(id: entry.changeset["decidim_user_id"]&.last)
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

private

def role_class
case role
when "admin"
"text-alert"
when "valuator"
"text-secondary"
end
end
end
end
end
Loading

0 comments on commit 10958f9

Please sign in to comment.