diff options
Diffstat (limited to 'app/models/concerns')
-rw-r--r-- | app/models/concerns/analytics/cycle_analytics/parentable.rb | 22 | ||||
-rw-r--r-- | app/models/concerns/analytics/cycle_analytics/stageable.rb (renamed from app/models/concerns/analytics/cycle_analytics/stage.rb) | 68 | ||||
-rw-r--r-- | app/models/concerns/board_recent_visit.rb | 4 | ||||
-rw-r--r-- | app/models/concerns/ci/has_runner_executor.rb | 24 | ||||
-rw-r--r-- | app/models/concerns/counter_attribute.rb | 40 | ||||
-rw-r--r-- | app/models/concerns/has_user_type.rb | 22 | ||||
-rw-r--r-- | app/models/concerns/noteable.rb | 12 | ||||
-rw-r--r-- | app/models/concerns/project_features_compatibility.rb | 9 | ||||
-rw-r--r-- | app/models/concerns/resolvable_discussion.rb | 2 | ||||
-rw-r--r-- | app/models/concerns/safely_change_column_default.rb | 46 | ||||
-rw-r--r-- | app/models/concerns/update_project_statistics.rb | 5 | ||||
-rw-r--r-- | app/models/concerns/work_item_resource_event.rb | 23 |
12 files changed, 216 insertions, 61 deletions
diff --git a/app/models/concerns/analytics/cycle_analytics/parentable.rb b/app/models/concerns/analytics/cycle_analytics/parentable.rb new file mode 100644 index 00000000000..785f6eea6bf --- /dev/null +++ b/app/models/concerns/analytics/cycle_analytics/parentable.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + module Parentable + extend ActiveSupport::Concern + + included do + belongs_to :namespace, class_name: 'Namespace', foreign_key: :group_id, optional: false # rubocop: disable Rails/InverseOf + + validate :ensure_namespace_type + + def ensure_namespace_type + return if namespace.nil? + return if namespace.is_a?(::Namespaces::ProjectNamespace) || namespace.is_a?(::Group) + + errors.add(:namespace, s_('CycleAnalytics|the assigned object is not supported')) + end + end + end + end +end diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stageable.rb index d9e6756ab86..d1f948d1366 100644 --- a/app/models/concerns/analytics/cycle_analytics/stage.rb +++ b/app/models/concerns/analytics/cycle_analytics/stageable.rb @@ -2,7 +2,7 @@ module Analytics module CycleAnalytics - module Stage + module Stageable extend ActiveSupport::Concern include RelativePositioning include Gitlab::Utils::StrongMemoize @@ -10,7 +10,7 @@ module Analytics included do belongs_to :start_event_label, class_name: 'GroupLabel', optional: true belongs_to :end_event_label, class_name: 'GroupLabel', optional: true - belongs_to :stage_event_hash, class_name: 'Analytics::CycleAnalytics::StageEventHash', foreign_key: :stage_event_hash_id, optional: true + belongs_to :stage_event_hash, class_name: 'Analytics::CycleAnalytics::StageEventHash', optional: true validates :name, presence: true validates :name, exclusion: { in: Gitlab::Analytics::CycleAnalytics::DefaultStages.names }, if: :custom? @@ -21,39 +21,31 @@ module Analytics validate :validate_stage_event_pairs validate :validate_labels - enum start_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum, _prefix: :start_event_identifier - enum end_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum, _prefix: :end_event_identifier + enum start_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum, + _prefix: :start_event_identifier + enum end_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum, + _prefix: :end_event_identifier alias_attribute :custom_stage?, :custom scope :default_stages, -> { where(custom: false) } scope :ordered, -> { order(:relative_position, :id) } scope :with_preloaded_labels, -> { includes(:start_event_label, :end_event_label) } scope :for_list, -> { with_preloaded_labels.ordered } - scope :by_value_stream, -> (value_stream) { where(value_stream_id: value_stream.id) } + scope :by_value_stream, ->(value_stream) { where(value_stream_id: value_stream.id) } before_save :ensure_stage_event_hash_id after_commit :cleanup_old_stage_event_hash end - def parent=(_) - raise NotImplementedError - end - - def parent - raise NotImplementedError - end - def start_event - strong_memoize(:start_event) do - Gitlab::Analytics::CycleAnalytics::StageEvents[start_event_identifier].new(params_for_start_event) - end + Gitlab::Analytics::CycleAnalytics::StageEvents[start_event_identifier].new(params_for_start_event) end + strong_memoize_attr :start_event def end_event - strong_memoize(:end_event) do - Gitlab::Analytics::CycleAnalytics::StageEvents[end_event_identifier].new(params_for_end_event) - end + Gitlab::Analytics::CycleAnalytics::StageEvents[end_event_identifier].new(params_for_end_event) end + strong_memoize_attr :end_event def events_hash_code Digest::SHA256.hexdigest("#{start_event.hash_code}-#{end_event.hash_code}") @@ -109,9 +101,9 @@ module Analytics def validate_stage_event_pairs return if start_event_identifier.nil? || end_event_identifier.nil? - unless pairing_rules.fetch(start_event.class, []).include?(end_event.class) - errors.add(:end_event, s_('CycleAnalytics|not allowed for the given start event')) - end + return if pairing_rules.fetch(start_event.class, []).include?(end_event.class) + + errors.add(:end_event, s_('CycleAnalytics|not allowed for the given start event')) end def pairing_rules @@ -119,21 +111,23 @@ module Analytics end def validate_labels - validate_label_within_group(:start_event_label_id, start_event_label_id) if start_event_label_id_changed? - validate_label_within_group(:end_event_label_id, end_event_label_id) if end_event_label_id_changed? + validate_label_within_namespace(:start_event_label_id, start_event_label_id) if start_event_label_id_changed? + validate_label_within_namespace(:end_event_label_id, end_event_label_id) if end_event_label_id_changed? end - def validate_label_within_group(association_name, label_id) + def validate_label_within_namespace(association_name, label_id) return unless label_id - return unless group - unless label_available_for_group?(label_id) - errors.add(association_name, s_('CycleAnalyticsStage|is not available for the selected group')) - end + return if label_available_for_namespace?(label_id) + + errors.add(association_name, s_('CycleAnalyticsStage|is not available for the selected group')) end - def label_available_for_group?(label_id) - LabelsFinder.new(nil, { group_id: group.id, include_ancestor_groups: true, only_group_labels: true }) + def label_available_for_namespace?(label_id) + subject = is_a?(::Analytics::CycleAnalytics::Stage) ? namespace : project.group + return unless subject + + LabelsFinder.new(nil, { group_id: subject.id, include_ancestor_groups: true, only_group_labels: true }) .execute(skip_authorization: true) .id_in(label_id) .exists? @@ -142,15 +136,15 @@ module Analytics def ensure_stage_event_hash_id previous_stage_event_hash = stage_event_hash&.hash_sha256 - if previous_stage_event_hash.blank? || events_hash_code != previous_stage_event_hash - self.stage_event_hash_id = Analytics::CycleAnalytics::StageEventHash.record_id_by_hash_sha256(events_hash_code) - end + return unless previous_stage_event_hash.blank? || events_hash_code != previous_stage_event_hash + + self.stage_event_hash_id = Analytics::CycleAnalytics::StageEventHash.record_id_by_hash_sha256(events_hash_code) end def cleanup_old_stage_event_hash - if stage_event_hash_id_previously_changed? && stage_event_hash_id_previously_was - Analytics::CycleAnalytics::StageEventHash.cleanup_if_unused(stage_event_hash_id_previously_was) - end + return unless stage_event_hash_id_previously_changed? && stage_event_hash_id_previously_was + + Analytics::CycleAnalytics::StageEventHash.cleanup_if_unused(stage_event_hash_id_previously_was) end end end diff --git a/app/models/concerns/board_recent_visit.rb b/app/models/concerns/board_recent_visit.rb index fd4d574ac58..c1c8307500e 100644 --- a/app/models/concerns/board_recent_visit.rb +++ b/app/models/concerns/board_recent_visit.rb @@ -9,9 +9,7 @@ module BoardRecentVisit "user" => user, board_parent_relation => board.resource_parent, board_relation => board - ).tap do |visit| - visit.touch - end + ).tap(&:touch) rescue ActiveRecord::RecordNotUnique retry end diff --git a/app/models/concerns/ci/has_runner_executor.rb b/app/models/concerns/ci/has_runner_executor.rb new file mode 100644 index 00000000000..dc70cdb2018 --- /dev/null +++ b/app/models/concerns/ci/has_runner_executor.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Ci + module HasRunnerExecutor + extend ActiveSupport::Concern + + included do + enum executor_type: { + unknown: 0, + custom: 1, + shell: 2, + docker: 3, + docker_windows: 4, + docker_ssh: 5, + ssh: 6, + parallels: 7, + virtualbox: 8, + docker_machine: 9, + docker_ssh_machine: 10, + kubernetes: 11 + }, _suffix: true + end + end +end diff --git a/app/models/concerns/counter_attribute.rb b/app/models/concerns/counter_attribute.rb index f1efbba67e1..784afd1f231 100644 --- a/app/models/concerns/counter_attribute.rb +++ b/app/models/concerns/counter_attribute.rb @@ -88,12 +88,20 @@ module CounterAttribute end def increment_counter(attribute, increment) - return if increment == 0 + return if increment.amount == 0 run_after_commit_or_now do new_value = counter(attribute).increment(increment) - log_increment_counter(attribute, increment, new_value) + log_increment_counter(attribute, increment.amount, new_value) + end + end + + def bulk_increment_counter(attribute, increments) + run_after_commit_or_now do + new_value = counter(attribute).bulk_increment(increments) + + log_increment_counter(attribute, increments.sum(&:amount), new_value) end end @@ -103,14 +111,22 @@ module CounterAttribute end end - def reset_counter!(attribute) + def initiate_refresh!(attribute) + raise ArgumentError, %(attribute "#{attribute}" cannot be refreshed) unless counter_attribute_enabled?(attribute) + detect_race_on_record(log_fields: { caller: __method__, attributes: attribute }) do - counter(attribute).reset! + counter(attribute).initiate_refresh! end log_clear_counter(attribute) end + def finalize_refresh(attribute) + raise ArgumentError, %(attribute "#{attribute}" cannot be refreshed) unless counter_attribute_enabled?(attribute) + + counter(attribute).finalize_refresh + end + def execute_after_commit_callbacks self.class.after_commit_callbacks.each do |callback| callback.call(self.reset) @@ -122,11 +138,17 @@ module CounterAttribute def build_counter_for(attribute) raise ArgumentError, %(attribute "#{attribute}" does not exist) unless has_attribute?(attribute) - if counter_attribute_enabled?(attribute) - Gitlab::Counters::BufferedCounter.new(self, attribute) - else - Gitlab::Counters::LegacyCounter.new(self, attribute) - end + return legacy_counter(attribute) unless counter_attribute_enabled?(attribute) + + buffered_counter(attribute) + end + + def legacy_counter(attribute) + Gitlab::Counters::LegacyCounter.new(self, attribute) + end + + def buffered_counter(attribute) + Gitlab::Counters::BufferedCounter.new(self, attribute) end def database_lock_key diff --git a/app/models/concerns/has_user_type.rb b/app/models/concerns/has_user_type.rb index 1af655277b8..b02c95c9662 100644 --- a/app/models/concerns/has_user_type.rb +++ b/app/models/concerns/has_user_type.rb @@ -14,20 +14,32 @@ module HasUserType migration_bot: 7, security_bot: 8, automation_bot: 9, - admin_bot: 11 + admin_bot: 11, + suggested_reviewers_bot: 12 }.with_indifferent_access.freeze - BOT_USER_TYPES = %w[alert_bot project_bot support_bot visual_review_bot migration_bot security_bot automation_bot admin_bot].freeze + BOT_USER_TYPES = %w[ + alert_bot + project_bot + support_bot + visual_review_bot + migration_bot + security_bot + automation_bot + admin_bot + suggested_reviewers_bot + ].freeze + NON_INTERNAL_USER_TYPES = %w[human project_bot service_user].freeze INTERNAL_USER_TYPES = (USER_TYPES.keys - NON_INTERNAL_USER_TYPES).freeze included do scope :humans, -> { where(user_type: :human) } scope :bots, -> { where(user_type: BOT_USER_TYPES) } - scope :without_bots, -> { humans.or(where.not(user_type: BOT_USER_TYPES)) } + scope :without_bots, -> { humans.or(where(user_type: USER_TYPES.keys - BOT_USER_TYPES)) } scope :non_internal, -> { humans.or(where(user_type: NON_INTERNAL_USER_TYPES)) } - scope :without_ghosts, -> { humans.or(where.not(user_type: :ghost)) } - scope :without_project_bot, -> { humans.or(where.not(user_type: :project_bot)) } + scope :without_ghosts, -> { humans.or(where(user_type: USER_TYPES.keys - ['ghost'])) } + scope :without_project_bot, -> { humans.or(where(user_type: USER_TYPES.keys - ['project_bot'])) } scope :human_or_service_user, -> { humans.or(where(user_type: :service_user)) } enum user_type: USER_TYPES diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb index 492d55c74e2..eed396f785b 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -88,7 +88,7 @@ module Noteable def discussions @discussions ||= discussion_notes - .inc_relations_for_view + .inc_relations_for_view(self) .discussions(self) end @@ -126,7 +126,7 @@ module Noteable def grouped_diff_discussions(*args) # Doesn't use `discussion_notes`, because this may include commit diff notes # besides MR diff notes, that we do not want to display on the MR Changes tab. - notes.inc_relations_for_view.grouped_diff_discussions(*args) + notes.inc_relations_for_view(self).grouped_diff_discussions(*args) end # rubocop:disable Gitlab/ModuleWithInstanceVariables @@ -205,6 +205,14 @@ module Noteable model_name.singular end + def commenters(user: nil) + eligable_notes = notes.user + + eligable_notes = eligable_notes.not_internal unless user&.can?(:read_internal_note, self) + + User.where(id: eligable_notes.select(:author_id).distinct) + end + private # Synthetic system notes don't have discussion IDs because these are generated dynamically diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index d37f20e2e7c..b910c0ab5c2 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -124,8 +124,13 @@ module ProjectFeaturesCompatibility private def write_feature_attribute_boolean(field, value) - access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED - write_feature_attribute_raw(field, access_level) + value_type = Gitlab::Utils.to_boolean(value) + if value_type.in?([true, false]) + access_level = value_type ? ProjectFeature::ENABLED : ProjectFeature::DISABLED + write_feature_attribute_raw(field, access_level) + else + write_feature_attribute_string(field, value) + end end def write_feature_attribute_string(field, value) diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb index 92a88d2f7c8..141c480ea1f 100644 --- a/app/models/concerns/resolvable_discussion.rb +++ b/app/models/concerns/resolvable_discussion.rb @@ -96,7 +96,7 @@ module ResolvableDiscussion def unresolve! return unless resolvable? - update { |notes| notes.unresolve! } + update(&:unresolve!) end def clear_memoized_values diff --git a/app/models/concerns/safely_change_column_default.rb b/app/models/concerns/safely_change_column_default.rb new file mode 100644 index 00000000000..567f690d950 --- /dev/null +++ b/app/models/concerns/safely_change_column_default.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# == SafelyChangeColumnDefault concern. +# +# Contains functionality that allows safely changing a column default without downtime. +# Without this concern, Rails can mutate the old default value to the new default value if the old default is explicitly +# specified. +# +# Usage: +# +# class SomeModel < ApplicationRecord +# include SafelyChangeColumnDefault +# +# columns_changing_default :value +# end +# +# # Assume a default of 100 for value +# SomeModel.create!(value: 100) # INSERT INTO some_model (value) VALUES (100) +# change_column_default('some_model', 'value', from: 100, to: 101) +# SomeModel.create!(value: 100) # INSERT INTO some_model (value) VALUES (100) +# # Without this concern, would be INSERT INTO some_model (value) DEFAULT VALUES and would insert 101. +module SafelyChangeColumnDefault + extend ActiveSupport::Concern + + class_methods do + # Indicate that one or more columns will have their database default change. + # + # By indicating those columns here, this helper prevents a case where explicitly writing the old database default + # will be mutated to the new database default. + def columns_changing_default(*columns) + self.columns_with_changing_default = columns.map(&:to_s) + end + end + + included do + class_attribute :columns_with_changing_default, default: [] + + before_create do + columns_with_changing_default.to_a.each do |attr_name| + attr = @attributes[attr_name] + + attribute_will_change!(attr_name) if !attr.changed? && attr.came_from_user? + end + end + end +end diff --git a/app/models/concerns/update_project_statistics.rb b/app/models/concerns/update_project_statistics.rb index 586f1dbb65c..89398537e0a 100644 --- a/app/models/concerns/update_project_statistics.rb +++ b/app/models/concerns/update_project_statistics.rb @@ -78,9 +78,10 @@ module UpdateProjectStatistics return if delta == 0 return if project.nil? + increment = Gitlab::Counters::Increment.new(amount: delta, ref: id) + run_after_commit do - ProjectStatistics.increment_statistic( - project, self.class.project_statistics_name, delta) + ProjectStatistics.increment_statistic(project, self.class.project_statistics_name, increment) end end end diff --git a/app/models/concerns/work_item_resource_event.rb b/app/models/concerns/work_item_resource_event.rb new file mode 100644 index 00000000000..d0323feb029 --- /dev/null +++ b/app/models/concerns/work_item_resource_event.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module WorkItemResourceEvent + extend ActiveSupport::Concern + + included do + belongs_to :work_item, foreign_key: 'issue_id' + end + + def work_item_synthetic_system_note(events: nil) + # System notes for label resource events are handled in batches, so that we have single system note for multiple + # label changes. + if is_a?(ResourceLabelEvent) && events.present? + return synthetic_note_class.from_events(events, resource: work_item, resource_parent: work_item.project) + end + + synthetic_note_class.from_event(self, resource: work_item, resource_parent: work_item.project) + end + + def synthetic_note_class + raise NoMethodError, 'must implement `synthetic_note_class` method' + end +end |