diff options
Diffstat (limited to 'app/models')
-rw-r--r-- | app/models/event.rb | 31 | ||||
-rw-r--r-- | app/models/project_wiki.rb | 9 | ||||
-rw-r--r-- | app/models/wiki_page.rb | 13 | ||||
-rw-r--r-- | app/models/wiki_page/meta.rb | 136 | ||||
-rw-r--r-- | app/models/wiki_page/slug.rb | 26 |
5 files changed, 210 insertions, 5 deletions
diff --git a/app/models/event.rb b/app/models/event.rb index 18682bd694c..c4ca5389fdf 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -6,6 +6,7 @@ class Event < ApplicationRecord include Presentable include DeleteWithLimit include CreatedAtFilterable + include Gitlab::Utils::StrongMemoize default_scope { reorder(nil) } @@ -42,7 +43,8 @@ class Event < ApplicationRecord note: Note, project: Project, snippet: Snippet, - user: User + user: User, + wiki: WikiPage::Meta ).freeze RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour @@ -79,6 +81,7 @@ class Event < ApplicationRecord scope :recent, -> { reorder(id: :desc) } scope :code_push, -> { where(action: PUSHED) } scope :merged, -> { where(action: MERGED) } + scope :for_wiki_page, -> { where(target_type: WikiPage::Meta.name) } scope :with_associations, -> do # We're using preload for "push_event_payload" as otherwise the association @@ -197,6 +200,14 @@ class Event < ApplicationRecord created_action? && !target && target_type.nil? end + def created_wiki_page? + wiki_page? && action == CREATED + end + + def updated_wiki_page? + wiki_page? && action == UPDATED + end + def created_target? created_action? && target end @@ -217,6 +228,10 @@ class Event < ApplicationRecord target_type == "MergeRequest" end + def wiki_page? + target_type == WikiPage::Meta.name + end + def milestone target if milestone? end @@ -229,6 +244,14 @@ class Event < ApplicationRecord target if merge_request? end + def wiki_page + strong_memoize(:wiki_page) do + next unless wiki_page? + + ProjectWiki.new(project, author).find_page(target.canonical_slug) + end + end + def note target if note? end @@ -250,6 +273,10 @@ class Event < ApplicationRecord 'destroyed' elsif commented_action? "commented on" + elsif created_wiki_page? + 'created' + elsif updated_wiki_page? + 'updated' elsif created_project_action? created_project_action_name else @@ -362,6 +389,8 @@ class Event < ApplicationRecord :read_snippet elsif milestone? :read_milestone + elsif wiki_page? + :read_wiki end end end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index f8528a41634..50ce1c67453 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -19,10 +19,11 @@ class ProjectWiki DIRECTION_DESC = 'desc' DIRECTION_ASC = 'asc' + attr_reader :project, :user + # Returns a string describing what went wrong after # an operation fails. attr_reader :error_message - attr_reader :project def initialize(project, user = nil) @project = project @@ -196,9 +197,9 @@ class ProjectWiki def commit_details(action, message = nil, title = nil) commit_message = message.presence || default_message(action, title) - git_user = Gitlab::Git::User.from_gitlab(@user) + git_user = Gitlab::Git::User.from_gitlab(user) - Gitlab::Git::Wiki::CommitDetails.new(@user.id, + Gitlab::Git::Wiki::CommitDetails.new(user.id, git_user.username, git_user.name, git_user.email, @@ -206,7 +207,7 @@ class ProjectWiki end def default_message(action, title) - "#{@user.username} #{action} page: #{title}" + "#{user.username} #{action} page: #{title}" end def update_project_activity diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 5436f034657..fb65432024e 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -21,6 +21,14 @@ class WikiPage ActiveModel::Name.new(self, nil, 'wiki') end + def eql?(other) + return false unless other.present? && other.is_a?(self.class) + + slug == other.slug && wiki.project == other.wiki.project + end + + alias_method :==, :eql? + # Sorts and groups pages by directory. # # pages - an array of WikiPage objects. @@ -58,6 +66,7 @@ class WikiPage # The GitLab ProjectWiki instance. attr_reader :wiki + delegate :project, to: :wiki # The raw Gitlab::Git::WikiPage instance. attr_reader :page @@ -70,6 +79,10 @@ class WikiPage Gitlab::HookData::WikiPageBuilder.new(self).build end + # Construct a new WikiPage + # + # @param [ProjectWiki] wiki + # @param [Gitlab::Git::WikiPage] page def initialize(wiki, page = nil) @wiki = wiki @page = page diff --git a/app/models/wiki_page/meta.rb b/app/models/wiki_page/meta.rb new file mode 100644 index 00000000000..2af7d86ebcc --- /dev/null +++ b/app/models/wiki_page/meta.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +class WikiPage + class Meta < ApplicationRecord + include Gitlab::Utils::StrongMemoize + + CanonicalSlugConflictError = Class.new(ActiveRecord::RecordInvalid) + + self.table_name = 'wiki_page_meta' + + belongs_to :project + + has_many :slugs, class_name: 'WikiPage::Slug', foreign_key: 'wiki_page_meta_id', inverse_of: :wiki_page_meta + has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent + + validates :title, presence: true + validates :project_id, presence: true + validate :no_two_metarecords_in_same_project_can_have_same_canonical_slug + + scope :with_canonical_slug, ->(slug) do + joins(:slugs).where(wiki_page_slugs: { canonical: true, slug: slug }) + end + + alias_method :resource_parent, :project + + # Return the (updated) WikiPage::Meta record for a given wiki page + # + # If none is found, then a new record is created, and its fields are set + # to reflect the wiki_page passed. + # + # @param [String] last_known_slug + # @param [WikiPage] wiki_page + # + # As with all `find_or_create` methods, this one raises errors on + # validation issues. + def self.find_or_create(last_known_slug, wiki_page) + project = wiki_page.wiki.project + known_slugs = [last_known_slug, wiki_page.slug].compact.uniq + raise 'no slugs!' if known_slugs.empty? + + transaction do + found = find_by_canonical_slug(known_slugs, project) + meta = found || create(title: wiki_page.title, project_id: project.id) + + meta.update_state(found.nil?, known_slugs, wiki_page) + + # We don't need to run validations here, since find_by_canonical_slug + # guarantees that there is no conflict in canonical_slug, and DB + # constraints on title and project_id enforce our other invariants + # This saves us a query. + meta + end + end + + def self.find_by_canonical_slug(canonical_slug, project) + meta, conflict = with_canonical_slug(canonical_slug) + .where(project_id: project.id) + .limit(2) + + if conflict.present? + meta.errors.add(:canonical_slug, 'Duplicate value found') + raise CanonicalSlugConflictError.new(meta) + end + + meta + end + + def canonical_slug + strong_memoize(:canonical_slug) { slugs.canonical.first&.slug } + end + + def canonical_slug=(slug) + return if @canonical_slug == slug + + if persisted? + transaction do + slugs.canonical.update_all(canonical: false) + page_slug = slugs.create_with(canonical: true).find_or_create_by(slug: slug) + page_slug.update_columns(canonical: true) unless page_slug.canonical? + end + else + slugs.new(slug: slug, canonical: true) + end + + @canonical_slug = slug + end + + def update_state(created, known_slugs, wiki_page) + update_wiki_page_attributes(wiki_page) + insert_slugs(known_slugs, created, wiki_page.slug) + self.canonical_slug = wiki_page.slug + end + + def update_columns(attrs = {}) + super(attrs.reverse_merge(updated_at: Time.now.utc)) + end + + def self.update_all(attrs = {}) + super(attrs.reverse_merge(updated_at: Time.now.utc)) + end + + private + + def update_wiki_page_attributes(page) + update_columns(title: page.title) unless page.title == title + end + + def insert_slugs(strings, is_new, canonical_slug) + creation = Time.now.utc + + slug_attrs = strings.map do |slug| + { + wiki_page_meta_id: id, + slug: slug, + canonical: (is_new && slug == canonical_slug), + created_at: creation, + updated_at: creation + } + end + slugs.insert_all(slug_attrs) unless !is_new && slug_attrs.size == 1 + + @canonical_slug = canonical_slug if is_new || strings.size == 1 + end + + def no_two_metarecords_in_same_project_can_have_same_canonical_slug + return unless project_id.present? && canonical_slug.present? + + offending = self.class.with_canonical_slug(canonical_slug).where(project_id: project_id) + offending = offending.where.not(id: id) if persisted? + + if offending.exists? + errors.add(:canonical_slug, 'each page in a wiki must have a distinct canonical slug') + end + end + end +end diff --git a/app/models/wiki_page/slug.rb b/app/models/wiki_page/slug.rb new file mode 100644 index 00000000000..246fa8d6442 --- /dev/null +++ b/app/models/wiki_page/slug.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class WikiPage + class Slug < ApplicationRecord + self.table_name = 'wiki_page_slugs' + + belongs_to :wiki_page_meta, class_name: 'WikiPage::Meta', inverse_of: :slugs + + validates :slug, presence: true, uniqueness: { scope: :wiki_page_meta_id } + validates :canonical, uniqueness: { + scope: :wiki_page_meta_id, + if: :canonical?, + message: 'Only one slug can be canonical per wiki metadata record' + } + + scope :canonical, -> { where(canonical: true) } + + def update_columns(attrs = {}) + super(attrs.reverse_merge(updated_at: Time.now.utc)) + end + + def self.update_all(attrs = {}) + super(attrs.reverse_merge(updated_at: Time.now.utc)) + end + end +end |