diff options
Diffstat (limited to 'app/models/concerns')
20 files changed, 240 insertions, 57 deletions
diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb index d459af23a2f..de176ffde5c 100644 --- a/app/models/concerns/cacheable_attributes.rb +++ b/app/models/concerns/cacheable_attributes.rb @@ -55,7 +55,7 @@ module CacheableAttributes current_without_cache.tap { |current_record| current_record&.cache! } rescue => e if Rails.env.production? - Rails.logger.warn("Cached record for #{name} couldn't be loaded, falling back to uncached record: #{e}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.warn("Cached record for #{name} couldn't be loaded, falling back to uncached record: #{e}") else raise e end diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb index ccd90ea5900..7ea5382a4fa 100644 --- a/app/models/concerns/ci/contextable.rb +++ b/app/models/concerns/ci/contextable.rb @@ -18,7 +18,7 @@ module Ci variables.concat(deployment_variables(environment: environment)) variables.concat(yaml_variables) variables.concat(user_variables) - variables.concat(dependency_variables) if Feature.enabled?(:ci_dependency_variables, project) + variables.concat(dependency_variables) variables.concat(secret_instance_variables) variables.concat(secret_group_variables) variables.concat(secret_project_variables(environment: environment)) diff --git a/app/models/concerns/each_batch.rb b/app/models/concerns/each_batch.rb index 6314b46a7e3..af5f4e30d06 100644 --- a/app/models/concerns/each_batch.rb +++ b/app/models/concerns/each_batch.rb @@ -17,7 +17,7 @@ module EachBatch # Example: # # User.each_batch do |relation| - # relation.update_all(updated_at: Time.now) + # relation.update_all(updated_at: Time.current) # end # # The supplied block is also passed an optional batch index: diff --git a/app/models/concerns/featurable.rb b/app/models/concerns/featurable.rb new file mode 100644 index 00000000000..60aa46ce04c --- /dev/null +++ b/app/models/concerns/featurable.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +# == Featurable concern +# +# This concern adds features (tools) functionality to Project and Group +# To enable features you need to call `set_available_features` +# +# Example: +# +# class ProjectFeature +# include Featurable +# set_available_features %i(wiki merge_request) + +module Featurable + extend ActiveSupport::Concern + + # Can be enabled only for members, everyone or disabled + # Access control is made only for non private containers. + # + # Permission levels: + # + # Disabled: not enabled for anyone + # Private: enabled only for team members + # Enabled: enabled for everyone able to access the project + # Public: enabled for everyone (only allowed for pages) + DISABLED = 0 + PRIVATE = 10 + ENABLED = 20 + PUBLIC = 30 + + STRING_OPTIONS = HashWithIndifferentAccess.new({ + 'disabled' => DISABLED, + 'private' => PRIVATE, + 'enabled' => ENABLED, + 'public' => PUBLIC + }).freeze + + class_methods do + def set_available_features(available_features = []) + @available_features = available_features + + class_eval do + available_features.each do |feature| + define_method("#{feature}_enabled?") do + public_send("#{feature}_access_level") > DISABLED # rubocop:disable GitlabSecurity/PublicSend + end + end + end + end + + def available_features + @available_features + end + + def access_level_attribute(feature) + feature = ensure_feature!(feature) + + "#{feature}_access_level".to_sym + end + + def quoted_access_level_column(feature) + attribute = connection.quote_column_name(access_level_attribute(feature)) + table = connection.quote_table_name(table_name) + + "#{table}.#{attribute}" + end + + def access_level_from_str(level) + STRING_OPTIONS.fetch(level) + end + + def str_from_access_level(level) + STRING_OPTIONS.key(level) + end + + def ensure_feature!(feature) + feature = feature.model_name.plural if feature.respond_to?(:model_name) + feature = feature.to_sym + raise ArgumentError, "invalid feature: #{feature}" unless available_features.include?(feature) + + feature + end + end + + def access_level(feature) + public_send(self.class.access_level_attribute(feature)) # rubocop:disable GitlabSecurity/PublicSend + end + + def feature_available?(feature, user) + # This feature might not be behind a feature flag at all, so default to true + return false unless ::Feature.enabled?(feature, user, default_enabled: true) + + get_permission(user, feature) + end + + def string_access_level(feature) + self.class.str_from_access_level(access_level(feature)) + end +end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index b80f8c2bbb2..c885dea862f 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -66,7 +66,7 @@ module HasStatus # 1. By plucking all related objects, # 2. Or executes expensive SQL query def slow_composite_status(project:) - if Feature.enabled?(:ci_composite_status, project, default_enabled: false) + if ::Gitlab::Ci::Features.composite_status?(project) Gitlab::Ci::Status::Composite .new(all, with_allow_failure: columns_hash.key?('allow_failure')) .status @@ -160,7 +160,7 @@ module HasStatus if started_at && finished_at finished_at - started_at elsif started_at - Time.now - started_at + Time.current - started_at end end end diff --git a/app/models/concerns/import_state/sidekiq_job_tracker.rb b/app/models/concerns/import_state/sidekiq_job_tracker.rb index 55f171d158d..b7d0ed0f51b 100644 --- a/app/models/concerns/import_state/sidekiq_job_tracker.rb +++ b/app/models/concerns/import_state/sidekiq_job_tracker.rb @@ -5,14 +5,17 @@ module ImportState extend ActiveSupport::Concern included do + scope :with_jid, -> { where.not(jid: nil) } + scope :without_jid, -> { where(jid: nil) } + # Refreshes the expiration time of the associated import job ID. # # This method can be used by asynchronous importers to refresh the status, - # preventing the StuckImportJobsWorker from marking the import as failed. + # preventing the Gitlab::Import::StuckProjectImportJobsWorker from marking the import as failed. def refresh_jid_expiration return unless jid - Gitlab::SidekiqStatus.set(jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION) + Gitlab::SidekiqStatus.set(jid, Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION) end def self.jid_by(project_id:, status:) diff --git a/app/models/concerns/integration.rb b/app/models/concerns/integration.rb new file mode 100644 index 00000000000..644a0ba1b5e --- /dev/null +++ b/app/models/concerns/integration.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Integration + extend ActiveSupport::Concern + + class_methods do + def with_custom_integration_for(integration, page = nil, per = nil) + custom_integration_project_ids = Service + .where(type: integration.type) + .where(inherit_from_id: nil) + .distinct # Required until https://gitlab.com/gitlab-org/gitlab/-/issues/207385 + .page(page) + .per(per) + .pluck(:project_id) + + Project.where(id: custom_integration_project_ids) + end + end +end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index a1b14dca4ac..220af8ab7c7 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -39,15 +39,6 @@ module Issuable 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 - # lists avoiding n+1 queries and improving performance. - IssuableMeta = Struct.new(:upvotes, :downvotes, :user_notes_count, :mrs_count) do - def merge_requests_count(user = nil) - mrs_count - end - end - included do cache_markdown_field :title, pipeline: :single_line cache_markdown_field :description, issuable_state_filter_enabled: true @@ -139,7 +130,6 @@ module Issuable scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) } scope :with_label_ids, ->(label_ids) { joins(:label_links).where(label_links: { label_id: label_ids }) } - scope :any_label, -> { joins(:label_links).distinct } scope :join_project, -> { joins(:project) } scope :inc_notes_with_associations, -> { includes(notes: [:project, :author, :award_emoji]) } scope :references_project, -> { references(:project) } @@ -185,6 +175,10 @@ module Issuable assignees.count > 1 end + def supports_weight? + false + end + private def description_max_length_for_new_records_is_valid @@ -201,7 +195,7 @@ module Issuable class_methods do # Searches for records with a matching title. # - # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # This method uses ILIKE on PostgreSQL. # # query - The search query as a String # @@ -225,7 +219,7 @@ module Issuable # Searches for records with a matching title or description. # - # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # This method uses ILIKE on PostgreSQL. # # query - The search query as a String # matched_columns - Modify the scope of the query. 'title', 'description' or joining them with a comma. @@ -316,6 +310,14 @@ module Issuable end end + def any_label(sort = nil) + if sort + joins(:label_links).group(*grouping_columns(sort)) + else + joins(:label_links).distinct + end + end + # Includes table keys in group by clause when sorting # preventing errors in postgres # @@ -401,6 +403,10 @@ module Issuable participants(user).include?(user) end + def can_assign_epic?(user) + false + end + def to_hook_data(user, old_associations: {}) changes = previous_changes diff --git a/app/models/concerns/limitable.rb b/app/models/concerns/limitable.rb index f320f54bb82..3cb0bd85936 100644 --- a/app/models/concerns/limitable.rb +++ b/app/models/concerns/limitable.rb @@ -2,6 +2,7 @@ module Limitable extend ActiveSupport::Concern + GLOBAL_SCOPE = :limitable_global_scope included do class_attribute :limit_scope @@ -14,14 +15,34 @@ module Limitable private def validate_plan_limit_not_exceeded + if GLOBAL_SCOPE == limit_scope + validate_global_plan_limit_not_exceeded + else + validate_scoped_plan_limit_not_exceeded + end + end + + def validate_scoped_plan_limit_not_exceeded scope_relation = self.public_send(limit_scope) # rubocop:disable GitlabSecurity/PublicSend return unless scope_relation relation = self.class.where(limit_scope => scope_relation) + limits = scope_relation.actual_limits - if scope_relation.actual_limits.exceeded?(limit_name, relation) - errors.add(:base, _("Maximum number of %{name} (%{count}) exceeded") % - { name: limit_name.humanize(capitalize: false), count: scope_relation.actual_limits.public_send(limit_name) }) # rubocop:disable GitlabSecurity/PublicSend - end + check_plan_limit_not_exceeded(limits, relation) + end + + def validate_global_plan_limit_not_exceeded + relation = self.class.all + limits = Plan.default.actual_limits + + check_plan_limit_not_exceeded(limits, relation) + end + + def check_plan_limit_not_exceeded(limits, 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 end end diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index d157404f7bc..7b4485376d4 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -259,8 +259,8 @@ module Mentionable # for the test period. # During the test period the flag should be enabled at the group level. def store_mentioned_users_to_db_enabled? - return Feature.enabled?(:store_mentioned_users_to_db, self.project&.group) if self.respond_to?(:project) - return Feature.enabled?(:store_mentioned_users_to_db, self.group) if self.respond_to?(:group) + return Feature.enabled?(:store_mentioned_users_to_db, self.project&.group, default_enabled: true) if self.respond_to?(:project) + return Feature.enabled?(:store_mentioned_users_to_db, self.group, default_enabled: true) if self.respond_to?(:group) end end diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index fa5a79cc12b..5f24564dc56 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -97,26 +97,6 @@ module Milestoneish due_date && due_date.past? end - def group_milestone? - false - end - - def project_milestone? - false - end - - def legacy_group_milestone? - false - end - - def dashboard_milestone? - false - end - - def global_milestone? - 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/noteable.rb b/app/models/concerns/noteable.rb index 933a0b167e2..183b902dd37 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -24,7 +24,7 @@ module Noteable # The timestamp of the note (e.g. the :created_at or :updated_at attribute if provided via # API call) def system_note_timestamp - @system_note_timestamp || Time.now # rubocop:disable Gitlab/ModuleWithInstanceVariables + @system_note_timestamp || Time.current # rubocop:disable Gitlab/ModuleWithInstanceVariables end attr_writer :system_note_timestamp diff --git a/app/models/concerns/prometheus_adapter.rb b/app/models/concerns/prometheus_adapter.rb index 761a151a474..adb6a59e11c 100644 --- a/app/models/concerns/prometheus_adapter.rb +++ b/app/models/concerns/prometheus_adapter.rb @@ -44,7 +44,7 @@ module PrometheusAdapter { success: true, data: data, - last_update: Time.now.utc + last_update: Time.current.utc } rescue Gitlab::PrometheusClient::Error => err { success: false, result: err.message } diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb index 1653ecdb305..1d89a4497d9 100644 --- a/app/models/concerns/relative_positioning.rb +++ b/app/models/concerns/relative_positioning.rb @@ -50,7 +50,7 @@ module RelativePositioning # This method takes two integer values (positions) and # calculates the position between them. The range is huge as # the maximum integer value is 2147483647. We are incrementing position by IDEAL_DISTANCE * 2 every time - # when we have enough space. If distance is less then IDEAL_DISTANCE we are calculating an average number + # when we have enough space. If distance is less than IDEAL_DISTANCE, we are calculating an average number. def position_between(pos_before, pos_after) pos_before ||= MIN_POSITION pos_after ||= MAX_POSITION diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb index 5d78eea7fca..5174ae05d15 100644 --- a/app/models/concerns/resolvable_discussion.rb +++ b/app/models/concerns/resolvable_discussion.rb @@ -23,7 +23,10 @@ module ResolvableDiscussion :last_note ) - delegate :potentially_resolvable?, to: :first_note + delegate :potentially_resolvable?, + :noteable_id, + :noteable_type, + to: :first_note delegate :resolved_at, :resolved_by, @@ -79,7 +82,7 @@ module ResolvableDiscussion return false unless current_user return false unless resolvable? - current_user == self.noteable.author || + current_user == self.noteable.try(:author) || current_user.can?(:resolve_note, self.project) end diff --git a/app/models/concerns/resolvable_note.rb b/app/models/concerns/resolvable_note.rb index 2d2d5fb7168..4e8a1bb643e 100644 --- a/app/models/concerns/resolvable_note.rb +++ b/app/models/concerns/resolvable_note.rb @@ -23,7 +23,7 @@ module ResolvableNote class_methods do # This method must be kept in sync with `#resolve!` def resolve!(current_user) - unresolved.update_all(resolved_at: Time.now, resolved_by_id: current_user.id) + unresolved.update_all(resolved_at: Time.current, resolved_by_id: current_user.id) end # This method must be kept in sync with `#unresolve!` @@ -57,7 +57,7 @@ module ResolvableNote return false unless resolvable? return false if resolved? - self.resolved_at = Time.now + self.resolved_at = Time.current self.resolved_by = current_user self.resolved_by_push = resolved_by_push diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb index da4f2a79895..250889fdf8b 100644 --- a/app/models/concerns/storage/legacy_namespace.rb +++ b/app/models/concerns/storage/legacy_namespace.rb @@ -67,7 +67,7 @@ module Storage unless gitlab_shell.mv_namespace(repository_storage, full_path_before_last_save, full_path) - Rails.logger.error "Exception moving path #{repository_storage} from #{full_path_before_last_save} to #{full_path}" # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error("Exception moving path #{repository_storage} from #{full_path_before_last_save} to #{full_path}") # if we cannot move namespace directory we should rollback # db changes in order to prevent out of sync between db and fs diff --git a/app/models/concerns/timebox.rb b/app/models/concerns/timebox.rb index d29e6a01c56..8927e42dd97 100644 --- a/app/models/concerns/timebox.rb +++ b/app/models/concerns/timebox.rb @@ -7,7 +7,9 @@ module Timebox include CacheMarkdownField include Gitlab::SQL::Pattern include IidRoutes + include Referable include StripAttribute + include FromUnion TimeboxStruct = Struct.new(:title, :name, :id) do # Ensure these models match the interface required for exporting @@ -64,7 +66,11 @@ module Timebox groups = groups.compact if groups.is_a? Array groups = [] if groups.nil? - where(project_id: projects).or(where(group_id: groups)) + if Feature.enabled?(:optimized_timebox_queries, default_enabled: true) + from_union([where(project_id: projects), where(group_id: groups)], remove_duplicates: false) + else + where(project_id: projects).or(where(group_id: groups)) + end end scope :within_timeframe, -> (start_date, end_date) do @@ -122,6 +128,35 @@ module Timebox end end + ## + # Returns the String necessary to reference a Timebox in Markdown. Group + # timeboxes only support name references, and do not support cross-project + # references. + # + # format - Symbol format to use (default: :iid, optional: :name) + # + # Examples: + # + # Milestone.first.to_reference # => "%1" + # Iteration.first.to_reference(format: :name) # => "*iteration:\"goal\"" + # Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-foss%1" + # Iteration.first.to_reference(same_namespace_project) # => "gitlab-foss*iteration:1" + # + def to_reference(from = nil, format: :name, full: false) + format_reference = timebox_format_reference(format) + reference = "#{self.class.reference_prefix}#{format_reference}" + + if project + "#{project.to_reference_base(from, full: full)}#{reference}" + else + reference + end + end + + def reference_link_text(from = nil) + self.class.reference_prefix + self.title + end + def title=(value) write_attribute(:title, sanitize_title(value)) if value.present? end @@ -162,6 +197,20 @@ module Timebox private + def timebox_format_reference(format = :iid) + raise ArgumentError, _('Unknown format') unless [:iid, :name].include?(format) + + if group_timebox? && format == :iid + raise ArgumentError, _('Cannot refer to a group %{timebox_type} by an internal id!') % { timebox_type: timebox_name } + end + + if format == :name && !name.include?('"') + %("#{name}") + else + iid + end + end + # Timebox titles must be unique across project and group timeboxes def uniqueness_of_title if project diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 4099039dd96..a1f83884f02 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -4,6 +4,10 @@ module TokenAuthenticatable extend ActiveSupport::Concern class_methods do + def encrypted_token_authenticatable_fields + @encrypted_token_authenticatable_fields ||= [] + end + private def add_authentication_token_field(token_field, options = {}) @@ -12,6 +16,7 @@ module TokenAuthenticatable end token_authenticatable_fields.push(token_field) + encrypted_token_authenticatable_fields.push(token_field) if options[:encrypted] attr_accessor :cleartext_tokens diff --git a/app/models/concerns/update_highest_role.rb b/app/models/concerns/update_highest_role.rb index 7efc436c6c8..6432cc794a5 100644 --- a/app/models/concerns/update_highest_role.rb +++ b/app/models/concerns/update_highest_role.rb @@ -29,9 +29,7 @@ module UpdateHighestRole UpdateHighestRoleWorker.perform_in(HIGHEST_ROLE_JOB_DELAY, update_highest_role_attribute) else # use same logging as ExclusiveLeaseGuard - # rubocop:disable Gitlab/RailsLogger - Rails.logger.error('Cannot obtain an exclusive lease. There must be another instance already in execution.') - # rubocop:enable Gitlab/RailsLogger + Gitlab::AppLogger.error('Cannot obtain an exclusive lease. There must be another instance already in execution.') end end end |