2021-06-01 12:20:20 +00:00
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
2017-05-02 15:21:13 +00:00
module ApplicationModel::CanAssociations
2017-01-31 17:13:45 +00:00
extend ActiveSupport :: Concern
= begin
set relations of model based on params
model = Model . find ( 1 )
result = model . associations_from_param ( params )
returns
result = true | false
= end
def associations_from_param ( params )
2017-06-16 20:43:09 +00:00
# special handling for group access association
{
groups : :group_names_access_map = ,
group_ids : :group_ids_access_map =
} . each do | param , setter |
2018-02-28 11:58:10 +00:00
next if ! params . key? ( param )
2018-10-09 06:17:41 +00:00
2017-06-16 20:43:09 +00:00
map = params [ param ]
next if ! respond_to? ( setter )
2018-10-09 06:17:41 +00:00
2017-06-16 20:43:09 +00:00
send ( setter , map )
end
2017-01-31 17:13:45 +00:00
# set relations by id/verify if ref exists
2017-10-01 12:25:52 +00:00
self . class . reflect_on_all_associations . map do | assoc |
2017-01-31 17:13:45 +00:00
assoc_name = assoc . name
2017-06-16 20:43:09 +00:00
next if association_attributes_ignored . include? ( assoc_name )
2018-10-09 06:17:41 +00:00
2020-09-30 09:07:01 +00:00
real_ids = " #{ assoc_name [ 0 , assoc_name . length - 1 ] } _ids "
2017-01-31 17:13:45 +00:00
real_ids = real_ids . to_sym
next if ! params . key? ( real_ids )
2018-10-09 06:17:41 +00:00
2017-01-31 17:13:45 +00:00
list_of_items = params [ real_ids ]
if ! params [ real_ids ] . instance_of? ( Array )
list_of_items = [ params [ real_ids ] ]
end
list = [ ]
2017-10-01 12:25:52 +00:00
list_of_items . each do | item_id |
2017-01-31 17:13:45 +00:00
next if ! item_id
2018-10-09 06:17:41 +00:00
2017-01-31 17:13:45 +00:00
lookup = assoc . klass . lookup ( id : item_id )
# complain if we found no reference
if ! lookup
2020-03-06 14:31:43 +00:00
raise Exceptions :: UnprocessableEntity , " No value found for ' #{ assoc_name } ' with id #{ item_id . inspect } "
2017-01-31 17:13:45 +00:00
end
2018-10-09 06:17:41 +00:00
2017-01-31 17:13:45 +00:00
list . push item_id
2017-10-01 12:25:52 +00:00
end
2017-01-31 17:13:45 +00:00
send ( " #{ real_ids } = " , list )
2017-10-01 12:25:52 +00:00
end
2017-01-31 17:13:45 +00:00
# set relations by name/lookup
2017-10-01 12:25:52 +00:00
self . class . reflect_on_all_associations . map do | assoc |
2017-01-31 17:13:45 +00:00
assoc_name = assoc . name
2017-06-16 20:43:09 +00:00
next if association_attributes_ignored . include? ( assoc_name )
2018-10-09 06:17:41 +00:00
2020-09-30 09:07:01 +00:00
real_ids = " #{ assoc_name [ 0 , assoc_name . length - 1 ] } _ids "
2017-01-31 17:13:45 +00:00
next if ! respond_to? ( real_ids )
2018-10-09 06:17:41 +00:00
2020-09-30 09:07:01 +00:00
real_values = " #{ assoc_name [ 0 , assoc_name . length - 1 ] } s "
2017-01-31 17:13:45 +00:00
real_values = real_values . to_sym
next if ! respond_to? ( real_values )
next if ! params [ real_values ]
2018-10-09 06:17:41 +00:00
2018-02-20 04:29:30 +00:00
if params [ real_values ] . instance_of? ( String ) || params [ real_values ] . instance_of? ( Integer ) || params [ real_values ] . instance_of? ( Float )
params [ real_values ] = [ params [ real_values ] ]
end
2017-01-31 17:13:45 +00:00
next if ! params [ real_values ] . instance_of? ( Array )
2018-10-09 06:17:41 +00:00
2017-01-31 17:13:45 +00:00
list = [ ]
class_object = assoc . klass
2017-10-01 12:25:52 +00:00
params [ real_values ] . each do | value |
2017-01-31 17:13:45 +00:00
lookup = nil
if class_object == User
if ! lookup
lookup = class_object . lookup ( login : value )
end
if ! lookup
lookup = class_object . lookup ( email : value )
end
else
lookup = class_object . lookup ( name : value )
end
# complain if we found no reference
if ! lookup
2020-03-06 14:31:43 +00:00
raise Exceptions :: UnprocessableEntity , " No lookup value found for ' #{ assoc_name } ': #{ value . inspect } "
2017-01-31 17:13:45 +00:00
end
2018-10-09 06:17:41 +00:00
2017-01-31 17:13:45 +00:00
list . push lookup . id
2017-10-01 12:25:52 +00:00
end
2017-01-31 17:13:45 +00:00
send ( " #{ real_ids } = " , list )
2017-10-01 12:25:52 +00:00
end
2017-01-31 17:13:45 +00:00
end
= begin
get relations of model based on params
model = Model . find ( 1 )
attributes = model . attributes_with_association_ids
returns
hash with attributes and association ids
= end
def attributes_with_association_ids
key = " #{ self . class } ::aws:: #{ id } "
2021-05-31 13:05:54 +00:00
cache = Cache . read ( key )
2017-01-31 17:13:45 +00:00
return cache if cache
attributes = self . attributes
2017-11-23 08:09:44 +00:00
relevant = % i [ has_and_belongs_to_many has_many ]
2017-10-18 09:21:10 +00:00
eager_load = [ ]
pluck = [ ]
keys = [ ]
self . class . reflect_on_all_associations . each do | assoc |
next if relevant . exclude? ( assoc . macro )
assoc_name = assoc . name
next if association_attributes_ignored . include? ( assoc_name )
eager_load . push ( assoc_name )
2019-07-04 11:16:55 +00:00
pluck . push ( Arel . sql ( " #{ ActiveRecord :: Base . connection . quote_table_name ( assoc . table_name ) } .id AS #{ ActiveRecord :: Base . connection . quote_table_name ( assoc_name ) } " ) )
2017-10-18 09:21:10 +00:00
keys . push ( " #{ assoc_name . to_s . singularize } _ids " )
end
if eager_load . present?
ids = self . class . eager_load ( eager_load )
. where ( id : id )
. pluck ( * pluck )
if keys . size > 1
values = ids . transpose . map ( & :compact ) . map ( & :uniq )
attributes . merge! ( keys . zip ( values ) . to_h )
else
attributes [ keys . first ] = ids . compact
end
2017-10-01 12:25:52 +00:00
end
2017-02-15 12:20:42 +00:00
2017-06-16 20:43:09 +00:00
# special handling for group access associations
if respond_to? ( :group_ids_access_map )
attributes [ 'group_ids' ] = send ( :group_ids_access_map )
end
2017-02-15 12:20:42 +00:00
filter_attributes ( attributes )
2017-01-31 17:13:45 +00:00
Cache . write ( key , attributes )
attributes
end
= begin
get relation name of model based on params
model = Model . find ( 1 )
attributes = model . attributes_with_association_names
returns
hash with attributes , association ids , association names and relation name
= end
def attributes_with_association_names
# get relations
attributes = attributes_with_association_ids
2017-10-01 12:25:52 +00:00
self . class . reflect_on_all_associations . map do | assoc |
2017-01-31 17:13:45 +00:00
next if ! respond_to? ( assoc . name )
2017-06-16 20:43:09 +00:00
next if association_attributes_ignored . include? ( assoc . name )
2018-10-09 06:17:41 +00:00
2017-01-31 17:13:45 +00:00
ref = send ( assoc . name )
next if ! ref
2018-10-09 06:17:41 +00:00
2017-01-31 17:13:45 +00:00
if ref . respond_to? ( :first )
attributes [ assoc . name . to_s ] = [ ]
2017-10-01 12:25:52 +00:00
ref . each do | item |
2017-01-31 17:13:45 +00:00
if item [ :login ]
attributes [ assoc . name . to_s ] . push item [ :login ]
next
end
next if ! item [ :name ]
2018-10-09 06:17:41 +00:00
2017-01-31 17:13:45 +00:00
attributes [ assoc . name . to_s ] . push item [ :name ]
2017-10-01 12:25:52 +00:00
end
2017-11-23 08:09:44 +00:00
if ref . count . positive? && attributes [ assoc . name . to_s ] . blank?
2017-01-31 17:13:45 +00:00
attributes . delete ( assoc . name . to_s )
end
next
end
if ref [ :login ]
attributes [ assoc . name . to_s ] = ref [ :login ]
next
end
next if ! ref [ :name ]
2018-10-09 06:17:41 +00:00
2017-01-31 17:13:45 +00:00
attributes [ assoc . name . to_s ] = ref [ :name ]
2017-10-01 12:25:52 +00:00
end
2017-01-31 17:13:45 +00:00
2017-06-16 20:43:09 +00:00
# special handling for group access associations
if respond_to? ( :group_names_access_map )
attributes [ 'groups' ] = send ( :group_names_access_map )
end
2017-01-31 17:13:45 +00:00
# fill created_by/updated_by
{
'created_by_id' = > 'created_by' ,
'updated_by_id' = > 'updated_by' ,
2017-10-01 12:25:52 +00:00
} . each do | source , destination |
2017-01-31 17:13:45 +00:00
next if ! attributes [ source ]
2018-10-09 06:17:41 +00:00
2017-01-31 17:13:45 +00:00
user = User . lookup ( id : attributes [ source ] )
next if ! user
2018-10-09 06:17:41 +00:00
2017-01-31 17:13:45 +00:00
attributes [ destination ] = user . login
2017-10-01 12:25:52 +00:00
end
2017-01-31 17:13:45 +00:00
2017-02-15 12:20:42 +00:00
filter_attributes ( attributes )
attributes
end
def filter_attributes ( attributes )
2019-07-31 08:23:48 +00:00
# remove forbidden attributes
2020-02-12 14:25:31 +00:00
attributes . except! ( 'password' , 'token' , 'tokens' , 'token_ids' )
2017-01-31 17:13:45 +00:00
end
= begin
reference if association id check
model = Model . find ( 123 )
attributes = model . association_id_validation ( 'attribute_id' , value )
returns
true | false
= end
def association_id_validation ( attribute_id , value )
return true if value . nil?
2017-11-23 08:09:44 +00:00
attributes . each_key do | key |
2017-01-31 17:13:45 +00:00
next if key != attribute_id
# check if id is assigned
next if ! key . end_with? ( '_id' )
2018-10-09 06:17:41 +00:00
2017-01-31 17:13:45 +00:00
key_short = key . chomp ( '_id' )
2017-10-01 12:25:52 +00:00
self . class . reflect_on_all_associations . map do | assoc |
2017-01-31 17:13:45 +00:00
next if assoc . name . to_s != key_short
2018-10-09 06:17:41 +00:00
2017-01-31 17:13:45 +00:00
item = assoc . class_name . constantize
return false if ! item . respond_to? ( :find_by )
2018-10-09 06:17:41 +00:00
2017-01-31 17:13:45 +00:00
ref_object = item . find_by ( id : value )
return false if ! ref_object
2018-10-09 06:17:41 +00:00
2017-01-31 17:13:45 +00:00
return true
2017-10-01 12:25:52 +00:00
end
end
2017-01-31 17:13:45 +00:00
true
end
2017-06-16 20:43:09 +00:00
private
def association_attributes_ignored
@association_attributes_ignored || = self . class . instance_variable_get ( :@association_attributes_ignored ) || [ ]
end
2017-01-31 17:13:45 +00:00
# methods defined here are going to extend the class, not the instance of it
class_methods do
= begin
2019-07-31 08:23:48 +00:00
serve method to ignore model attribute associations
2017-01-31 17:13:45 +00:00
class Model < ApplicationModel
include AssociationConcern
2017-06-16 20:43:09 +00:00
association_attributes_ignored :users
2017-01-31 17:13:45 +00:00
end
= end
def association_attributes_ignored ( * attributes )
2017-06-16 20:43:09 +00:00
@association_attributes_ignored || = [ ]
@association_attributes_ignored |= attributes
2017-01-31 17:13:45 +00:00
end
= begin
do name / login / email based lookup for associations
params = {
login : 'some login' ,
firstname : 'some firstname' ,
lastname : 'some lastname' ,
email : 'some email' ,
organization : 'some organization' ,
roles : [ 'Agent' , 'Admin' ] ,
}
attributes = Model . association_name_to_id_convert ( params )
returns
attributes = params # params with possible lookups
attributes = {
login : 'some login' ,
firstname : 'some firstname' ,
lastname : 'some lastname' ,
email : 'some email' ,
organization_id : 123 ,
role_ids : [ 2 , 1 ] ,
}
= end
def association_name_to_id_convert ( params )
2017-09-08 08:28:34 +00:00
if params . respond_to? ( :permit! )
params = params . permit! . to_h
end
2017-01-31 17:13:45 +00:00
data = { }
2017-10-01 12:25:52 +00:00
params . each do | key , value |
2017-01-31 17:13:45 +00:00
data [ key . to_sym ] = value
2017-10-01 12:25:52 +00:00
end
2017-01-31 17:13:45 +00:00
data . symbolize_keys!
available_attributes = attribute_names
2017-10-01 12:25:52 +00:00
reflect_on_all_associations . map do | assoc |
2017-01-31 17:13:45 +00:00
assoc_name = assoc . name
value = data [ assoc_name ]
next if ! value # next if we do not have a value
2018-10-09 06:17:41 +00:00
2017-01-31 17:13:45 +00:00
ref_name = " #{ assoc_name } _id "
# handle _id values
if available_attributes . include? ( ref_name ) # if we do have an _id attribute
next if data [ ref_name . to_sym ] # next if we have already the _id filled
# get association class and do lookup
class_object = assoc . klass
lookup = nil
if class_object == User
2017-11-23 08:09:44 +00:00
if ! value . instance_of? ( String )
2020-03-06 14:31:43 +00:00
raise Exceptions :: UnprocessableEntity , " String is needed as ref value #{ value . inspect } for ' #{ assoc_name } ' "
2017-01-31 17:13:45 +00:00
end
2018-10-09 06:17:41 +00:00
2017-11-23 08:09:44 +00:00
if ! lookup
lookup = class_object . lookup ( login : value )
end
if ! lookup
lookup = class_object . lookup ( email : value )
end
2017-01-31 17:13:45 +00:00
else
lookup = class_object . lookup ( name : value )
end
# complain if we found no reference
if ! lookup
2020-03-06 14:31:43 +00:00
raise Exceptions :: UnprocessableEntity , " No lookup value found for ' #{ assoc_name } ': #{ value . inspect } "
2017-01-31 17:13:45 +00:00
end
# release data value
data . delete ( assoc_name )
# remember id reference
data [ ref_name . to_sym ] = lookup . id
next
end
next if ! value . instance_of? ( Array )
2017-11-23 08:09:44 +00:00
next if value . blank?
2017-01-31 17:13:45 +00:00
next if ! value [ 0 ] . instance_of? ( String )
# handle _ids values
next if ! assoc_name . to_s . end_with? ( 's' )
2018-10-09 06:17:41 +00:00
2017-01-31 17:13:45 +00:00
ref_names = " #{ assoc_name . to_s . chomp ( 's' ) } _ids "
generic_object_tmp = new
2018-05-04 14:05:10 +00:00
next if ! generic_object_tmp . respond_to? ( ref_names ) # if we do have an _ids attribute
2017-01-31 17:13:45 +00:00
next if data [ ref_names . to_sym ] # next if we have already the _ids filled
# get association class and do lookup
class_object = assoc . klass
lookup_ids = [ ]
2017-10-01 12:25:52 +00:00
value . each do | item |
2017-01-31 17:13:45 +00:00
lookup = nil
if class_object == User
2017-11-23 08:09:44 +00:00
if ! item . instance_of? ( String )
2020-03-06 14:31:43 +00:00
raise Exceptions :: UnprocessableEntity , " String is needed in array ref as ref value #{ value . inspect } for ' #{ assoc_name } ' "
2017-01-31 17:13:45 +00:00
end
2018-10-09 06:17:41 +00:00
2017-11-23 08:09:44 +00:00
if ! lookup
lookup = class_object . lookup ( login : item )
end
if ! lookup
lookup = class_object . lookup ( email : item )
end
2017-01-31 17:13:45 +00:00
else
lookup = class_object . lookup ( name : item )
end
# complain if we found no reference
if ! lookup
2020-03-06 14:31:43 +00:00
raise Exceptions :: UnprocessableEntity , " No lookup value found for ' #{ assoc_name } ': #{ item . inspect } "
2017-01-31 17:13:45 +00:00
end
2018-10-09 06:17:41 +00:00
2017-01-31 17:13:45 +00:00
lookup_ids . push lookup . id
2017-10-01 12:25:52 +00:00
end
2017-01-31 17:13:45 +00:00
# release data value
data . delete ( assoc_name )
# remember id reference
data [ ref_names . to_sym ] = lookup_ids
2017-10-01 12:25:52 +00:00
end
2017-01-31 17:13:45 +00:00
data
end
end
end