diff options
Diffstat (limited to 'app/models/concerns')
-rw-r--r-- | app/models/concerns/analytics/cycle_analytics/stage_event_model.rb | 7 | ||||
-rw-r--r-- | app/models/concerns/as_cte.rb | 12 | ||||
-rw-r--r-- | app/models/concerns/async_devise_email.rb | 5 | ||||
-rw-r--r-- | app/models/concerns/awardable.rb | 16 | ||||
-rw-r--r-- | app/models/concerns/cache_markdown_field.rb | 7 | ||||
-rw-r--r-- | app/models/concerns/ci/artifactable.rb | 2 | ||||
-rw-r--r-- | app/models/concerns/enums/ci/pipeline.rb | 2 | ||||
-rw-r--r-- | app/models/concerns/file_store_mounter.rb | 14 | ||||
-rw-r--r-- | app/models/concerns/integrations/base_data_fields.rb | 29 | ||||
-rw-r--r-- | app/models/concerns/integrations/has_data_fields.rb | 3 | ||||
-rw-r--r-- | app/models/concerns/issuable.rb | 20 | ||||
-rw-r--r-- | app/models/concerns/limitable.rb | 26 | ||||
-rw-r--r-- | app/models/concerns/pg_full_text_searchable.rb | 11 | ||||
-rw-r--r-- | app/models/concerns/project_features_compatibility.rb | 4 | ||||
-rw-r--r-- | app/models/concerns/sensitive_serializable_hash.rb | 2 | ||||
-rw-r--r-- | app/models/concerns/storage/legacy_namespace.rb | 14 |
16 files changed, 124 insertions, 50 deletions
diff --git a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb index 7cc4bc569d3..1bdb89349aa 100644 --- a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb +++ b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb @@ -33,9 +33,14 @@ module Analytics ) duration_in_seconds = Arel::Nodes::Extract.new(duration, :epoch) + # start_event_timestamp and end_event_timestamp do not really influence the order, + # but are included so that they are part of the returned result, for example when + # using Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher keyset_order( :total_time => { order_expression: arel_order(duration_in_seconds, direction), distinct: false, sql_type: 'double precision' }, - issuable_id_column => { order_expression: arel_order(arel_table[issuable_id_column], direction), distinct: true } + issuable_id_column => { order_expression: arel_order(arel_table[issuable_id_column], direction), distinct: true }, + :end_event_timestamp => { order_expression: arel_order(arel_table[:end_event_timestamp], direction), distinct: true }, + :start_event_timestamp => { order_expression: arel_order(arel_table[:start_event_timestamp], direction), distinct: true } ) end end diff --git a/app/models/concerns/as_cte.rb b/app/models/concerns/as_cte.rb new file mode 100644 index 00000000000..aa38ae3a9c1 --- /dev/null +++ b/app/models/concerns/as_cte.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# Convert any ActiveRecord::Relation to a Gitlab::SQL::CTE +module AsCte + extend ActiveSupport::Concern + + class_methods do + def as_cte(name, **opts) + Gitlab::SQL::CTE.new(name, all, **opts) + end + end +end diff --git a/app/models/concerns/async_devise_email.rb b/app/models/concerns/async_devise_email.rb index 38c99dc7e71..7cdbed2eef6 100644 --- a/app/models/concerns/async_devise_email.rb +++ b/app/models/concerns/async_devise_email.rb @@ -2,6 +2,7 @@ module AsyncDeviseEmail extend ActiveSupport::Concern + include AfterCommitQueue private @@ -9,6 +10,8 @@ module AsyncDeviseEmail def send_devise_notification(notification, *args) return true unless can?(:receive_notifications) - devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend + run_after_commit_or_now do + devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend + end end end diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index 896f0916d8c..1d0ce594f63 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -18,7 +18,7 @@ module Awardable inner_query = award_emoji_table .project('true') .where(award_emoji_table[:user_id].eq(user.id)) - .where(award_emoji_table[:awardable_type].eq(self.name)) + .where(award_emoji_table[:awardable_type].eq(base_class.name)) .where(award_emoji_table[:awardable_id].eq(self.arel_table[:id])) inner_query = inner_query.where(award_emoji_table[:name].eq(name)) if name.present? @@ -31,7 +31,7 @@ module Awardable inner_query = award_emoji_table .project('true') .where(award_emoji_table[:user_id].eq(user.id)) - .where(award_emoji_table[:awardable_type].eq(self.name)) + .where(award_emoji_table[:awardable_type].eq(base_class.name)) .where(award_emoji_table[:awardable_id].eq(self.arel_table[:id])) inner_query = inner_query.where(award_emoji_table[:name].eq(name)) if name.present? @@ -56,13 +56,11 @@ module Awardable awardable_table = self.arel_table awards_table = AwardEmoji.arel_table - join_clause = awardable_table.join(awards_table, Arel::Nodes::OuterJoin).on( - awards_table[:awardable_id].eq(awardable_table[:id]).and( - awards_table[:awardable_type].eq(self.name).and( - awards_table[:name].eq(emoji_name) - ) - ) - ).join_sources + join_clause = awardable_table + .join(awards_table, Arel::Nodes::OuterJoin) + .on(awards_table[:awardable_id].eq(awardable_table[:id]) + .and(awards_table[:awardable_type].eq(base_class.name).and(awards_table[:name].eq(emoji_name)))) + .join_sources joins(join_clause).group(awardable_table[:id]).reorder( Arel.sql("COUNT(award_emoji.id) #{direction}") diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index 9414d16beef..99dbe464a7c 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -24,6 +24,9 @@ module CacheMarkdownField true end + attr_accessor :skip_markdown_cache_validation + alias_method :skip_markdown_cache_validation?, :skip_markdown_cache_validation + # Returns the default Banzai render context for the cached markdown field. def banzai_render_context(field) raise ArgumentError, "Unknown field: #{field.inspect}" unless @@ -91,7 +94,7 @@ module CacheMarkdownField end def invalidated_markdown_cache? - cached_markdown_fields.html_fields.any? {|html_field| attribute_invalidated?(html_field) } + cached_markdown_fields.html_fields.any? { |html_field| attribute_invalidated?(html_field) } end def attribute_invalidated?(attr) @@ -218,6 +221,8 @@ module CacheMarkdownField # The HTML becomes invalid if any dependent fields change. For now, assume # author and project invalidate the cache in all circumstances. define_method(invalidation_method) do + return false if skip_markdown_cache_validation? + changed_fields = changed_attributes.keys invalidations = changed_fields & [markdown_field.to_s, *INVALIDATED_BY] !invalidations.empty? || !cached_html_up_to_date?(markdown_field) diff --git a/app/models/concerns/ci/artifactable.rb b/app/models/concerns/ci/artifactable.rb index 27040a677ff..78340cf967b 100644 --- a/app/models/concerns/ci/artifactable.rb +++ b/app/models/concerns/ci/artifactable.rb @@ -21,7 +21,7 @@ module Ci }, _suffix: true scope :expired_before, -> (timestamp) { where(arel_table[:expire_at].lt(timestamp)) } - scope :expired, -> (limit) { expired_before(Time.current).limit(limit) } + scope :expired, -> { expired_before(Time.current) } scope :project_id_in, ->(ids) { where(project_id: ids) } end diff --git a/app/models/concerns/enums/ci/pipeline.rb b/app/models/concerns/enums/ci/pipeline.rb index 94d11c871ca..8ed6c54441b 100644 --- a/app/models/concerns/enums/ci/pipeline.rb +++ b/app/models/concerns/enums/ci/pipeline.rb @@ -15,7 +15,7 @@ module Enums size_limit_exceeded: 21, job_activity_limit_exceeded: 22, deployments_limit_exceeded: 23, - user_blocked: 24, + # 24 was previously used by the deprecated `user_blocked` project_deleted: 25 } end diff --git a/app/models/concerns/file_store_mounter.rb b/app/models/concerns/file_store_mounter.rb index bfcf8a1e7b9..f1ac734635d 100644 --- a/app/models/concerns/file_store_mounter.rb +++ b/app/models/concerns/file_store_mounter.rb @@ -4,9 +4,16 @@ module FileStoreMounter extend ActiveSupport::Concern class_methods do - def mount_file_store_uploader(uploader) + # When `skip_store_file: true` is used, the model MUST explicitly call `store_file_now!` + def mount_file_store_uploader(uploader, skip_store_file: false) mount_uploader(:file, uploader) + if skip_store_file + skip_callback :save, :after, :store_file! + + return + end + # This hook is a no-op when the file is uploaded after_commit after_save :update_file_store, if: :saved_change_to_file? end @@ -16,4 +23,9 @@ module FileStoreMounter # The file.object_store is set during `uploader.store!` and `uploader.migrate!` update_column(:file_store, file.object_store) end + + def store_file_now! + store_file! + update_file_store + end end diff --git a/app/models/concerns/integrations/base_data_fields.rb b/app/models/concerns/integrations/base_data_fields.rb index 3cedb90756f..11bdd3aae7b 100644 --- a/app/models/concerns/integrations/base_data_fields.rb +++ b/app/models/concerns/integrations/base_data_fields.rb @@ -4,12 +4,15 @@ module Integrations module BaseDataFields extend ActiveSupport::Concern + LEGACY_FOREIGN_KEY_NAME = %w( + Integrations::IssueTrackerData + Integrations::JiraTrackerData + ).freeze + included do # TODO: Once we rename the tables we can't rely on `table_name` anymore. # https://gitlab.com/gitlab-org/gitlab/-/issues/331953 - belongs_to :integration, inverse_of: self.table_name.to_sym, foreign_key: :service_id - - delegate :activated?, to: :integration, allow_nil: true + belongs_to :integration, inverse_of: self.table_name.to_sym, foreign_key: foreign_key_name validates :integration, presence: true end @@ -23,6 +26,26 @@ module Integrations algorithm: 'aes-256-gcm' } end + + private + + # Older data field models use the `service_id` foreign key for the + # integration association. + def foreign_key_name + return :service_id if self.name.in?(LEGACY_FOREIGN_KEY_NAME) + + :integration_id + end + end + + def activated? + !!integration&.activated? + end + + def to_database_hash + as_json( + only: self.class.column_names + ).except('id', 'service_id', 'integration_id', 'created_at', 'updated_at') end end end diff --git a/app/models/concerns/integrations/has_data_fields.rb b/app/models/concerns/integrations/has_data_fields.rb index 25a1d855119..635147a2f3c 100644 --- a/app/models/concerns/integrations/has_data_fields.rb +++ b/app/models/concerns/integrations/has_data_fields.rb @@ -12,7 +12,8 @@ module Integrations self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 unless method_defined?(arg) def #{arg} - data_fields.send('#{arg}') || (properties && properties['#{arg}']) + value = data_fields.send('#{arg}') + value.nil? ? properties&.dig('#{arg}') : value end end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 713a4386fee..4dca07132ef 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -106,23 +106,23 @@ module Issuable scope :closed, -> { with_state(:closed) } # rubocop:disable GitlabSecurity/SqlInjection - # The `to_ability_name` method is not an user input. + # The `assignee_association_name` method is not an user input. scope :assigned, -> do - where("EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE #{to_ability_name}_id = #{to_ability_name}s.id)") + where("EXISTS (SELECT TRUE FROM #{assignee_association_name}_assignees WHERE #{assignee_association_name}_id = #{assignee_association_name}s.id)") end scope :unassigned, -> do - where("NOT EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE #{to_ability_name}_id = #{to_ability_name}s.id)") + where("NOT EXISTS (SELECT TRUE FROM #{assignee_association_name}_assignees WHERE #{assignee_association_name}_id = #{assignee_association_name}s.id)") end scope :assigned_to, ->(users) do - assignees_class = self.reflect_on_association("#{to_ability_name}_assignees").klass + assignees_class = self.reflect_on_association("#{assignee_association_name}_assignees").klass - condition = assignees_class.where(user_id: users).where(Arel.sql("#{to_ability_name}_id = #{to_ability_name}s.id")) + condition = assignees_class.where(user_id: users).where(Arel.sql("#{assignee_association_name}_id = #{assignee_association_name}s.id")) where(condition.arel.exists) end scope :not_assigned_to, ->(users) do - assignees_class = self.reflect_on_association("#{to_ability_name}_assignees").klass + assignees_class = self.reflect_on_association("#{assignee_association_name}_assignees").klass - condition = assignees_class.where(user_id: users).where(Arel.sql("#{to_ability_name}_id = #{to_ability_name}s.id")) + condition = assignees_class.where(user_id: users).where(Arel.sql("#{assignee_association_name}_id = #{assignee_association_name}s.id")) where(condition.arel.exists.not) end # rubocop:enable GitlabSecurity/SqlInjection @@ -195,8 +195,6 @@ module Issuable end def supports_escalation? - return false unless ::Feature.enabled?(:incident_escalations, project) - incident? end @@ -414,6 +412,10 @@ module Issuable def parent_class ::Project end + + def assignee_association_name + to_ability_name + end end def state diff --git a/app/models/concerns/limitable.rb b/app/models/concerns/limitable.rb index 6ff540b7866..0cccb7b51a8 100644 --- a/app/models/concerns/limitable.rb +++ b/app/models/concerns/limitable.rb @@ -15,17 +15,29 @@ module Limitable validate :validate_plan_limit_not_exceeded, on: :create end + def exceeds_limits? + limits, relation = fetch_plan_limit_data + + limits&.exceeded?(limit_name, relation) + end + private def validate_plan_limit_not_exceeded + limits, relation = fetch_plan_limit_data + + check_plan_limit_not_exceeded(limits, relation) + end + + def fetch_plan_limit_data if GLOBAL_SCOPE == limit_scope - validate_global_plan_limit_not_exceeded + global_plan_limits else - validate_scoped_plan_limit_not_exceeded + scoped_plan_limits end end - def validate_scoped_plan_limit_not_exceeded + def scoped_plan_limits scope_relation = self.public_send(limit_scope) # rubocop:disable GitlabSecurity/PublicSend return unless scope_relation return if limit_feature_flag && ::Feature.disabled?(limit_feature_flag, scope_relation) @@ -34,18 +46,18 @@ module Limitable relation = limit_relation ? self.public_send(limit_relation) : self.class.where(limit_scope => scope_relation) # rubocop:disable GitlabSecurity/PublicSend limits = scope_relation.actual_limits - check_plan_limit_not_exceeded(limits, relation) + [limits, relation] end - def validate_global_plan_limit_not_exceeded + def global_plan_limits relation = self.class.all limits = Plan.default.actual_limits - check_plan_limit_not_exceeded(limits, relation) + [limits, relation] end def check_plan_limit_not_exceeded(limits, relation) - return unless limits.exceeded?(limit_name, relation) + return unless limits&.exceeded?(limit_name, relation) errors.add(:base, _("Maximum number of %{name} (%{count}) exceeded") % { name: limit_name.humanize(capitalize: false), count: limits.public_send(limit_name) }) # rubocop:disable GitlabSecurity/PublicSend diff --git a/app/models/concerns/pg_full_text_searchable.rb b/app/models/concerns/pg_full_text_searchable.rb index bfc539ee392..813827478da 100644 --- a/app/models/concerns/pg_full_text_searchable.rb +++ b/app/models/concerns/pg_full_text_searchable.rb @@ -24,6 +24,7 @@ module PgFullTextSearchable LONG_WORDS_REGEX = %r([A-Za-z0-9+/@]{50,}).freeze TSVECTOR_MAX_LENGTH = 1.megabyte.freeze TEXT_SEARCH_DICTIONARY = 'english' + URL_SCHEME_REGEX = %r{(?<=\A|\W)\w+://(?=\w+)}.freeze def update_search_data! tsvector_sql_nodes = self.class.pg_full_text_searchable_columns.map do |column, weight| @@ -104,6 +105,10 @@ module PgFullTextSearchable def pg_full_text_search(search_term) search_data_table = reflect_on_association(:search_data).klass.arel_table + # This fixes an inconsistency with how to_tsvector and websearch_to_tsquery process URLs + # See https://gitlab.com/gitlab-org/gitlab/-/issues/354784#note_905431920 + search_term = remove_url_scheme(search_term) + joins(:search_data).where( Arel::Nodes::InfixOperation.new( '@@', @@ -115,5 +120,11 @@ module PgFullTextSearchable ) ) end + + private + + def remove_url_scheme(search_term) + search_term.gsub(URL_SCHEME_REGEX, '') + end end end diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index 0cab874a240..900e8f7d39b 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -66,6 +66,10 @@ module ProjectFeaturesCompatibility write_feature_attribute_string(:snippets_access_level, value) end + def package_registry_access_level=(value) + write_feature_attribute_string(:package_registry_access_level, value) + end + def pages_access_level=(value) write_feature_attribute_string(:pages_access_level, value) end diff --git a/app/models/concerns/sensitive_serializable_hash.rb b/app/models/concerns/sensitive_serializable_hash.rb index 94451fcd2c2..4ad8d16fcb9 100644 --- a/app/models/concerns/sensitive_serializable_hash.rb +++ b/app/models/concerns/sensitive_serializable_hash.rb @@ -10,7 +10,7 @@ module SensitiveSerializableHash class_methods do def prevent_from_serialization(*keys) self.attributes_exempt_from_serializable_hash ||= [] - self.attributes_exempt_from_serializable_hash.concat keys + self.attributes_exempt_from_serializable_hash += keys end end diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb index 948190dfadf..e418842a30b 100644 --- a/app/models/concerns/storage/legacy_namespace.rb +++ b/app/models/concerns/storage/legacy_namespace.rb @@ -23,22 +23,8 @@ module Storage former_parent_full_path = parent_was&.full_path parent_full_path = parent&.full_path Gitlab::UploadsTransfer.new.move_namespace(path, former_parent_full_path, parent_full_path) - - if any_project_with_pages_deployed? - run_after_commit do - Gitlab::PagesTransfer.new.async.move_namespace(path, former_parent_full_path, parent_full_path) - end - end else Gitlab::UploadsTransfer.new.rename_namespace(full_path_before_last_save, full_path) - - if any_project_with_pages_deployed? - full_path_was = full_path_before_last_save - - run_after_commit do - Gitlab::PagesTransfer.new.async.rename_namespace(full_path_was, full_path) - end - end end # If repositories moved successfully we need to |