# frozen_string_literal: true module Decidim # Scopes are used in some entities through Decidim to help users know which is # the scope of a participatory process. # (i.e. does it affect the whole city or just a district?) class Scope < ApplicationRecord include Decidim::Traceable include Decidim::Loggable belongs_to :organization, foreign_key: "decidim_organization_id", class_name: "Decidim::Organization", inverse_of: :scopes belongs_to :scope_type, foreign_key: "scope_type_id", class_name: "Decidim::ScopeType", inverse_of: :scopes, optional: true belongs_to :parent, foreign_key: "parent_id", class_name: "Decidim::Scope", inverse_of: :children, optional: true has_many :children, foreign_key: "parent_id", class_name: "Decidim::Scope", inverse_of: :parent, dependent: :destroy before_validation :update_part_of, on: :update validates :name, :code, :organization, presence: true validates :code, uniqueness: { scope: :organization } validate :forbid_cycles after_create_commit :create_part_of # Scope to return only the top level scopes. # # Returns an ActiveRecord::Relation. def self.top_level(organization_id = nil) query = where parent_id: nil query = query.where(decidim_organization_id: organization_id) if organization_id query end def self.log_presenter_class_for(_log) Decidim::AdminLog::ScopePresenter end def descendants organization.scopes.where("? = ANY(decidim_scopes.part_of)", id) end def ancestor_of?(scope) scope && scope.part_of.member?(id) end # Gets the scopes from the part_of list in descending order (first the top level scope, last itself) # # root - The root scope to start retrieval. If present, ignores top level scopes until reaching the root scope. # # Returns an array of Scope objects def part_of_scopes(root = nil) scope_ids = part_of scope_ids.select! { |id| id == root.id || !root.part_of.member?(id) } if root organization.scopes.where(id: scope_ids).sort { |s1, s2| part_of.index(s2.id) <=> part_of.index(s1.id) } end private def forbid_cycles return unless parent errors.add(:parent_id, :cycle_detected) if parent.part_of.include?(id) end def create_part_of build_part_of save if changed? end def update_part_of build_part_of end def build_part_of if parent part_of.clear.append(id).concat(parent.reload.part_of) else part_of.clear.append(id) end end end end