ministryofjustice/peoplefinder

View on GitHub
app/models/group.rb

Summary

Maintainability
A
2 hrs
Test Coverage
A
100%
# == Schema Information
#
# Table name: groups
#
#  id                            :integer          not null, primary key
#  name                          :text
#  created_at                    :datetime
#  updated_at                    :datetime
#  slug                          :string
#  description                   :text
#  ancestry                      :text
#  ancestry_depth                :integer          default(0), not null
#  acronym                       :text
#  description_reminder_email_at :datetime
#  members_completion_score      :integer
#

class Group < ApplicationRecord
  include Hierarchical
  include Placeholder

  MAX_DESCRIPTION = 2000

  has_paper_trail versions: { class_name: "Version" },
                  ignore: %i[updated_at
                             created_at
                             slug
                             id
                             description_reminder_email_at
                             members_completion_score]

  extend FriendlyId
  friendly_id :slug_candidates, use: :slugged

  def slug_candidates
    return [name] unless parent

    [name, [parent.name, name], [parent.name, name_and_sequence]]
  end

  def should_generate_new_friendly_id?
    name_changed?
  end

  has_many :memberships,
           -> { includes(:person).order("people.surname") },
           dependent: :destroy
  has_many :people, through: :memberships
  has_many :leaderships, -> { where(leader: true) }, class_name: "Membership"
  has_many :leaders, through: :leaderships, source: :person
  has_many :non_leaders, through: :non_leaderships, source: :person

  def distinct_non_leaderships
    DistinctMembershipQuery.new(group: self, leadership: false).call
  end

  validates :name, presence: true
  validates :slug, uniqueness: true # rubocop:disable Rails/UniqueValidationWithoutIndex
  validates :description, length: { maximum: MAX_DESCRIPTION }

  validate :not_second_root_group

  before_destroy :check_deletability

  default_scope { order(name: :asc) }

  scope :without_description, -> { unscoped.where(description: ["", nil]) }

  after_save { |group| UpdateGroupMembersCompletionScoreJob.perform_later(group) }

  def self.department
    roots.first
  end

  def self.hierarchy_hash
    arrange(order: :name)
  end

  def to_s
    name
  end

  def short_name
    acronym.presence || name
  end

  def deletable?
    leaf_node? && memberships.reject(&:new_record?).empty?
  end

  def all_people
    Person.all_in_subtree(self).ordered_by_name
  end

  def all_people_count
    Person.count_in_groups(subtree_ids)
  end

  def people_outside_subteams
    # TODO: replace with scope and person method for role_names
    # i.e. Person.outside_subteams(self)
    Person.all_in_groups([id]) - Person.all_in_groups(subteam_ids) - leaders
  end

  def people_outside_subteams_count
    Person.outside_subteams(self).count
  end

  def leaderships_by_person
    leaderships.group_by(&:person)
  end

  def average_completion_score
    people = all_people
    people.blank? ? 0 : Person.average_completion_score(people.pluck(:id))
  end

  def update_members_completion_score!
    score = average_completion_score
    update_columns(members_completion_score: score)
  end

  def editable_parent?
    new_record? || parent.present? || children.empty?
  end

  def subscribers
    memberships.subscribing.joins(:person).map(&:person)
  end

  def description_reminder_email_sent?(within_days:)
    description_reminder_email_at.present? &&
      description_reminder_email_at.end_of_day >= within_days.day.ago
  end

private

  def not_second_root_group
    if parent_id.nil?
      department = Group.department
      root_group_exists = department.present? && department != self
      errors.add(:parent_id, "is required") if root_group_exists
    end
  end

  def name_and_sequence
    slug = name.to_param
    sequence = Group.where("slug like ?", "#{slug}-%").count + 2
    "#{slug}-#{sequence}"
  end

  def check_deletability
    errors.add :base, :memberships_exist unless deletable?
  end

  def subteam_ids
    subtree_ids - [id]
  end
end