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
- #attribute_invalidated?(attr) ⇒ Boolean
-
#banzai_render_context(field) ⇒ Object
Returns the default Banzai render context for the cached markdown field.
- #cached_html_for(markdown_field) ⇒ Object
- #cached_html_up_to_date?(markdown_field) ⇒ Boolean
- #can_cache_field?(field) ⇒ Boolean
- #invalidated_markdown_cache? ⇒ Boolean
- #latest_cached_markdown_version ⇒ Object
- #local_version ⇒ Object
- #mentionable_attributes_changed?(changes = saved_changes) ⇒ Boolean
- #parent_user ⇒ Object
-
#refresh_markdown_cache ⇒ Object
Update every applicable column in a row if any one is invalidated, as we only store one version per row.
- #refresh_markdown_cache! ⇒ Object
- #rendered_field_content(markdown_field) ⇒ Object
- #skip_project_check? ⇒ Boolean
- #store_mentions! ⇒ Object
-
#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.
Instance Method Details
#attribute_invalidated?(attr) ⇒ 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.
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. 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
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
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
23 24 25 |
# File 'app/models/concerns/cache_markdown_field.rb', line 23 def can_cache_field?(field) true end |
#invalidated_markdown_cache? ⇒ 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_version ⇒ Object
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_version ⇒ Object
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
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_user ⇒ Object
155 156 157 |
# File 'app/models/concerns/cache_markdown_field.rb', line 155 def parent_user nil end |
#refresh_markdown_cache ⇒ Object
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) = { skip_project_check: skip_project_check? } Banzai::Renderer.cacheless_render_field(self, markdown_field, ) end |
#skip_project_check? ⇒ 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.) 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 |