Module: CacheMarkdownField

Extended by:
ActiveSupport::Concern
Included in:
AbuseReport, Appearance, ApplicationSetting, ApplicationSetting::Term, BroadcastMessage, Commit, Issuable, Label, Namespace, Note, Project, Release, ResourceLabelEvent, Snippet, Timebox, UserStatus, WorkItems::Type
Defined in:
app/models/concerns/cache_markdown_field.rb

Overview

This module takes care of updating cache columns for Markdown-containing fields. Use like this in the body of your class:

include CacheMarkdownField
cache_markdown_field :foo
cache_markdown_field :bar
cache_markdown_field :baz, pipeline: :single_line
cache_markdown_field :baz, whitelisted: true

Corresponding foo_html, bar_html and baz_html fields should exist.

Constant Summary collapse

INVALIDATED_BY =

changes to these attributes cause the cache to be invalidates

%w[author project].freeze

Instance Method Summary collapse

Instance Method Details

#attribute_invalidated?(attr) ⇒ Boolean

Returns:

  • (Boolean)

97
98
99
# File 'app/models/concerns/cache_markdown_field.rb', line 97

def attribute_invalidated?(attr)
  __send__("#{attr}_invalidated?") # rubocop:disable GitlabSecurity/PublicSend
end

#banzai_render_context(field) ⇒ Object

Returns the default Banzai render context for the cached markdown field.

Raises:

  • (ArgumentError)

28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'app/models/concerns/cache_markdown_field.rb', line 28

def banzai_render_context(field)
  raise ArgumentError, "Unknown field: #{field.inspect}" unless
    cached_markdown_fields.key?(field)

  # Always include a project key, or Banzai complains
  project = self.project if self.respond_to?(:project)
  group   = self.group if self.respond_to?(:group)
  context = cached_markdown_fields[field].merge(project: project, group: group)

  # Banzai is less strict about authors, so don't always have an author key
  context[:author] = self.author if self.respond_to?(:author)

  context[:markdown_engine] = :common_mark

  if Feature.enabled?(:personal_snippet_reference_filters, context[:author])
    context[:user] = self.parent_user
  end

  context
end

#cached_html_for(markdown_field) ⇒ Object

Raises:

  • (ArgumentError)

101
102
103
104
105
106
# File 'app/models/concerns/cache_markdown_field.rb', line 101

def cached_html_for(markdown_field)
  raise ArgumentError, "Unknown field: #{markdown_field}" unless
    cached_markdown_fields.key?(markdown_field)

  __send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend
end

#cached_html_up_to_date?(markdown_field) ⇒ Boolean

Returns:

  • (Boolean)

81
82
83
84
85
86
87
88
89
90
91
# File 'app/models/concerns/cache_markdown_field.rb', line 81

def cached_html_up_to_date?(markdown_field)
  return false if cached_html_for(markdown_field).nil? && __send__(markdown_field).present? # rubocop:disable GitlabSecurity/PublicSend

  html_field = cached_markdown_fields.html_field(markdown_field)

  markdown_changed = markdown_field_changed?(markdown_field)
  html_changed = markdown_field_changed?(html_field)

  latest_cached_markdown_version == cached_markdown_version &&
    (html_changed || markdown_changed == html_changed)
end

#can_cache_field?(field) ⇒ Boolean

Returns:

  • (Boolean)

23
24
25
# File 'app/models/concerns/cache_markdown_field.rb', line 23

def can_cache_field?(field)
  true
end

#invalidated_markdown_cache?Boolean

Returns:

  • (Boolean)

93
94
95
# File 'app/models/concerns/cache_markdown_field.rb', line 93

def invalidated_markdown_cache?
  cached_markdown_fields.html_fields.any? {|html_field| attribute_invalidated?(html_field) }
end

#latest_cached_markdown_versionObject


130
131
132
# File 'app/models/concerns/cache_markdown_field.rb', line 130

def latest_cached_markdown_version
  @latest_cached_markdown_version ||= (Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION << 16) | local_version
end

#local_versionObject


134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'app/models/concerns/cache_markdown_field.rb', line 134

def local_version
  # because local_markdown_version is stored in application_settings which
  # uses cached_markdown_version too, we check explicitly to avoid
  # endless loop
  return local_markdown_version if respond_to?(:has_attribute?) && has_attribute?(:local_markdown_version)

  settings = Gitlab::CurrentSettings.current_application_settings

  # Following migrations are not properly isolated and
  # use real models (by calling .ghost method), in these migrations
  # local_markdown_version attribute doesn't exist yet, so we
  # use a default value:
  # db/migrate/20170825104051_migrate_issues_to_ghost_user.rb
  # db/migrate/20171114150259_merge_requests_author_id_foreign_key.rb
  if settings.respond_to?(:local_markdown_version)
    settings.local_markdown_version
  else
    0
  end
