Module: CacheMarkdownField

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)

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

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.new("Unknown field: #{field.inspect}") unless
    cached_markdown_fields.markdown_fields.include?(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)

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

def cached_html_for(markdown_field)
  raise ArgumentError.new("Unknown field: #{markdown_field}") unless
    cached_markdown_fields.markdown_fields.include?(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)

77
78
79
80
81
82
83
84
85
86
87
# File 'app/models/concerns/cache_markdown_field.rb', line 77

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)

89
90
91
# File 'app/models/concerns/cache_markdown_field.rb', line 89

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

#latest_cached_markdown_versionObject


114
115
116
# File 'app/models/concerns/cache_markdown_field.rb', line 114

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

#local_versionObject


118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'app/models/concerns/cache_markdown_field.rb', line 118

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

#parent_userObject


139
140
141
# File 'app/models/concerns/cache_markdown_field.rb', line 139

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.map do |markdown_field|
    [
      cached_markdown_fields.html_field(markdown_field),
      rendered_field_content(markdown_field)
    ]
  end.to_h

  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
# File 'app/models/concerns/cache_markdown_field.rb', line 71

def refresh_markdown_cache!
  updates = refresh_markdown_cache

  save_markdown(updates)
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

#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


106
107
108
109
110
111
112
# File 'app/models/concerns/cache_markdown_field.rb', line 106

def updated_cached_html_for(markdown_field)
  return unless cached_markdown_fields.markdown_fields.include?(markdown_field)

  refresh_markdown_cache! if attribute_invalidated?(cached_markdown_fields.html_field(markdown_field))

  cached_html_for(markdown_field)
end