summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/application_setting.rb3
-rw-r--r--app/models/ci/pipeline.rb6
-rw-r--r--app/models/commit.rb1
-rw-r--r--app/models/concerns/issuable.rb6
-rw-r--r--app/models/concerns/noteable.rb19
-rw-r--r--app/models/concerns/project_services_loggable.rb26
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb4
-rw-r--r--app/models/event.rb8
-rw-r--r--app/models/label.rb1
-rw-r--r--app/models/label_link.rb2
-rw-r--r--app/models/label_note.rb97
-rw-r--r--app/models/license_template.rb2
-rw-r--r--app/models/namespace.rb12
-rw-r--r--app/models/note.rb13
-rw-r--r--app/models/project.rb34
-rw-r--r--app/models/project_services/asana_service.rb2
-rw-r--r--app/models/project_services/irker_service.rb2
-rw-r--r--app/models/project_services/issue_tracker_service.rb2
-rw-r--r--app/models/project_services/jira_service.rb9
-rw-r--r--app/models/prometheus_metric.rb89
-rw-r--r--app/models/repository.rb1
-rw-r--r--app/models/resource_label_event.rb101
-rw-r--r--app/models/service.rb1
-rw-r--r--app/models/system_note_metadata.rb2
-rw-r--r--app/models/user.rb22
-rw-r--r--app/models/user_callout.rb3
26 files changed, 398 insertions, 70 deletions
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 03bd7fa016e..d8536c5512d 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -302,7 +302,8 @@ class ApplicationSetting < ActiveRecord::Base
instance_statistics_visibility_private: false,
user_default_external: false,
user_default_internal_regex: nil,
- user_show_add_ssh_key_message: true
+ user_show_add_ssh_key_message: true,
+ usage_stats_set_by_user_id: nil
}
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 526bf7af99b..2955e0b2bca 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -161,6 +161,12 @@ module Ci
PipelineNotificationWorker.perform_async(pipeline.id)
end
end
+
+ after_transition any => [:failed] do |pipeline|
+ next unless pipeline.auto_devops_source?
+
+ pipeline.run_after_commit { AutoDevops::DisableWorker.perform_async(pipeline.id) }
+ end
end
scope :internal, -> { where(source: internal_sources) }
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 594972ad344..49c36ad9d3f 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -22,6 +22,7 @@ class Commit
attr_accessor :project, :author
attr_accessor :redacted_description_html
attr_accessor :redacted_title_html
+ attr_accessor :redacted_full_title_html
attr_reader :gpg_commit
DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines]
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index f881ce2321c..5f65fceb7af 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -49,7 +49,7 @@ module Issuable
end
end
- has_many :label_links, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :label_links, as: :target, dependent: :destroy, inverse_of: :target # rubocop:disable Cop/ActiveRecordDependent
has_many :labels, through: :label_links
has_many :todos, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -109,10 +109,6 @@ module Issuable
false
end
- def etag_caching_enabled?
- false
- end
-
def has_multiple_assignees?
assignees.count > 1
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index ce778eae271..098eed137ba 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -82,4 +82,23 @@ module Noteable
def lockable?
[MergeRequest, Issue].include?(self.class)
end
+
+ def etag_caching_enabled?
+ false
+ end
+
+ def expire_note_etag_cache
+ return unless discussions_rendered_on_frontend?
+ return unless etag_caching_enabled?
+
+ Gitlab::EtagCaching::Store.new.touch(note_etag_key)
+ end
+
+ def note_etag_key
+ Gitlab::Routing.url_helpers.project_noteable_notes_path(
+ project,
+ target_type: self.class.name.underscore,
+ target_id: id
+ )
+ end
end
diff --git a/app/models/concerns/project_services_loggable.rb b/app/models/concerns/project_services_loggable.rb
new file mode 100644
index 00000000000..248a21f3578
--- /dev/null
+++ b/app/models/concerns/project_services_loggable.rb
@@ -0,0 +1,26 @@
+module ProjectServicesLoggable
+ def log_info(message, params = {})
+ message = build_message(message, params)
+
+ logger.info(message)
+ end
+
+ def log_error(message, params = {})
+ message = build_message(message, params)
+
+ logger.error(message)
+ end
+
+ def build_message(message, params = {})
+ {
+ service_class: self.class.name,
+ project_id: project.id,
+ project_path: project.full_path,
+ message: message
+ }.merge(params)
+ end
+
+ def logger
+ Gitlab::ProjectServiceLogger
+ end
+end
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
index 3b745657a9e..9785011720a 100644
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ b/app/models/concerns/storage/legacy_namespace.rb
@@ -25,8 +25,6 @@ module Storage
Gitlab::PagesTransfer.new.rename_namespace(full_path_was, full_path)
end
- remove_exports!
-
# If repositories moved successfully we need to
# send update instructions to users.
# However we cannot allow rollback since we moved namespace dir
@@ -101,8 +99,6 @@ module Storage
end
end
end
-
- remove_exports!
end
def remove_legacy_exports!
diff --git a/app/models/event.rb b/app/models/event.rb
index ba28866e8e6..041dac6941b 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -151,15 +151,17 @@ class Event < ActiveRecord::Base
if push? || commit_note?
Ability.allowed?(user, :download_code, project)
elsif membership_changed?
- true
+ Ability.allowed?(user, :read_project, project)
elsif created_project?
- true
+ Ability.allowed?(user, :read_project, project)
elsif issue? || issue_note?
Ability.allowed?(user, :read_issue, note? ? note_target : target)
elsif merge_request? || merge_request_note?
Ability.allowed?(user, :read_merge_request, note? ? note_target : target)
+ elsif milestone?
+ Ability.allowed?(user, :read_project, project)
else
- milestone?
+ false # No other event types are visible
end
end
diff --git a/app/models/label.rb b/app/models/label.rb
index 96c1515b41a..8db7c3abd10 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -5,6 +5,7 @@ class Label < ActiveRecord::Base
include Referable
include Subscribable
include Gitlab::SQL::Pattern
+ include OptionallySearch
# Represents a "No Label" state used for filtering Issues and Merge
# Requests that have no label assigned.
diff --git a/app/models/label_link.rb b/app/models/label_link.rb
index 779657b25d5..1d93a55e8e9 100644
--- a/app/models/label_link.rb
+++ b/app/models/label_link.rb
@@ -3,7 +3,7 @@
class LabelLink < ActiveRecord::Base
include Importable
- belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
+ belongs_to :target, polymorphic: true, inverse_of: :label_links # rubocop:disable Cop/PolymorphicAssociations
belongs_to :label
validates :target, presence: true, unless: :importing?
diff --git a/app/models/label_note.rb b/app/models/label_note.rb
new file mode 100644
index 00000000000..680952cf421
--- /dev/null
+++ b/app/models/label_note.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+class LabelNote < Note
+ attr_accessor :resource_parent
+ attr_reader :events
+
+ def self.from_events(events, resource: nil, resource_parent: nil)
+ resource ||= events.first.issuable
+
+ attrs = {
+ system: true,
+ author: events.first.user,
+ created_at: events.first.created_at,
+ discussion_id: events.first.discussion_id,
+ noteable: resource,
+ system_note_metadata: SystemNoteMetadata.new(action: 'label'),
+ events: events,
+ resource_parent: resource_parent
+ }
+
+ if resource_parent.is_a?(Project)
+ attrs[:project_id] = resource_parent.id
+ end
+
+ LabelNote.new(attrs)
+ end
+
+ def events=(events)
+ @events = events
+
+ update_outdated_markdown
+ end
+
+ def cached_html_up_to_date?(markdown_field)
+ true
+ end
+
+ def note
+ @note ||= note_text
+ end
+
+ def note_html
+ @note_html ||= "<p dir=\"auto\">#{note_text(html: true)}</p>"
+ end
+
+ def project
+ resource_parent if resource_parent.is_a?(Project)
+ end
+
+ def group
+ resource_parent if resource_parent.is_a?(Group)
+ end
+
+ private
+
+ def update_outdated_markdown
+ events.each do |event|
+ if event.outdated_markdown?
+ event.refresh_invalid_reference
+ end
+ end
+ end
+
+ def note_text(html: false)
+ added = labels_str('added', label_refs_by_action('add', html))
+ removed = labels_str('removed', label_refs_by_action('remove', html))
+
+ [added, removed].compact.join(' and ')
+ end
+
+ # returns string containing added/removed labels including
+ # count of deleted labels:
+ #
+ # added ~1 ~2 + 1 deleted label
+ # added 3 deleted labels
+ # added ~1 ~2 labels
+ def labels_str(prefix, label_refs)
+ existing_refs = label_refs.select { |ref| ref.present? }.sort
+ refs_str = existing_refs.empty? ? nil : existing_refs.join(' ')
+
+ deleted = label_refs.count - existing_refs.count
+ deleted_str = deleted == 0 ? nil : "#{deleted} deleted"
+
+ return nil unless refs_str || deleted_str
+
+ label_list_str = [refs_str, deleted_str].compact.join(' + ')
+ suffix = 'label'.pluralize(deleted > 0 ? deleted : existing_refs.count)
+
+ "#{prefix} #{label_list_str} #{suffix}"
+ end
+
+ def label_refs_by_action(action, html)
+ field = html ? :reference_html : :reference
+
+ events.select { |e| e.action == action }.map(&field)
+ end
+end
diff --git a/app/models/license_template.rb b/app/models/license_template.rb
index 0ad75b27827..693a6a89fd2 100644
--- a/app/models/license_template.rb
+++ b/app/models/license_template.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class LicenseTemplate
PROJECT_TEMPLATE_REGEX =
%r{[\<\{\[]
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 0deb44d7916..76920c3c039 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -253,18 +253,6 @@ class Namespace < ActiveRecord::Base
end
end
- # Exports belonging to projects with legacy storage are placed in a common
- # subdirectory of the namespace, so a simple `rm -rf` is sufficient to remove
- # them.
- #
- # Exports of projects using hashed storage are placed in a location defined
- # only by the project ID, so each must be removed individually.
- def remove_exports!
- remove_legacy_exports!
-
- all_projects.with_storage_feature(:repository).find_each(&:remove_exports)
- end
-
def refresh_project_authorizations
owner.refresh_authorized_projects
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 2e343b8f9f8..8f090cc31e6 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -389,18 +389,7 @@ class Note < ActiveRecord::Base
end
def expire_etag_cache
- return unless noteable&.discussions_rendered_on_frontend?
- return unless noteable&.etag_caching_enabled?
-
- Gitlab::EtagCaching::Store.new.touch(etag_key)
- end
-
- def etag_key
- Gitlab::Routing.url_helpers.project_noteable_notes_path(
- project,
- target_type: noteable_type.underscore,
- target_id: noteable_id
- )
+ noteable&.expire_note_etag_cache
end
def touch(*args)
diff --git a/app/models/project.rb b/app/models/project.rb
index 97d9fa355ef..8928bffd36c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -232,6 +232,8 @@ class Project < ActiveRecord::Base
has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
has_many :cluster_ingresses, through: :clusters, source: :application_ingress, class_name: 'Clusters::Applications::Ingress'
+ has_many :prometheus_metrics
+
# Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy
# here.
@@ -1733,16 +1735,12 @@ class Project < ActiveRecord::Base
import_export_shared.archive_path
end
- def export_project_path
- Dir.glob("#{export_path}/*export.tar.gz").max_by { |f| File.ctime(f) }
- end
-
def export_status
if export_in_progress?
:started
elsif after_export_in_progress?
:after_export_action
- elsif export_project_path || export_project_object_exists?
+ elsif export_file_exists?
:finished
else
:none
@@ -1757,21 +1755,19 @@ class Project < ActiveRecord::Base
import_export_shared.after_export_in_progress?
end
- def remove_exports(path = export_path)
- if path.present?
- FileUtils.rm_rf(path)
- elsif export_project_object_exists?
- import_export_upload.remove_export_file!
- import_export_upload.save
- end
+ def remove_exports
+ return unless export_file_exists?
+
+ import_export_upload.remove_export_file!
+ import_export_upload.save
end
- def remove_exported_project_file
- remove_exports(export_project_path)
+ def export_file_exists?
+ export_file&.file
end
- def export_project_object_exists?
- Gitlab::ImportExport.object_storage? && import_export_upload&.export_file&.file
+ def export_file
+ import_export_upload&.export_file
end
def full_path_slug
@@ -2067,6 +2063,12 @@ class Project < ActiveRecord::Base
auto_cancel_pending_pipelines == 'enabled'
end
+ # Update the default branch querying the remote to determine its HEAD
+ def update_root_ref(remote_name)
+ root_ref = repository.find_remote_root_ref(remote_name)
+ change_head(root_ref) if root_ref.present? && root_ref != default_branch
+ end
+
private
def rename_or_migrate_repository!
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
index 35c19049c04..568f870c2db 100644
--- a/app/models/project_services/asana_service.rb
+++ b/app/models/project_services/asana_service.rb
@@ -101,7 +101,7 @@ http://app.asana.com/-/account_api'
task.update(completed: true)
end
rescue => e
- Rails.logger.error(e.message)
+ log_error(e.message)
next
end
end
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index a783a314071..a15780c14f9 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -104,7 +104,7 @@ class IrkerService < Service
new_recipient = URI.join(default_irc_uri, '/', recipient).to_s
uri = consider_uri(URI.parse(new_recipient))
rescue
- Rails.logger.error("Unable to create a valid URL from #{default_irc_uri} and #{recipient}")
+ log_error("Unable to create a valid URL", default_irc_uri: default_irc_uri, recipient: recipient)
end
end
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index c7520d766a8..e1d342be188 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -88,7 +88,7 @@ class IssueTrackerService < Service
rescue Gitlab::HTTP::Error, Timeout::Error, SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED, OpenSSL::SSL::SSLError => error
message = "#{self.type} had an error when trying to connect to #{self.project_url}: #{error.message}"
end
- Rails.logger.info(message)
+ log_info(message)
result
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index cc98b3f5a41..ba7fcb0cf93 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -205,7 +205,7 @@ class JiraService < IssueTrackerService
begin
issue.transitions.build.save!(transition: { id: transition_id })
rescue => error
- Rails.logger.info "#{self.class.name} Issue Transition failed message ERROR: #{client_url} - #{error.message}"
+ log_error("Issue transition failed", error: error.message, client_url: client_url)
return false
end
end
@@ -257,9 +257,8 @@ class JiraService < IssueTrackerService
new_remote_link.save!(remote_link_props)
end
- result_message = "#{self.class.name} SUCCESS: Successfully posted to #{client_url}."
- Rails.logger.info(result_message)
- result_message
+ log_info("Successfully posted", client_url: client_url)
+ "SUCCESS: Successfully posted to http://jira.example.net."
end
end
@@ -317,7 +316,7 @@ class JiraService < IssueTrackerService
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => e
@error = e.message
- Rails.logger.info "#{self.class.name} Send message ERROR: #{client_url} - #{@error}"
+ log_error("Error sending message", client_url: client_url, error: @error)
nil
end
diff --git a/app/models/prometheus_metric.rb b/app/models/prometheus_metric.rb
new file mode 100644
index 00000000000..ce2db9cb44c
--- /dev/null
+++ b/app/models/prometheus_metric.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+class PrometheusMetric < ActiveRecord::Base
+ belongs_to :project, validate: true, inverse_of: :prometheus_metrics
+
+ enum group: {
+ # built-in groups
+ nginx_ingress: -1,
+ ha_proxy: -2,
+ aws_elb: -3,
+ nginx: -4,
+ kubernetes: -5,
+
+ # custom/user groups
+ business: 0,
+ response: 1,
+ system: 2
+ }
+
+ validates :title, presence: true
+ validates :query, presence: true
+ validates :group, presence: true
+ validates :y_label, presence: true
+ validates :unit, presence: true
+
+ validates :project, presence: true, unless: :common?
+ validates :project, absence: true, if: :common?
+
+ scope :common, -> { where(common: true) }
+
+ GROUP_TITLES = {
+ # built-in groups
+ nginx_ingress: _('Response metrics (NGINX Ingress)'),
+ ha_proxy: _('Response metrics (HA Proxy)'),
+ aws_elb: _('Response metrics (AWS ELB)'),
+ nginx: _('Response metrics (NGINX)'),
+ kubernetes: _('System metrics (Kubernetes)'),
+
+ # custom/user groups
+ business: _('Business metrics (Custom)'),
+ response: _('Response metrics (Custom)'),
+ system: _('System metrics (Custom)')
+ }.freeze
+
+ REQUIRED_METRICS = {
+ nginx_ingress: %w(nginx_upstream_responses_total nginx_upstream_response_msecs_avg),
+ ha_proxy: %w(haproxy_frontend_http_requests_total haproxy_frontend_http_responses_total),
+ aws_elb: %w(aws_elb_request_count_sum aws_elb_latency_average aws_elb_httpcode_backend_5_xx_sum),
+ nginx: %w(nginx_server_requests nginx_server_requestMsec),
+ kubernetes: %w(container_memory_usage_bytes container_cpu_usage_seconds_total)
+ }.freeze
+
+ def group_title
+ GROUP_TITLES[group.to_sym]
+ end
+
+ def required_metrics
+ REQUIRED_METRICS[group.to_sym].to_a.map(&:to_s)
+ end
+
+ def to_query_metric
+ Gitlab::Prometheus::Metric.new(id: id, title: title, required_metrics: required_metrics, weight: 0, y_label: y_label, queries: queries)
+ end
+
+ def queries
+ [
+ {
+ query_range: query,
+ unit: unit,
+ label: legend,
+ series: query_series
+ }.compact
+ ]
+ end
+
+ def query_series
+ case legend
+ when 'Status Code'
+ [{
+ label: 'status_code',
+ when: [
+ { value: '2xx', color: 'green' },
+ { value: '4xx', color: 'orange' },
+ { value: '5xx', color: 'red' }
+ ]
+ }]
+ end
+ end
+end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 929d28b9d88..e98021af818 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -24,6 +24,7 @@ class Repository
delegate :ref_name_for_sha, to: :raw_repository
delegate :bundle_to_disk, to: :raw_repository
+ delegate :find_remote_root_ref, to: :raw_repository
CreateTreeError = Class.new(StandardError)
diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb
index 42c255fcd1e..3fd96b9dc18 100644
--- a/app/models/resource_label_event.rb
+++ b/app/models/resource_label_event.rb
@@ -3,33 +3,122 @@
# This model is not used yet, it will be used for:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/48483
class ResourceLabelEvent < ActiveRecord::Base
+ include Importable
+ include Gitlab::Utils::StrongMemoize
+ include CacheMarkdownField
+
+ cache_markdown_field :reference
+
belongs_to :user
belongs_to :issue
belongs_to :merge_request
belongs_to :label
- validates :user, presence: true, on: :create
- validates :label, presence: true, on: :create
+ scope :created_after, ->(time) { where('created_at > ?', time) }
+
+ validates :user, presence: { unless: :importing? }, on: :create
+ validates :label, presence: { unless: :importing? }, on: :create
validate :exactly_one_issuable
+ after_save :expire_etag_cache
+ after_destroy :expire_etag_cache
+
enum action: {
add: 1,
remove: 2
}
- def self.issuable_columns
- %i(issue_id merge_request_id).freeze
+ def self.issuable_attrs
+ %i(issue merge_request).freeze
end
def issuable
issue || merge_request
end
+ # create same discussion id for all actions with the same user and time
+ def discussion_id(resource = nil)
+ strong_memoize(:discussion_id) do
+ Digest::SHA1.hexdigest([self.class.name, created_at, user_id].join("-"))
+ end
+ end
+
+ def project
+ issuable.project
+ end
+
+ def group
+ issuable.group if issuable.respond_to?(:group)
+ end
+
+ def outdated_markdown?
+ return true if label_id.nil? && reference.present?
+
+ reference.nil? || latest_cached_markdown_version != cached_markdown_version
+ end
+
+ def banzai_render_context(field)
+ super.merge(pipeline: 'label', only_path: true)
+ end
+
+ def refresh_invalid_reference
+ # label_id could be nullified on label delete
+ self.reference = '' if label_id.nil?
+
+ # reference is not set for events which were not rendered yet
+ self.reference ||= label_reference
+
+ if changed?
+ save
+ elsif invalidated_markdown_cache?
+ refresh_markdown_cache!
+ end
+ end
+
private
+ def label_reference
+ if local_label?
+ label.to_reference(format: :id)
+ elsif label.is_a?(GroupLabel)
+ label.to_reference(label.group, target_project: resource_parent, format: :id)
+ else
+ label.to_reference(resource_parent, format: :id)
+ end
+ end
+
def exactly_one_issuable
- if self.class.issuable_columns.count { |attr| self[attr] } != 1
- errors.add(:base, "Exactly one of #{self.class.issuable_columns.join(', ')} is required")
+ issuable_count = self.class.issuable_attrs.count { |attr| self["#{attr}_id"] }
+
+ return true if issuable_count == 1
+
+ # if none of issuable IDs is set, check explicitly if nested issuable
+ # object is set, this is used during project import
+ if issuable_count == 0 && importing?
+ issuable_count = self.class.issuable_attrs.count { |attr| self.public_send(attr) } # rubocop:disable GitlabSecurity/PublicSend
+
+ return true if issuable_count == 1
end
+
+ errors.add(:base, "Exactly one of #{self.class.issuable_attrs.join(', ')} is required")
+ end
+
+ def expire_etag_cache
+ issuable.expire_note_etag_cache
+ end
+
+ def local_label?
+ params = { include_ancestor_groups: true }
+ if resource_parent.is_a?(Project)
+ params[:project_id] = resource_parent.id
+ else
+ params[:group_id] = resource_parent.id
+ end
+
+ LabelsFinder.new(nil, params).execute(skip_authorization: true).where(id: label.id).any?
+ end
+
+ def resource_parent
+ issuable.project || issuable.group
end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index 140058771ee..4dbda7acab6 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -5,6 +5,7 @@
class Service < ActiveRecord::Base
include Sortable
include Importable
+ include ProjectServicesLoggable
serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 376ef673ca8..6fadbcefa53 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -15,7 +15,7 @@ class SystemNoteMetadata < ActiveRecord::Base
commit description merge confidential visible label assignee cross_reference
title time_tracking branch milestone discussion task moved
opened closed merged duplicate locked unlocked
- outdated tag
+ outdated tag due_date
].freeze
validates :note, presence: true
diff --git a/app/models/user.rb b/app/models/user.rb
index 0fcc952b5cd..568ec101016 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -490,6 +490,16 @@ class User < ActiveRecord::Base
u.name = 'Ghost User'
end
end
+
+ # Return true if there is only single non-internal user in the deployment,
+ # ghost user is ignored.
+ def single_user?
+ User.non_internal.limit(2).count == 1
+ end
+
+ def single_user
+ User.non_internal.first if single_user?
+ end
end
def full_path
@@ -1287,6 +1297,10 @@ class User < ActiveRecord::Base
!terms_accepted?
end
+ def requires_usage_stats_consent?
+ !consented_usage_stats? && 7.days.ago > self.created_at && !has_current_license? && User.single_user?
+ end
+
# @deprecated
alias_method :owned_or_masters_groups, :owned_or_maintainers_groups
@@ -1301,6 +1315,14 @@ class User < ActiveRecord::Base
private
+ def has_current_license?
+ false
+ end
+
+ def consented_usage_stats?
+ Gitlab::CurrentSettings.usage_stats_set_by_user_id == self.id
+ end
+
def owned_projects_union
Gitlab::SQL::Union.new([
Project.where(namespace: namespace),
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index 97e955ace36..1cd05cf3aac 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -5,7 +5,8 @@ class UserCallout < ActiveRecord::Base
enum feature_name: {
gke_cluster_integration: 1,
- gcp_signup_offer: 2
+ gcp_signup_offer: 2,
+ cluster_security_warning: 3
}
validates :user, presence: true