Welcome to mirror list, hosted at ThFree Co, Russian Federation.

meta.rb « wiki_page « models « app - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 215d84dc463f2953fb931bc3d0ee8d85fe795e84 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# frozen_string_literal: true

class WikiPage
  class Meta < ApplicationRecord
    include Gitlab::Utils::StrongMemoize

    CanonicalSlugConflictError = Class.new(ActiveRecord::RecordInvalid)
    WikiPageInvalid = Class.new(ArgumentError)

    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

    class << self
      # 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
      #
      # This method raises errors on validation issues.
      def find_or_create(last_known_slug, wiki_page)
        raise WikiPageInvalid unless wiki_page.valid?

        project = wiki_page.wiki.project
        known_slugs = [last_known_slug, wiki_page.slug].compact.uniq
        raise 'No slugs found! This should not be possible.' if known_slugs.empty?

        transaction do
          updates = wiki_page_updates(wiki_page)
          found = find_by_canonical_slug(known_slugs, project)
          meta = found || create!(updates.merge(project_id: project.id))

          meta.update_state(found.nil?, known_slugs, wiki_page, updates)

          # 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 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

      private

      def wiki_page_updates(wiki_page)
        last_commit_date = wiki_page.version_commit_timestamp || Time.now.utc

        {
          title: wiki_page.title,
          created_at: last_commit_date,
          updated_at: last_commit_date
        }
      end
    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, updates)
      update_wiki_page_attributes(updates)
      insert_slugs(known_slugs, created, wiki_page.slug)
      self.canonical_slug = wiki_page.slug
    end

    private

    def update_wiki_page_attributes(updates)
      # Remove all unnecessary updates:
      updates.delete(:updated_at) if updated_at == updates[:updated_at]
      updates.delete(:created_at) if created_at <= updates[:created_at]
      updates.delete(:title) if title == updates[:title]

      update_columns(updates) unless updates.empty?
    end

    def insert_slugs(strings, is_new, canonical_slug)
      creation = Time.current.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