end

#mentionable_attributes_changed?(changes = saved_changes) ⇒ Boolean

Returns:

  • (Boolean)

185
186
187
188
189
190
191
192
# File 'app/models/concerns/cache_markdown_field.rb', line 185

def mentionable_attributes_changed?(changes = saved_changes)
  return false unless is_a?(Mentionable)

  self.class.mentionable_attrs.any? do |attr|
    changes.key?(cached_markdown_fields.html_field(attr.first)) &&
      changes.fetch(cached_markdown_fields.html_field(attr.first)).last.present?
  end
end

#parent_userObject


155
156
157
# File 'app/models/concerns/cache_markdown_field.rb', line 155

def parent_user
  nil
end

#refresh_markdown_cacheObject

Update every applicable column in a row if any one is invalidated, as we only store one version per row


58
59
60
61
62
63
64
65
66
67
68
69
# File 'app/models/concerns/cache_markdown_field.rb', line 58

def refresh_markdown_cache
  updates = cached_markdown_fields.markdown_fields.to_h do |markdown_field|
    [
      cached_markdown_fields.html_field(markdown_field),
      rendered_field_content(markdown_field)
    ]
  end

  updates['cached_markdown_version'] = latest_cached_markdown_version

  updates.each { |field, data| write_markdown_field(field, data) }
end

#refresh_markdown_cache!Object


71
72
73
74
75
76
77
78
79
# File 'app/models/concerns/cache_markdown_field.rb', line 71

def refresh_markdown_cache!
  updates = refresh_markdown_cache
  if updates.present? && save_markdown(updates)
    # save_markdown updates DB columns directly, so compute and save mentions
    # by calling store_mentions! or we end-up with missing mentions although those
    # would appear in the notes, descriptions, etc in the UI
    store_mentions! if mentionable_attributes_changed?(updates)
  end
end

#rendered_field_content(markdown_field) ⇒ Object


49
50
51
52
53
54
# File 'app/models/concerns/cache_markdown_field.rb', line 49

def rendered_field_content(markdown_field)
  return unless can_cache_field?(markdown_field)

  options = { skip_project_check: skip_project_check? }
  Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
end

#skip_project_check?Boolean

Returns:

  • (Boolean)

19
20
21
# File 'app/models/concerns/cache_markdown_field.rb', line 19

def skip_project_check?
  false
end

#store_mentions!Object


159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'app/models/concerns/cache_markdown_field.rb', line 159

def store_mentions!
  # We can only store mentions if the mentionable is a database object
  return unless self.is_a?(ApplicationRecord)

  identifier = user_mention_identifier

  # this may happen due to notes polymorphism, so noteable_id may point to a record
  # that no longer exists as we cannot have FK on noteable_id
  return if identifier.blank?

  refs = all_references(self.author)

  references = {}
  references[:mentioned_users_ids] = refs.mentioned_user_ids.presence
  references[:mentioned_groups_ids] = refs.mentioned_group_ids.presence
  references[:mentioned_projects_ids] = refs.mentioned_project_ids.presence

  if references.compact.any?
    user_mention_class.upsert(references.merge(identifier), unique_by: identifier.compact.keys)
  else
    user_mention_class.delete_by(identifier)
  end

  true
end

#updated_cached_html_for(markdown_field) ⇒ Object

Updates the markdown cache if necessary, then returns the field Unlike `cached_html_for` it returns `nil` if the field does not exist


110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'app/models/concerns/cache_markdown_field.rb', line 110

def updated_cached_html_for(markdown_field)
  return unless cached_markdown_fields.key?(markdown_field)

  if attribute_invalidated?(cached_markdown_fields.html_field(markdown_field))
    # Invalidated due to Markdown content change
    # We should not persist the updated HTML here since this will depend on whether the
    # Markdown content change will be persisted. Both will be persisted together when the model is saved.
    if changed_attributes.key?(markdown_field)
      refresh_markdown_cache
    else
      # Invalidated due to stale HTML cache
      # This could happen when the Markdown cache version is bumped or when a model is imported and the HTML is empty.
      # We persist the updated HTML here so that subsequent calls to this method do not have to regenerate the HTML again.
      refresh_markdown_cache!
    end
  end

  cached_html_for(markdown_field)
end