diff options
author | Jan Provaznik <jprovaznik@gitlab.com> | 2018-09-07 16:39:20 +0300 |
---|---|---|
committer | Jan Provaznik <jprovaznik@gitlab.com> | 2018-09-07 16:39:20 +0300 |
commit | d95c1f0335f7309114fcbb0d5413b28e1701a640 (patch) | |
tree | 6b22580a79dd1f929aecd158c31706ce3870c39b /app/models | |
parent | 81f4dc059db91577f72134e6008680b72029a29e (diff) |
Use ResourceLabelEvent for tracking label changes
Diffstat (limited to 'app/models')
-rw-r--r-- | app/models/concerns/issuable.rb | 4 | ||||
-rw-r--r-- | app/models/concerns/noteable.rb | 19 | ||||
-rw-r--r-- | app/models/label_note.rb | 97 | ||||
-rw-r--r-- | app/models/note.rb | 13 | ||||
-rw-r--r-- | app/models/resource_label_event.rb | 101 |
5 files changed, 212 insertions, 22 deletions
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 7f14d78e976..5f65fceb7af 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -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/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/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/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 |