diff options
Diffstat (limited to 'app/models/concerns')
-rw-r--r-- | app/models/concerns/analytics/cycle_analytics/stage.rb | 4 | ||||
-rw-r--r-- | app/models/concerns/any_field_validation.rb | 25 | ||||
-rw-r--r-- | app/models/concerns/approvable_base.rb | 13 | ||||
-rw-r--r-- | app/models/concerns/atomic_internal_id.rb | 23 | ||||
-rw-r--r-- | app/models/concerns/avatarable.rb | 7 | ||||
-rw-r--r-- | app/models/concerns/cache_markdown_field.rb | 6 | ||||
-rw-r--r-- | app/models/concerns/cascading_namespace_setting_attribute.rb | 13 | ||||
-rw-r--r-- | app/models/concerns/ci/maskable.rb | 2 | ||||
-rw-r--r-- | app/models/concerns/ci/metadatable.rb | 2 | ||||
-rw-r--r-- | app/models/concerns/enums/ci/commit_status.rb | 1 | ||||
-rw-r--r-- | app/models/concerns/has_integrations.rb | 2 | ||||
-rw-r--r-- | app/models/concerns/integrations/has_web_hook.rb | 36 | ||||
-rw-r--r-- | app/models/concerns/issuable.rb | 30 | ||||
-rw-r--r-- | app/models/concerns/milestoneish.rb | 4 | ||||
-rw-r--r-- | app/models/concerns/partitioned_table.rb | 6 | ||||
-rw-r--r-- | app/models/concerns/sortable.rb | 2 | ||||
-rw-r--r-- | app/models/concerns/taggable_queries.rb | 21 |
17 files changed, 149 insertions, 48 deletions
diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stage.rb index 90d48aa81d0..2a0274f5706 100644 --- a/app/models/concerns/analytics/cycle_analytics/stage.rb +++ b/app/models/concerns/analytics/cycle_analytics/stage.rb @@ -50,6 +50,10 @@ module Analytics end end + def events_hash_code + Digest::SHA256.hexdigest("#{start_event.hash_code}-#{end_event.hash_code}") + end + def start_event_label_based? start_event_identifier && start_event.label_based? end diff --git a/app/models/concerns/any_field_validation.rb b/app/models/concerns/any_field_validation.rb new file mode 100644 index 00000000000..987c4e7800e --- /dev/null +++ b/app/models/concerns/any_field_validation.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# This module enables a record to be valid if any field is present +# +# Overwrite one_of_required_fields to set one of which fields must be present +module AnyFieldValidation + extend ActiveSupport::Concern + + included do + validate :any_field_present + end + + private + + def any_field_present + return unless one_of_required_fields.all? { |field| self[field].blank? } + + errors.add(:base, _("At least one field of %{one_of_required_fields} must be present") % + { one_of_required_fields: one_of_required_fields }) + end + + def one_of_required_fields + raise NotImplementedError + end +end diff --git a/app/models/concerns/approvable_base.rb b/app/models/concerns/approvable_base.rb index c2d94b50f8d..ef7ba7b1089 100644 --- a/app/models/concerns/approvable_base.rb +++ b/app/models/concerns/approvable_base.rb @@ -24,6 +24,19 @@ module ApprovableBase .group(:id) .having("COUNT(users.id) = ?", usernames.size) end + + scope :not_approved_by_users_with_usernames, -> (usernames) do + users = User.where(username: usernames).select(:id) + self_table = self.arel_table + app_table = Approval.arel_table + + where( + Approval.where(approvals: { user_id: users }) + .where(app_table[:merge_request_id].eq(self_table[:id])) + .select('true') + .arel.exists.not + ) + end end class_methods do diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb index 80cf6260b0b..88f577c3e23 100644 --- a/app/models/concerns/atomic_internal_id.rb +++ b/app/models/concerns/atomic_internal_id.rb @@ -159,9 +159,8 @@ module AtomicInternalId # Defines class methods: # # - with_{scope}_{column}_supply - # This method can be used to allocate a block of IID values during - # bulk operations (importing/copying, etc). This can be more efficient - # than creating instances one-by-one. + # This method can be used to allocate a stream of IID values during + # bulk operations (importing/copying, etc). # # Pass in a block that receives a `Supply` instance. To allocate a new # IID value, call `Supply#next_value`. @@ -181,14 +180,8 @@ module AtomicInternalId scope_attrs = ::AtomicInternalId.scope_attrs(scope_value) usage = ::AtomicInternalId.scope_usage(self) - generator = InternalId::InternalIdGenerator.new(subject, scope_attrs, usage, init) - - generator.with_lock do - supply = Supply.new(generator.record.last_value) - block.call(supply) - ensure - generator.track_greatest(supply.current_value) if supply - end + supply = Supply.new(-> { InternalId.generate_next(subject, scope_attrs, usage, init) }) + block.call(supply) end end end @@ -236,14 +229,14 @@ module AtomicInternalId end class Supply - attr_reader :current_value + attr_reader :generator - def initialize(start_value) - @current_value = start_value + def initialize(generator) + @generator = generator end def next_value - @current_value += 1 + @generator.call end end end diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb index fdc418029be..84a74386ff7 100644 --- a/app/models/concerns/avatarable.rb +++ b/app/models/concerns/avatarable.rb @@ -9,13 +9,18 @@ module Avatarable ALLOWED_IMAGE_SCALER_WIDTHS = (USER_AVATAR_SIZES | PROJECT_AVATAR_SIZES | GROUP_AVATAR_SIZES).freeze + # This value must not be bigger than then: https://gitlab.com/gitlab-org/gitlab/-/blob/master/workhorse/config.toml.example#L20 + # + # https://docs.gitlab.com/ee/development/image_scaling.html + MAXIMUM_FILE_SIZE = 200.kilobytes.to_i + included do prepend ShadowMethods include ObjectStorage::BackgroundMove include Gitlab::Utils::StrongMemoize validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } - validates :avatar, file_size: { maximum: 200.kilobytes.to_i }, if: :avatar_changed? + validates :avatar, file_size: { maximum: MAXIMUM_FILE_SIZE }, if: :avatar_changed? mount_uploader :avatar, AvatarUploader diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index 101bff32dfe..79b622c8dad 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -163,9 +163,9 @@ module CacheMarkdownField refs = all_references(self.author) references = {} - references[:mentioned_users_ids] = refs.mentioned_users&.pluck(:id).presence - references[:mentioned_groups_ids] = refs.mentioned_groups&.pluck(:id).presence - references[:mentioned_projects_ids] = refs.mentioned_projects&.pluck(:id).presence + references[:mentioned_users_ids] = refs.mentioned_user_ids.presence + references[:mentioned_groups_ids] = refs.mentioned_group_ids.presence + references[:mentioned_projects_ids] = refs.mentioned_project_ids.presence # One retry is enough as next time `model_user_mention` should return the existing mention record, # that threw the `ActiveRecord::RecordNotUnique` exception in first place. diff --git a/app/models/concerns/cascading_namespace_setting_attribute.rb b/app/models/concerns/cascading_namespace_setting_attribute.rb index 9efd90756b1..5d24e15d518 100644 --- a/app/models/concerns/cascading_namespace_setting_attribute.rb +++ b/app/models/concerns/cascading_namespace_setting_attribute.rb @@ -24,10 +24,6 @@ module CascadingNamespaceSettingAttribute include Gitlab::Utils::StrongMemoize class_methods do - def cascading_settings_feature_enabled? - ::Feature.enabled?(:cascading_namespace_settings, default_enabled: true) - end - private # Facilitates the cascading lookup of values and, @@ -82,8 +78,6 @@ module CascadingNamespaceSettingAttribute def define_attr_reader(attribute) define_method(attribute) do strong_memoize(attribute) do - next self[attribute] unless self.class.cascading_settings_feature_enabled? - next self[attribute] if will_save_change_to_attribute?(attribute) next locked_value(attribute) if cascading_attribute_locked?(attribute, include_self: false) next self[attribute] unless self[attribute].nil? @@ -189,7 +183,6 @@ module CascadingNamespaceSettingAttribute end def locked_ancestor(attribute) - return unless self.class.cascading_settings_feature_enabled? return unless namespace.has_parent? strong_memoize(:"#{attribute}_locked_ancestor") do @@ -202,14 +195,10 @@ module CascadingNamespaceSettingAttribute end def locked_by_ancestor?(attribute) - return false unless self.class.cascading_settings_feature_enabled? - locked_ancestor(attribute).present? end def locked_by_application_setting?(attribute) - return false unless self.class.cascading_settings_feature_enabled? - Gitlab::CurrentSettings.public_send("lock_#{attribute}") # rubocop:disable GitlabSecurity/PublicSend end @@ -241,7 +230,7 @@ module CascadingNamespaceSettingAttribute def namespace_ancestor_ids strong_memoize(:namespace_ancestor_ids) do - namespace.self_and_ancestors(hierarchy_order: :asc).pluck(:id).reject { |id| id == namespace_id } + namespace.ancestor_ids(hierarchy_order: :asc) end end diff --git a/app/models/concerns/ci/maskable.rb b/app/models/concerns/ci/maskable.rb index e1ef4531845..62be0150ee0 100644 --- a/app/models/concerns/ci/maskable.rb +++ b/app/models/concerns/ci/maskable.rb @@ -11,7 +11,7 @@ module Ci # * Minimal length of 8 characters # * Characters must be from the Base64 alphabet (RFC4648) with the addition of '@', ':', '.', and '~' # * Absolutely no fun is allowed - REGEX = /\A[a-zA-Z0-9_+=\/@:.~-]{8,}\z/.freeze + REGEX = %r{\A[a-zA-Z0-9_+=/@:.~-]{8,}\z}.freeze included do validates :masked, inclusion: { in: [true, false] } diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb index 601637ea32a..114435d5a21 100644 --- a/app/models/concerns/ci/metadatable.rb +++ b/app/models/concerns/ci/metadatable.rb @@ -77,7 +77,7 @@ module Ci def write_metadata_attribute(legacy_key, metadata_key, value) # save to metadata or this model depending on the state of feature flag - if Feature.enabled?(:ci_build_metadata_config) + if Feature.enabled?(:ci_build_metadata_config, project, default_enabled: :yaml) ensure_metadata.write_attribute(metadata_key, value) write_attribute(legacy_key, nil) else diff --git a/app/models/concerns/enums/ci/commit_status.rb b/app/models/concerns/enums/ci/commit_status.rb index 72788d15c0a..16dec5fb081 100644 --- a/app/models/concerns/enums/ci/commit_status.rb +++ b/app/models/concerns/enums/ci/commit_status.rb @@ -25,6 +25,7 @@ module Enums ci_quota_exceeded: 16, pipeline_loop_detected: 17, no_matching_runner: 18, # not used anymore, but cannot be deleted because of old data + trace_size_exceeded: 19, insufficient_bridge_permissions: 1_001, downstream_bridge_project_not_found: 1_002, invalid_bridge_trigger: 1_003, diff --git a/app/models/concerns/has_integrations.rb b/app/models/concerns/has_integrations.rb index b2775f4cbb2..25650ae56ad 100644 --- a/app/models/concerns/has_integrations.rb +++ b/app/models/concerns/has_integrations.rb @@ -19,7 +19,7 @@ module HasIntegrations def without_integration(integration) integrations = Integration .select('1') - .where('services.project_id = projects.id') + .where("#{Integration.table_name}.project_id = projects.id") .where(type: integration.type) Project diff --git a/app/models/concerns/integrations/has_web_hook.rb b/app/models/concerns/integrations/has_web_hook.rb new file mode 100644 index 00000000000..dabe7152b18 --- /dev/null +++ b/app/models/concerns/integrations/has_web_hook.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Integrations + module HasWebHook + extend ActiveSupport::Concern + + included do + after_save :update_web_hook!, if: :activated? + end + + # Return the URL to be used for the webhook. + def hook_url + raise NotImplementedError + end + + # Return whether the webhook should use SSL verification. + def hook_ssl_verification + true + end + + # Create or update the webhook, raising an exception if it cannot be saved. + def update_web_hook! + hook = service_hook || build_service_hook + hook.url = hook_url if hook.url != hook_url # avoid reencryption + hook.enable_ssl_verification = hook_ssl_verification + hook.save! if hook.changed? + hook + end + + # Execute the webhook, creating it if necessary. + def execute_web_hook!(*args) + update_web_hook! + service_hook.execute(*args) + end + end +end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 2d06247a486..d5e2e63402f 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -31,6 +31,7 @@ module Issuable TITLE_HTML_LENGTH_MAX = 800 DESCRIPTION_LENGTH_MAX = 1.megabyte DESCRIPTION_HTML_LENGTH_MAX = 5.megabytes + SEARCHABLE_FIELDS = %w(title description).freeze STATE_ID_MAP = { opened: 1, @@ -264,15 +265,16 @@ module Issuable # matched_columns - Modify the scope of the query. 'title', 'description' or joining them with a comma. # # Returns an ActiveRecord::Relation. - def full_search(query, matched_columns: 'title,description', use_minimum_char_limit: true) - allowed_columns = [:title, :description] - matched_columns = matched_columns.to_s.split(',').map(&:to_sym) - matched_columns &= allowed_columns + def full_search(query, matched_columns: nil, use_minimum_char_limit: true) + if matched_columns + matched_columns = matched_columns.to_s.split(',') + matched_columns &= SEARCHABLE_FIELDS + matched_columns.map!(&:to_sym) + end - # Matching title or description if the matched_columns did not contain any allowed columns. - matched_columns = [:title, :description] if matched_columns.empty? + search_columns = matched_columns.presence || [:title, :description] - fuzzy_search(query, matched_columns, use_minimum_char_limit: use_minimum_char_limit) + fuzzy_search(query, search_columns, use_minimum_char_limit: use_minimum_char_limit) end def simple_sorts @@ -330,12 +332,15 @@ module Issuable # When using CTE make sure to select the same columns that are on the group_by clause. # This prevents errors when ignored columns are present in the database. issuable_columns = with_cte ? issue_grouping_columns(use_cte: with_cte) : "#{table_name}.*" + group_columns = issue_grouping_columns(use_cte: with_cte) + ["highest_priorities.label_priority"] - extra_select_columns.unshift("(#{highest_priority}) AS highest_priority") + extra_select_columns.unshift("highest_priorities.label_priority as highest_priority") select(issuable_columns) .select(extra_select_columns) - .group(issue_grouping_columns(use_cte: with_cte)) + .from("#{table_name}") + .joins("JOIN LATERAL(#{highest_priority}) as highest_priorities ON TRUE") + .group(group_columns) .reorder(Gitlab::Database.nulls_last_order('highest_priority', direction)) end @@ -382,7 +387,7 @@ module Issuable if use_cte attribute_names.map { |attr| arel_table[attr.to_sym] } else - arel_table[:id] + [arel_table[:id]] end end @@ -457,6 +462,7 @@ module Issuable if old_associations old_labels = old_associations.fetch(:labels, labels) old_assignees = old_associations.fetch(:assignees, assignees) + old_severity = old_associations.fetch(:severity, severity) if old_labels != labels changes[:labels] = [old_labels.map(&:hook_attrs), labels.map(&:hook_attrs)] @@ -466,6 +472,10 @@ module Issuable changes[:assignees] = [old_assignees.map(&:hook_attrs), assignees.map(&:hook_attrs)] end + if supports_severity? && old_severity != severity + changes[:severity] = [old_severity, severity] + end + if self.respond_to?(:total_time_spent) old_total_time_spent = old_associations.fetch(:total_time_spent, total_time_spent) old_time_change = old_associations.fetch(:time_change, time_change) diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index eaf64f2541d..4f2ea58f36d 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -101,6 +101,10 @@ module Milestoneish due_date && due_date.past? end + def expired + expired? || false + end + def total_time_spent @total_time_spent ||= issues.joins(:timelogs).sum(:time_spent) + merge_requests.joins(:timelogs).sum(:time_spent) end diff --git a/app/models/concerns/partitioned_table.rb b/app/models/concerns/partitioned_table.rb index 9f1cec5d520..eab5d4c35bb 100644 --- a/app/models/concerns/partitioned_table.rb +++ b/app/models/concerns/partitioned_table.rb @@ -10,12 +10,12 @@ module PartitionedTable monthly: Gitlab::Database::Partitioning::MonthlyStrategy }.freeze - def partitioned_by(partitioning_key, strategy:) + def partitioned_by(partitioning_key, strategy:, **kwargs) strategy_class = PARTITIONING_STRATEGIES[strategy.to_sym] || raise(ArgumentError, "Unknown partitioning strategy: #{strategy}") - @partitioning_strategy = strategy_class.new(self, partitioning_key) + @partitioning_strategy = strategy_class.new(self, partitioning_key, **kwargs) - Gitlab::Database::Partitioning::PartitionCreator.register(self) + Gitlab::Database::Partitioning::PartitionManager.register(self) end end end diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index 9f5e9b2bb57..65fb62a814f 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -46,7 +46,7 @@ module Sortable private def highest_label_priority(target_type_column: nil, target_type: nil, target_column:, project_column:, excluded_labels: []) - query = Label.select(LabelPriority.arel_table[:priority].minimum) + query = Label.select(LabelPriority.arel_table[:priority].minimum.as('label_priority')) .left_join_priorities .joins(:label_links) .where("label_priorities.project_id = #{project_column}") diff --git a/app/models/concerns/taggable_queries.rb b/app/models/concerns/taggable_queries.rb index 2897e5e6420..cba2e93a86d 100644 --- a/app/models/concerns/taggable_queries.rb +++ b/app/models/concerns/taggable_queries.rb @@ -12,5 +12,26 @@ module TaggableQueries .where(taggings: { context: context, taggable_type: polymorphic_name }) .select('COALESCE(array_agg(tags.name ORDER BY name), ARRAY[]::text[])') end + + def matches_tag_ids(tag_ids, table: quoted_table_name, column: 'id') + matcher = ::ActsAsTaggableOn::Tagging + .where(taggable_type: CommitStatus.name) + .where(context: 'tags') + .where("taggable_id = #{connection.quote_table_name(table)}.#{connection.quote_column_name(column)}") # rubocop:disable GitlabSecurity/SqlInjection + .where.not(tag_id: tag_ids) + .select('1') + + where("NOT EXISTS (?)", matcher) + end + + def with_any_tags(table: quoted_table_name, column: 'id') + matcher = ::ActsAsTaggableOn::Tagging + .where(taggable_type: CommitStatus.name) + .where(context: 'tags') + .where("taggable_id = #{connection.quote_table_name(table)}.#{connection.quote_column_name(column)}") # rubocop:disable GitlabSecurity/SqlInjection + .select('1') + + where("EXISTS (?)", matcher) + end end end |