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/stage.rb12
-rw-r--r--app/models/concerns/atomic_internal_id.rb77
-rw-r--r--app/models/concerns/avatarable.rb2
-rw-r--r--app/models/concerns/checksummable.rb11
-rw-r--r--app/models/concerns/ci/contextable.rb1
-rw-r--r--app/models/concerns/ci/pipeline_delegator.rb3
-rw-r--r--app/models/concerns/deployable.rb31
-rw-r--r--app/models/concerns/deployment_platform.rb6
-rw-r--r--app/models/concerns/group_api_compatibility.rb22
-rw-r--r--app/models/concerns/has_status.rb22
-rw-r--r--app/models/concerns/issuable.rb54
-rw-r--r--app/models/concerns/issuable_states.rb22
-rw-r--r--app/models/concerns/mentionable.rb2
-rw-r--r--app/models/concerns/milestoneish.rb6
-rw-r--r--app/models/concerns/noteable.rb2
-rw-r--r--app/models/concerns/notification_branch_selection.rb2
-rw-r--r--app/models/concerns/prometheus_adapter.rb2
-rw-r--r--app/models/concerns/relative_positioning.rb2
-rw-r--r--app/models/concerns/routable.rb11
-rw-r--r--app/models/concerns/spammable.rb5
-rw-r--r--app/models/concerns/stepable.rb8
-rw-r--r--app/models/concerns/versioned_description.rb31
-rw-r--r--app/models/concerns/worker_attributes.rb48
23 files changed, 298 insertions, 84 deletions
diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stage.rb
index 0c603c2d5e6..54e9a13d1ea 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stage.rb
@@ -7,6 +7,7 @@ module Analytics
included do
validates :name, presence: true
+ validates :name, exclusion: { in: Gitlab::Analytics::CycleAnalytics::DefaultStages.names }, if: :custom?
validates :start_event_identifier, presence: true
validates :end_event_identifier, presence: true
validate :validate_stage_event_pairs
@@ -15,6 +16,7 @@ module Analytics
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) }
end
def parent=(_)
@@ -45,11 +47,17 @@ module Analytics
!custom
end
- # The model that is going to be queried, Issue or MergeRequest
- def subject_model
+ # The model class that is going to be queried, Issue or MergeRequest
+ def subject_class
start_event.object_type
end
+ def matches_with_stage_params?(stage_params)
+ default_stage? &&
+ start_event_identifier.to_s.eql?(stage_params[:start_event_identifier].to_s) &&
+ end_event_identifier.to_s.eql?(stage_params[:end_event_identifier].to_s)
+ end
+
private
def validate_stage_event_pairs
diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb
index dc1735a7e48..64df265dc25 100644
--- a/app/models/concerns/atomic_internal_id.rb
+++ b/app/models/concerns/atomic_internal_id.rb
@@ -27,40 +27,73 @@ module AtomicInternalId
extend ActiveSupport::Concern
class_methods do
- def has_internal_id(column, scope:, init:, presence: true) # rubocop:disable Naming/PredicateName
+ def has_internal_id(column, scope:, init:, ensure_if: nil, presence: true) # rubocop:disable Naming/PredicateName
# We require init here to retain the ability to recalculate in the absence of a
# InternaLId record (we may delete records in `internal_ids` for example).
raise "has_internal_id requires a init block, none given." unless init
+ raise "has_internal_id needs to be defined on association." unless self.reflect_on_association(scope)
- before_validation :"ensure_#{scope}_#{column}!", on: :create
+ before_validation :"track_#{scope}_#{column}!", on: :create
+ before_validation :"ensure_#{scope}_#{column}!", on: :create, if: ensure_if
validates column, presence: presence
define_method("ensure_#{scope}_#{column}!") do
- scope_value = association(scope).reader
+ scope_value = internal_id_read_scope(scope)
value = read_attribute(column)
-
return value unless scope_value
- scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value }
- usage = self.class.table_name.to_sym
-
- if value.present?
- InternalId.track_greatest(self, scope_attrs, usage, value, init)
- else
- value = InternalId.generate_next(self, scope_attrs, usage, init)
+ if value.nil?
+ # We don't have a value yet and use a InternalId record to generate
+ # the next value.
+ value = InternalId.generate_next(
+ self,
+ internal_id_scope_attrs(scope),
+ internal_id_scope_usage,
+ init)
write_attribute(column, value)
end
value
end
+ define_method("track_#{scope}_#{column}!") do
+ return unless @internal_id_needs_tracking
+
+ scope_value = internal_id_read_scope(scope)
+ return unless scope_value
+
+ value = read_attribute(column)
+
+ if value.present?
+ # The value was set externally, e.g. by the user
+ # We update the InternalId record to keep track of the greatest value.
+ InternalId.track_greatest(
+ self,
+ internal_id_scope_attrs(scope),
+ internal_id_scope_usage,
+ value,
+ init)
+
+ @internal_id_needs_tracking = false
+ end
+ end
+
+ define_method("#{column}=") do |value|
+ super(value).tap do |v|
+ # Indicate the iid was set from externally
+ @internal_id_needs_tracking = true
+ end
+ end
+
define_method("reset_#{scope}_#{column}") do
if value = read_attribute(column)
- scope_value = association(scope).reader
- scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value }
- usage = self.class.table_name.to_sym
+ did_reset = InternalId.reset(
+ self,
+ internal_id_scope_attrs(scope),
+ internal_id_scope_usage,
+ value)
- if InternalId.reset(self, scope_attrs, usage, value)
+ if did_reset
write_attribute(column, nil)
end
end
@@ -69,4 +102,18 @@ module AtomicInternalId
end
end
end
+
+ def internal_id_scope_attrs(scope)
+ scope_value = internal_id_read_scope(scope)
+
+ { scope_value.class.table_name.singularize.to_sym => scope_value } if scope_value
+ end
+
+ def internal_id_scope_usage
+ self.class.table_name.to_sym
+ end
+
+ def internal_id_read_scope(scope)
+ association(scope).reader
+ end
end
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index 269145309fc..a98baeb0e3d 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -38,7 +38,7 @@ module Avatarable
def avatar_type
unless self.avatar.image?
- errors.add :avatar, "file format is not supported. Please try one of the following supported formats: #{AvatarUploader::IMAGE_EXT.join(', ')}"
+ errors.add :avatar, "file format is not supported. Please try one of the following supported formats: #{AvatarUploader::SAFE_IMAGE_EXT.join(', ')}"
end
end
diff --git a/app/models/concerns/checksummable.rb b/app/models/concerns/checksummable.rb
new file mode 100644
index 00000000000..1f76eb87aa5
--- /dev/null
+++ b/app/models/concerns/checksummable.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Checksummable
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def hexdigest(path)
+ Digest::SHA256.file(path).hexdigest
+ end
+ end
+end
diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb
index 91dda803031..49d6f3d399c 100644
--- a/app/models/concerns/ci/contextable.rb
+++ b/app/models/concerns/ci/contextable.rb
@@ -78,6 +78,7 @@ module Ci
variables.append(key: "CI_JOB_MANUAL", value: 'true') if action?
variables.append(key: "CI_NODE_INDEX", value: self.options[:instance].to_s) if self.options&.include?(:instance)
variables.append(key: "CI_NODE_TOTAL", value: (self.options&.dig(:parallel) || 1).to_s)
+ variables.append(key: "CI_DEFAULT_BRANCH", value: project.default_branch)
variables.concat(legacy_variables)
end
end
diff --git a/app/models/concerns/ci/pipeline_delegator.rb b/app/models/concerns/ci/pipeline_delegator.rb
index dbc5ed1bc9a..76e0cbc7dff 100644
--- a/app/models/concerns/ci/pipeline_delegator.rb
+++ b/app/models/concerns/ci/pipeline_delegator.rb
@@ -15,7 +15,8 @@ module Ci
:merge_request_ref?,
:source_ref,
:source_ref_slug,
- :legacy_detached_merge_request_pipeline?, to: :pipeline
+ :legacy_detached_merge_request_pipeline?,
+ :merge_train_pipeline?, to: :pipeline
end
end
end
diff --git a/app/models/concerns/deployable.rb b/app/models/concerns/deployable.rb
deleted file mode 100644
index 957b72f3721..00000000000
--- a/app/models/concerns/deployable.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-module Deployable
- extend ActiveSupport::Concern
-
- included do
- after_create :create_deployment
-
- def create_deployment
- return unless starts_environment? && !has_deployment?
-
- environment = project.environments.find_or_create_by(
- name: expanded_environment_name
- )
-
- # If we failed to persist envirionment record by validation error, such as name with invalid character,
- # the job will fall back to a non-environment job.
- return unless environment.persisted?
-
- create_deployment!(
- cluster_id: environment.deployment_platform&.cluster_id,
- project_id: environment.project_id,
- environment: environment,
- ref: ref,
- tag: tag,
- sha: sha,
- user: user,
- on_stop: on_stop)
- end
- end
-end
diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb
index e1a8725e728..fe8e9609820 100644
--- a/app/models/concerns/deployment_platform.rb
+++ b/app/models/concerns/deployment_platform.rb
@@ -11,6 +11,10 @@ module DeploymentPlatform
private
+ def cluster_management_project_enabled?
+ Feature.enabled?(:cluster_management_project, default_enabled: true)
+ end
+
def find_deployment_platform(environment)
find_platform_kubernetes_with_cte(environment) ||
find_instance_cluster_platform_kubernetes(environment: environment)
@@ -18,7 +22,7 @@ module DeploymentPlatform
# EE would override this and utilize environment argument
def find_platform_kubernetes_with_cte(_environment)
- Clusters::ClustersHierarchy.new(self).base_and_ancestors
+ Clusters::ClustersHierarchy.new(self, include_management_project: cluster_management_project_enabled?).base_and_ancestors
.enabled.default_environment
.first&.platform_kubernetes
end
diff --git a/app/models/concerns/group_api_compatibility.rb b/app/models/concerns/group_api_compatibility.rb
new file mode 100644
index 00000000000..f02aa2035e5
--- /dev/null
+++ b/app/models/concerns/group_api_compatibility.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+# Add methods used by the groups API
+module GroupAPICompatibility
+ extend ActiveSupport::Concern
+
+ def project_creation_level_str
+ ::Gitlab::Access.project_creation_string_options.key(project_creation_level)
+ end
+
+ def project_creation_level_str=(value)
+ write_attribute(:project_creation_level, ::Gitlab::Access.project_creation_string_options.fetch(value))
+ end
+
+ def subgroup_creation_level_str
+ ::Gitlab::Access.subgroup_creation_string_options.key(subgroup_creation_level)
+ end
+
+ def subgroup_creation_level_str=(value)
+ write_attribute(:subgroup_creation_level, ::Gitlab::Access.subgroup_creation_string_options.fetch(value))
+ end
+end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index bcbbb27a9a8..c01fb4740e5 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -10,6 +10,8 @@ module HasStatus
ACTIVE_STATUSES = %w[preparing pending running].freeze
COMPLETED_STATUSES = %w[success failed canceled skipped].freeze
ORDERED_STATUSES = %w[failed preparing pending running manual scheduled canceled success skipped created].freeze
+ PASSED_WITH_WARNINGS_STATUSES = %w[failed canceled].to_set.freeze
+ EXCLUDE_IGNORED_STATUSES = %w[manual failed canceled].to_set.freeze
STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3,
failed: 4, canceled: 5, skipped: 6, manual: 7,
scheduled: 8, preparing: 9 }.freeze
@@ -17,7 +19,7 @@ module HasStatus
UnknownStatusError = Class.new(StandardError)
class_methods do
- def status_sql
+ def legacy_status_sql
scope_relevant = respond_to?(:exclude_ignored) ? exclude_ignored : all
scope_warnings = respond_to?(:failed_but_allowed) ? failed_but_allowed : none
@@ -53,8 +55,22 @@ module HasStatus
)
end
- def status
- all.pluck(status_sql).first
+ def legacy_status
+ all.pluck(legacy_status_sql).first
+ end
+
+ # This method should not be used.
+ # This method performs expensive calculation of status:
+ # 1. By plucking all related objects,
+ # 2. Or executes expensive SQL query
+ def slow_composite_status
+ if Feature.enabled?(:ci_composite_status, default_enabled: false)
+ Gitlab::Ci::Status::Composite
+ .new(all, with_allow_failure: columns_hash.key?('allow_failure'))
+ .status
+ else
+ legacy_status
+ end
end
def started_at
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index d02f3731cc2..852576dbbc2 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -4,7 +4,7 @@
#
# Contains common functionality shared between Issues and MergeRequests
#
-# Used by Issue, MergeRequest
+# Used by Issue, MergeRequest, Epic
#
module Issuable
extend ActiveSupport::Concern
@@ -25,6 +25,19 @@ module Issuable
include UpdatedAtFilterable
include IssuableStates
include ClosedAtFilterable
+ include VersionedDescription
+
+ TITLE_LENGTH_MAX = 255
+ TITLE_HTML_LENGTH_MAX = 800
+ DESCRIPTION_LENGTH_MAX = 1.megabyte
+ DESCRIPTION_HTML_LENGTH_MAX = 5.megabytes
+
+ STATE_ID_MAP = {
+ opened: 1,
+ closed: 2,
+ merged: 3,
+ locked: 4
+ }.with_indifferent_access.freeze
# This object is used to gather issuable meta data for displaying
# upvotes, downvotes, notes and closing merge requests count for issues and merge requests
@@ -72,10 +85,15 @@ module Issuable
prefix: true
validates :author, presence: true
- validates :title, presence: true, length: { maximum: 255 }
- validates :description, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }, allow_blank: true
+ validates :title, presence: true, length: { maximum: TITLE_LENGTH_MAX }
+ # we validate the description against DESCRIPTION_LENGTH_MAX only for Issuables being created
+ # to avoid breaking the existing Issuables which may have their descriptions longer
+ validates :description, length: { maximum: DESCRIPTION_LENGTH_MAX }, allow_blank: true, on: :create
+ validate :description_max_length_for_new_records_is_valid, on: :update
validate :milestone_is_valid
+ before_validation :truncate_description_on_import!
+
scope :authored, ->(user) { where(author_id: user) }
scope :recent, -> { reorder(id: :desc) }
scope :of_projects, ->(ids) { where(project_id: ids) }
@@ -138,6 +156,16 @@ module Issuable
def milestone_is_valid
errors.add(:milestone_id, message: "is invalid") if milestone_id.present? && !milestone_available?
end
+
+ def description_max_length_for_new_records_is_valid
+ if new_record? && description.length > Issuable::DESCRIPTION_LENGTH_MAX
+ errors.add(:description, :too_long, count: Issuable::DESCRIPTION_LENGTH_MAX)
+ end
+ end
+
+ def truncate_description_on_import!
+ self.description = description&.slice(0, Issuable::DESCRIPTION_LENGTH_MAX) if importing?
+ end
end
class_methods do
@@ -152,13 +180,17 @@ module Issuable
fuzzy_search(query, [:title])
end
- # Available state values persisted in state_id column using state machine
+ def available_states
+ @available_states ||= STATE_ID_MAP.slice(*available_state_names)
+ end
+
+ # Available state names used to persist state_id column using state machine
#
# Override this on subclasses if different states are needed
#
- # Check MergeRequest.available_states for example
- def available_states
- @available_states ||= { opened: 1, closed: 2 }.with_indifferent_access
+ # Check MergeRequest.available_states_names for example
+ def available_state_names
+ [:opened, :closed]
end
# Searches for records with a matching title or description.
@@ -277,6 +309,14 @@ module Issuable
end
end
+ def state
+ self.class.available_states.key(state_id)
+ end
+
+ def state=(value)
+ self.state_id = self.class.available_states[value]
+ end
+
def resource_parent
project
end
diff --git a/app/models/concerns/issuable_states.rb b/app/models/concerns/issuable_states.rb
index 33bc41d7f44..f0b9f0d1f3a 100644
--- a/app/models/concerns/issuable_states.rb
+++ b/app/models/concerns/issuable_states.rb
@@ -4,22 +4,20 @@ module IssuableStates
extend ActiveSupport::Concern
# The state:string column is being migrated to state_id:integer column
- # This is a temporary hook to populate state_id column with new values
- # and should be removed after the state column is removed.
- # Check https://gitlab.com/gitlab-org/gitlab-foss/issues/51789 for more information
+ # This is a temporary hook to keep state column in sync until it is removed.
+ # Check https: https://gitlab.com/gitlab-org/gitlab/issues/33814 for more information
+ # The state column can be safely removed after 2019-10-27
included do
- before_save :set_state_id
+ before_save :sync_issuable_deprecated_state
end
- def set_state_id
- return if state.nil? || state.empty?
+ def sync_issuable_deprecated_state
+ return if self.is_a?(Epic)
+ return unless respond_to?(:state)
+ return if state_id.nil?
- # Needed to prevent breaking some migration specs that
- # rollback database to a point where state_id does not exist.
- # We can use this guard clause for now since this file will
- # be removed in the next release.
- return unless self.has_attribute?(:state_id)
+ deprecated_state = self.class.available_states.key(state_id)
- self.state_id = self.class.available_states[state]
+ self.write_attribute(:state, deprecated_state)
end
end
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 377600ef6e5..9b6c57261d8 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -150,7 +150,7 @@ module Mentionable
#
# Returns a Hash.
def detect_mentionable_changes
- source = (changes.present? ? changes : previous_changes).dup
+ source = (changes.presence || previous_changes).dup
mentionable = self.class.mentionable_attrs.map { |attr, options| attr }
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 3deb86da6cf..42b370990ac 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -6,7 +6,9 @@ module Milestoneish
end
def closed_issues_count(user)
- count_issues_by_state(user)['closed'].to_i
+ closed_state_id = Issue.available_states[:closed]
+
+ count_issues_by_state(user)[closed_state_id].to_i
end
def complete?(user)
@@ -117,7 +119,7 @@ module Milestoneish
def count_issues_by_state(user)
memoize_per_user(user, :count_issues_by_state) do
- issues_visible_to_user(user).reorder(nil).group(:state).count
+ issues_visible_to_user(user).reorder(nil).group(:state_id).count
end
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 6caa23ef9b7..3065e0ba6c5 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -7,6 +7,8 @@ module Noteable
# avoiding n+1 queries and improving performance.
NoteableMeta = Struct.new(:user_notes_count)
+ MAX_NOTES_LIMIT = 5_000
+
class_methods do
# `Noteable` class names that support replying to individual notes.
def replyable_types
diff --git a/app/models/concerns/notification_branch_selection.rb b/app/models/concerns/notification_branch_selection.rb
index d8e18de7551..7f00b652530 100644
--- a/app/models/concerns/notification_branch_selection.rb
+++ b/app/models/concerns/notification_branch_selection.rb
@@ -21,7 +21,7 @@ module NotificationBranchSelection
end
is_default_branch = ref == project.default_branch
- is_protected_branch = project.protected_branches.exists?(name: ref)
+ is_protected_branch = ProtectedBranch.protected?(project, ref)
case branches_to_be_notified
when "all"
diff --git a/app/models/concerns/prometheus_adapter.rb b/app/models/concerns/prometheus_adapter.rb
index aab0589f7ca..9df77b565da 100644
--- a/app/models/concerns/prometheus_adapter.rb
+++ b/app/models/concerns/prometheus_adapter.rb
@@ -44,7 +44,7 @@ module PrometheusAdapter
end
def query_klass_for(query_name)
- Gitlab::Prometheus::Queries.const_get("#{query_name.to_s.classify}Query")
+ Gitlab::Prometheus::Queries.const_get("#{query_name.to_s.classify}Query", false)
end
def build_query_args(*args)
diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb
index dfe3c391880..b645cf71443 100644
--- a/app/models/concerns/relative_positioning.rb
+++ b/app/models/concerns/relative_positioning.rb
@@ -127,6 +127,7 @@ module RelativePositioning
if pos_after && (pos_after - pos_before) < 2
before.move_sequence_after
+ pos_after = before.next_relative_position
end
self.relative_position = self.class.position_between(pos_before, pos_after)
@@ -138,6 +139,7 @@ module RelativePositioning
if pos_before && (pos_after - pos_before) < 2
after.move_sequence_before
+ pos_before = after.prev_relative_position
end
self.relative_position = self.class.position_between(pos_before, pos_after)
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index bdd87437e2a..129d0fbb2c0 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -51,14 +51,21 @@ module Routable
# Klass.where_full_path_in(%w{gitlab-org/gitlab-foss gitlab-org/gitlab})
#
# Returns an ActiveRecord::Relation.
- def where_full_path_in(paths)
+ def where_full_path_in(paths, use_includes: true)
return none if paths.empty?
wheres = paths.map do |path|
"(LOWER(routes.path) = LOWER(#{connection.quote(path)}))"
end
- includes(:route).where(wheres.join(' OR ')).references(:routes)
+ route =
+ if use_includes
+ includes(:route).references(:routes)
+ else
+ joins(:route)
+ end
+
+ route.where(wheres.join(' OR '))
end
end
diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb
index 3ff4b4046d3..10bbeecc2f7 100644
--- a/app/models/concerns/spammable.rb
+++ b/app/models/concerns/spammable.rb
@@ -80,4 +80,9 @@ module Spammable
def check_for_spam?
true
end
+
+ # Override in Spammable if differs
+ def allow_possible_spam?
+ Feature.enabled?(:allow_possible_spam, project)
+ end
end
diff --git a/app/models/concerns/stepable.rb b/app/models/concerns/stepable.rb
index d00a049a004..dea241c5dbe 100644
--- a/app/models/concerns/stepable.rb
+++ b/app/models/concerns/stepable.rb
@@ -11,15 +11,15 @@ module Stepable
initial_result = {}
steps.inject(initial_result) do |previous_result, callback|
- result = method(callback).call
+ result = method(callback).call(previous_result)
- if result[:status] == :error
- result[:failed_step] = callback
+ if result[:status] != :success
+ result[:last_step] = callback
break result
end
- previous_result.merge(result)
+ result
end
end
diff --git a/app/models/concerns/versioned_description.rb b/app/models/concerns/versioned_description.rb
new file mode 100644
index 00000000000..63a24aadc8a
--- /dev/null
+++ b/app/models/concerns/versioned_description.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module VersionedDescription
+ extend ActiveSupport::Concern
+
+ included do
+ attr_accessor :saved_description_version
+
+ has_many :description_versions
+
+ after_update :save_description_version
+ end
+
+ private
+
+ def save_description_version
+ self.saved_description_version = nil
+
+ return unless Feature.enabled?(:save_description_versions, issuing_parent)
+ return unless saved_change_to_description?
+
+ unless description_versions.exists?
+ description_versions.create!(
+ description: description_before_last_save,
+ created_at: created_at
+ )
+ end
+
+ self.saved_description_version = description_versions.create!(description: description)
+ end
+end
diff --git a/app/models/concerns/worker_attributes.rb b/app/models/concerns/worker_attributes.rb
new file mode 100644
index 00000000000..af40e9e3b19
--- /dev/null
+++ b/app/models/concerns/worker_attributes.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module WorkerAttributes
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def feature_category(value)
+ raise "Invalid category. Use `feature_category_not_owned!` to mark a worker as not owned" if value == :not_owned
+
+ worker_attributes[:feature_category] = value
+ end
+
+ # Special case: mark this work as not associated with a feature category
+ # this should be used for cross-cutting concerns, such as mailer workers.
+ def feature_category_not_owned!
+ worker_attributes[:feature_category] = :not_owned
+ end
+
+ def get_feature_category
+ get_worker_attribute(:feature_category)
+ end
+
+ def feature_category_not_owned?
+ get_worker_attribute(:feature_category) == :not_owned
+ end
+
+ protected
+
+ # Returns a worker attribute declared on this class or its parent class.
+ # This approach allows declared attributes to be inherited by
+ # child classes.
+ def get_worker_attribute(name)
+ worker_attributes[name] || superclass_worker_attributes(name)
+ end
+
+ private
+
+ def worker_attributes
+ @attributes ||= {}
+ end
+
+ def superclass_worker_attributes(name)
+ return unless superclass.include? WorkerAttributes
+
+ superclass.get_worker_attribute(name)
+ end
+ end
+end