module Admin class UsersQuery SEARCH_CLAUSE = "users.name ILIKE :search OR " \ "users.email ILIKE :search OR " \ "users.username ILIKE :search".freeze # @api public # @param relation [ActiveRecord::Relation] # @param identifier [String, nil] def self.find(identifier, relation: User) return if identifier.blank? relation.where(id: identifier) .or(relation.where(username: identifier)) .or(relation.where(email: identifier)).first end # @api public # @param relation [ActiveRecord::Relation] # @param role [String, nil] # @param search [String, nil] # @param roles [Array, nil] # @param statuses [Array, nil] # @param ids [Array, nil] # @param organizations [Array, nil] # @param joining_start [String, nil] # @param joining_end [String, nil] # @param date_format [String] # @param limit [Integer, nil] def self.call(relation: User.registered, role: nil, search: nil, roles: [], organizations: [], statuses: [], ids: [], joining_start: nil, joining_end: nil, date_format: "DD/MM/YYYY", limit: nil) # We are at an interstitial moment where we are exposing both the role and roles param. We # need to favor one or the other. if role.presence relation = relation.with_role(role, :any) elsif roles.presence || statuses.presence # "statuses" are a subset of roles, so we can handle these filters together relation = filter_roles(relation: relation, roles: [roles, statuses].compact.reduce([], :|)) end if organizations.presence relation = filter_organization_memberships(relation: relation, organizations: organizations) end if joining_start.presence || joining_end.presence relation = filter_joining_date(relation: relation, joining_start: joining_start, joining_end: joining_end, date_format: date_format) end relation = search_relation(relation, search) if search.presence relation = filter_ids(relation, ids) if ids.presence relation = relation.limit(limit.to_i) if limit.to_i.positive? relation.distinct.order(created_at: :desc) end def self.search_relation(relation, search) relation.where(SEARCH_CLAUSE, search: "%#{search.strip}%") end def self.filter_ids(relation, ids) relation.where(id: ids) end def self.filter_joining_date(relation:, joining_start:, joining_end:, date_format:) ui_formats_to_parse_format = { "DD/MM/YYYY" => "%d/%m/%Y", "MM/DD/YYYY" => "%m/%d/%Y" } parse_format = ui_formats_to_parse_format.fetch(date_format) if joining_start.presence relation = relation.where("registered_at >= ?", DateTime.strptime(joining_start, parse_format).beginning_of_day) end return relation unless joining_end.presence relation.where("registered_at <= ?", DateTime.strptime(joining_end, parse_format).end_of_day) end def self.filter_organization_memberships(relation:, organizations:) sub_query = OrganizationMembership.select(:user_id).where(organization_id: organizations) relation.where(id: sub_query) end # Apply the "where" scope to the given relation for the given roles. # # @param relation [ActiveRecord::Relation] # @param roles [Array] # # @note Why not use `relation.with_roles`; As implemented in Rolify's `.with_any_role` performs # one User query per role passed. Given that we intend to use pagination, the one User # query per role is inadequate. # # @see https://github.com/RolifyCommunity/rolify/blob/0c883f4173f409766338b9c6dfc64b0fc8ec8a52/lib/rolify/finders.rb#L26-L32 def self.filter_roles(relation:, roles:, role_map: Constants::Role::ALL_ROLES_LABELS_TO_WHERE_CLAUSE) conditions = [] values = [] # Assemble the conditions and positional parameter values roles.each do |role| where_clause = role_map.fetch(role) resource_type = where_clause.fetch(:resource_type, nil) name = where_clause.fetch(:name) condition = "(#{Role.table_name}.name = ? AND #{Role.table_name}.resource_id IS NULL" values << name # We need to use `IS NULL` instead of `= NULL` as those are different meanings. if resource_type.nil? condition += " AND #{Role.table_name}.resource_type IS NULL" else condition += " AND #{Role.table_name}.resource_type = ?" values << resource_type end condition += ")" conditions << condition end sub_query = User.select(:id).distinct.joins(:roles).where(%((#{conditions.join(') OR (')})), *values) relation.where(id: sub_query) end private_class_method :filter_roles end end