132 lines
3.9 KiB
Ruby
132 lines
3.9 KiB
Ruby
|
# 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
|
||
|
#
|
||
|
# Corresponding foo_html, bar_html and baz_html fields should exist.
|
||
|
module CacheMarkdownField
|
||
|
# Knows about the relationship between markdown and html field names, and
|
||
|
# stores the rendering contexts for the latter
|
||
|
class FieldData
|
||
|
extend Forwardable
|
||
|
|
||
|
def initialize
|
||
|
@data = {}
|
||
|
end
|
||
|
|
||
|
def_delegators :@data, :[], :[]=
|
||
|
def_delegator :@data, :keys, :markdown_fields
|
||
|
|
||
|
def html_field(markdown_field)
|
||
|
"#{markdown_field}_html"
|
||
|
end
|
||
|
|
||
|
def html_fields
|
||
|
markdown_fields.map {|field| html_field(field) }
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# Dynamic registries don't really work in Rails as it's not guaranteed that
|
||
|
# every class will be loaded, so hardcode the list.
|
||
|
CACHING_CLASSES = %w[
|
||
|
AbuseReport
|
||
|
Appearance
|
||
|
ApplicationSetting
|
||
|
BroadcastMessage
|
||
|
Issue
|
||
|
Label
|
||
|
MergeRequest
|
||
|
Milestone
|
||
|
Namespace
|
||
|
Note
|
||
|
Project
|
||
|
Release
|
||
|
Snippet
|
||
|
]
|
||
|
|
||
|
def self.caching_classes
|
||
|
CACHING_CLASSES.map(&:constantize)
|
||
|
end
|
||
|
|
||
|
extend ActiveSupport::Concern
|
||
|
|
||
|
included do
|
||
|
cattr_reader :cached_markdown_fields do
|
||
|
FieldData.new
|
||
|
end
|
||
|
|
||
|
# Returns the default Banzai render context for the cached markdown field.
|
||
|
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)
|
||
|
context = cached_markdown_fields[field].merge(project: project)
|
||
|
|
||
|
# Banzai is less strict about authors, so don't always have an author key
|
||
|
context[:author] = self.author if self.respond_to?(:author)
|
||
|
|
||
|
context
|
||
|
end
|
||
|
|
||
|
# Allow callers to look up the cache field name, rather than hardcoding it
|
||
|
def markdown_cache_field_for(field)
|
||
|
raise ArgumentError.new("Unknown field: #{field}") unless
|
||
|
cached_markdown_fields.markdown_fields.include?(field)
|
||
|
|
||
|
cached_markdown_fields.html_field(field)
|
||
|
end
|
||
|
|
||
|
# Always exclude _html fields from attributes (including serialization).
|
||
|
# They contain unredacted HTML, which would be a security issue
|
||
|
alias_method :attributes_before_markdown_cache, :attributes
|
||
|
def attributes
|
||
|
attrs = attributes_before_markdown_cache
|
||
|
|
||
|
cached_markdown_fields.html_fields.each do |field|
|
||
|
attrs.delete(field)
|
||
|
end
|
||
|
|
||
|
attrs
|
||
|
end
|
||
|
end
|
||
|
|
||
|
class_methods do
|
||
|
private
|
||
|
|
||
|
# Specify that a field is markdown. Its rendered output will be cached in
|
||
|
# a corresponding _html field. Any custom rendering options may be provided
|
||
|
# as a context.
|
||
|
def cache_markdown_field(markdown_field, context = {})
|
||
|
raise "Add #{self} to CacheMarkdownField::CACHING_CLASSES" unless
|
||
|
CacheMarkdownField::CACHING_CLASSES.include?(self.to_s)
|
||
|
|
||
|
cached_markdown_fields[markdown_field] = context
|
||
|
|
||
|
html_field = cached_markdown_fields.html_field(markdown_field)
|
||
|
cache_method = "#{markdown_field}_cache_refresh".to_sym
|
||
|
invalidation_method = "#{html_field}_invalidated?".to_sym
|
||
|
|
||
|
define_method(cache_method) do
|
||
|
html = Banzai::Renderer.cacheless_render_field(self, markdown_field)
|
||
|
__send__("#{html_field}=", html)
|
||
|
true
|
||
|
end
|
||
|
|
||
|
# The HTML becomes invalid if any dependent fields change. For now, assume
|
||
|
# author and project invalidate the cache in all circumstances.
|
||
|
define_method(invalidation_method) do
|
||
|
changed_fields = changed_attributes.keys
|
||
|
invalidations = changed_fields & [markdown_field.to_s, "author", "project"]
|
||
|
!invalidations.empty?
|
||
|
end
|
||
|
|
||
|
before_save cache_method, if: invalidation_method
|
||
|
end
|
||
|
end
|
||
|
end
|