1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
|
# frozen_string_literal: true
module CascadingProjectSettingAttribute
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
class_methods do
private
# logic based on the cascading setting logic
# CascadingNamespaceSettingAttribute
#
# Code remove for this module:
# - logic related to 'lock_#{attribute}', because projects don't need to lock attributes.
# - logic related to descendants, because projects don't have descendants.
# - logic related to a `nil` value for the setting, because the first/only
# cascading project setting (`duo_features_enabled`) has a db-level not nil constraint.
def cascading_attr(*attributes)
attributes.map(&:to_sym).each do |attribute|
# public methods
define_attr_reader(attribute)
define_attr_writer(attribute)
define_lock_methods(attribute)
# private methods
define_validator_methods(attribute)
define_attr_before_save(attribute)
validate :"#{attribute}_changeable?"
before_save :"before_save_#{attribute}", if: -> { will_save_change_to_attribute?(attribute) }
end
end
def define_attr_reader(attribute)
define_method(attribute) do
strong_memoize(attribute) do
next self[attribute] if will_save_change_to_attribute?(attribute)
next locked_value(attribute) if cascading_attribute_locked?(attribute)
next self[attribute] unless self[attribute].nil?
cascaded_value = cascaded_ancestor_value(attribute)
next cascaded_value unless cascaded_value.nil?
application_setting_value(attribute)
end
end
end
def define_attr_writer(attribute)
define_method("#{attribute}=") do |value|
return value if read_attribute(attribute).nil? && to_bool(value) == cascaded_ancestor_value(attribute)
clear_memoization(attribute)
super(value)
end
end
def define_attr_before_save(attribute)
# rubocop:disable GitlabSecurity/PublicSend -- model attribute, not user input
define_method("before_save_#{attribute}") do
new_value = public_send(attribute)
return unless public_send("#{attribute}_was").nil? && new_value == cascaded_ancestor_value(attribute)
write_attribute(attribute, nil)
end
# rubocop:enable GitlabSecurity/PublicSend
private :"before_save_#{attribute}"
end
def define_lock_methods(attribute)
define_method("#{attribute}_locked?") do
cascading_attribute_locked?(attribute)
end
define_method("#{attribute}_locked_by_ancestor?") do
locked_by_ancestor?(attribute)
end
define_method("#{attribute}_locked_by_application_setting?") do
locked_by_application_setting?(attribute)
end
define_method("#{attribute}_locked_ancestor") do
locked_ancestor(attribute)
end
end
def define_validator_methods(attribute)
define_method("#{attribute}_changeable?") do
return unless cascading_attribute_changed?(attribute)
return unless cascading_attribute_locked?(attribute)
errors.add(attribute, s_('CascadingSettings|cannot be changed because it is locked by an ancestor'))
end
private :"#{attribute}_changeable?"
end
end
private
def locked_value(attribute)
return application_setting_value(attribute) if locked_by_application_setting?(attribute)
ancestor = locked_ancestor(attribute)
ancestor.read_attribute(attribute) if ancestor
end
def locked_ancestor(attribute)
return unless direct_ancestor_present?
strong_memoize(:"#{attribute}_locked_ancestor") do
NamespaceSetting
.select(:namespace_id, "lock_#{attribute}", attribute)
.where(namespace_id: namespace_ancestor_ids)
.where(NamespaceSetting.arel_table["lock_#{attribute}"].eq(true))
.limit(1).load.first
end
end
def locked_by_ancestor?(attribute)
locked_ancestor(attribute).present?
end
def locked_by_application_setting?(attribute)
Gitlab::CurrentSettings.public_send("lock_#{attribute}") # rubocop:disable GitlabSecurity/PublicSend -- model attribute, not user input
end
def cascading_attribute_locked?(attribute)
locked_by_ancestor?(attribute) || locked_by_application_setting?(attribute)
end
def cascading_attribute_changed?(attribute)
public_send("#{attribute}_changed?") # rubocop:disable GitlabSecurity/PublicSend -- model attribute, not user input
end
def cascaded_ancestor_value(attribute)
return unless direct_ancestor_present?
NamespaceSetting
.select(attribute)
.joins(
"join unnest(ARRAY[#{namespace_ancestor_ids_joined}]) with ordinality t(namespace_id, ord) USING (namespace_id)"
)
.where("#{attribute} IS NOT NULL")
.order('t.ord')
.limit(1).first&.read_attribute(attribute)
end
def application_setting_value(attribute)
Gitlab::CurrentSettings.public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend -- model attribute, not user input
end
def direct_ancestor_present?
project.group.present?
end
def namespace_ancestor_ids_joined
namespace_ancestor_ids.join(',')
end
def namespace_ancestor_ids
project.project_namespace.ancestor_ids(hierarchy_order: :asc)
end
strong_memoize_attr :namespace_ancestor_ids
def to_bool(value)
ActiveModel::Type::Boolean.new.cast(value)
end
end
|