summaryrefslogtreecommitdiff
path: root/app/models/concerns
diff options
context:
space:
mode:
Diffstat (limited to 'app/models/concerns')
-rw-r--r--app/models/concerns/analytics/cycle_analytics/parentable.rb22
-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.rb4
-rw-r--r--app/models/concerns/ci/has_runner_executor.rb24
-rw-r--r--app/models/concerns/counter_attribute.rb40
-rw-r--r--app/models/concerns/has_user_type.rb22
-rw-r--r--app/models/concerns/noteable.rb12
-rw-r--r--app/models/concerns/project_features_compatibility.rb9
-rw-r--r--app/models/concerns/resolvable_discussion.rb2
-rw-r--r--app/models/concerns/safely_change_column_default.rb46
-rw-r--r--app/models/concerns/update_project_statistics.rb5
-rw-r--r--app/models/concerns/work_item_resource_event.rb23
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