Class: Post
- Inherits:
-
Article
- Object
- ActiveRecord::Base
- ApplicationRecord
- Article
- Post
- Includes:
- Models::LiquidMethods
- Defined in:
- app/models/post.rb
Overview
== Schema Information
Table name: articles
Database name: primary
id :integer not null, primary key
applies_to_product_category_ids :integer default([]), is an Array
applies_to_product_line_ids :integer default([]), is an Array
archived_at :datetime
auto_sanitize_html :boolean default(TRUE), not null
auto_sanitize_urls :boolean default(TRUE), not null
breadcrumbs :string default([]), not null, is an Array
department :string
description :text
has_toc :boolean default(FALSE), not null
inline_js :text
link_audit_result :jsonb
meta_description :string
meta_keywords :string
objective :text
problem_code :string(255)
publish_on :datetime
published_at :datetime
reading_time_minutes :integer
requires_seo_check :boolean default(FALSE), not null
revised_at :datetime
sales :boolean default(TRUE), not null
schema_extracted_at :datetime
schema_markup :jsonb
search_text :text
search_text_tsv :tsvector
seo_report :jsonb
serial_number_high_range :integer
serial_number_low_range :integer
short_url :string
short_url_with_ref :string
slug :string
slug_custom :string
solution :text
sort_booster :integer default(0), not null
state :string(20) default("draft")
subject :string(255) not null
support :boolean default(FALSE), not null
time_required :integer
title :string
toc_selector :string
type :string
version :integer
warranty_labor :boolean
warranty_parts :boolean
word_count :integer
created_at :datetime
updated_at :datetime
active_revision_id :bigint
brafton_id :integer
creator_id :integer
draft_revision_id :bigint
original_author_id :integer
precursor_id :integer
preview_image_id :string
related_article_1_id :integer
related_article_2_id :integer
related_article_3_id :integer
related_article_4_id :integer
source_id :integer
successor_article_id :integer
updater_id :integer
user_id :integer
Indexes
idx_articles_type_state_id (type,state,id)
idx_source_id_type (source_id,type)
idx_state_type_publish_on (state,type,publish_on)
index_articles_on_active_revision_id (active_revision_id)
index_articles_on_draft_revision_id (draft_revision_id)
index_articles_on_published_at (published_at) USING brin
index_articles_on_revised_at (revised_at)
index_articles_on_search_text_tsv (search_text_tsv) USING gin
index_articles_on_successor_article_id (successor_article_id)
index_articles_on_type_and_slug_unique (type,slug) UNIQUE
Foreign Keys
fk_rails_... (id => articles.id)
fk_rails_... (related_article_1_id => articles.id) ON DELETE => nullify
fk_rails_... (related_article_2_id => articles.id) ON DELETE => nullify
fk_rails_... (related_article_3_id => articles.id) ON DELETE => nullify
fk_rails_... (related_article_4_id => articles.id)
fk_rails_... (source_id => sources.id)
fk_rails_... (successor_article_id => articles.id)
Constant Summary collapse
- PARENT_SOURCE_ID =
1422- COMMENT_COUNT_SUBQUERY =
'select count(pc.id) from post_comments pc where pc.post_id = articles.id'- COMMENT_COUNT_SUBQUERY_LAST7D =
%(#{COMMENT_COUNT_SUBQUERY} AND pc.created_at >= current_date - interval '7 days')- POST_TAGS =
%w[company-news countertop-heaters design-trends example-projects general-information heat-tape-for-pipes indoor-heating installation led-mirrors mirror-defoggers outdoor-heating press-industry-report press-release product-information radiant-floor-heating radiant-panels remodeling roof-and-gutter-deicing troubleshooting share-your-story shower-kits snow-melting towel-warmers trade-professionals].freeze
Constants inherited from Article
Article::FAQ_MAX_WORDS, Article::FAQ_TARGET_WORDS, Article::FAQ_WARN_WORDS, Article::FAQ_WORD_COUNT_SQL, Article::STI_TYPES
Constants included from Models::Embeddable
Models::Embeddable::DEFAULT_MODEL, Models::Embeddable::MAX_CONTENT_LENGTH
Constants included from Models::Auditable
Models::Auditable::ALWAYS_IGNORED
Instance Attribute Summary collapse
-
#auto_update_revised_at ⇒ Object
Returns the value of attribute auto_update_revised_at.
-
#revision_change_notes ⇒ Object
Returns the value of attribute revision_change_notes.
Attributes inherited from Article
#schema_markup, #serial_number_high_range, #serial_number_low_range, #subject
Belongs to collapse
Methods inherited from Article
#active_revision, #original_author, #precursor, #rma_reason_code, #successor_article, #user
Methods included from Models::Auditable
Has one collapse
Has many collapse
Methods inherited from Article
#article_pages, #embedded_assets, #item_product_lines, #revisions, #site_maps, #successors, #votes
Methods included from Models::CrossLinkable
#inbound_content_links, #outbound_content_links
Methods included from Models::Taggable
Methods included from Models::Embeddable
Class Method Summary collapse
- .breadcrumbs_select_option ⇒ Object
-
.by_date_range ⇒ ActiveRecord::Relation<Post>
A relation of Posts that are by date range.
- .category_tags_for_select ⇒ Object
-
.purge_all_posts_by_tag ⇒ Object
Class method to purge all posts using tag-based purging This is more efficient than URL-based purging when you want to purge all posts since it makes a single API call to Cloudflare instead of multiple calls for each URL.
- .tag_title(tag) ⇒ Object
-
.with_comments_count ⇒ ActiveRecord::Relation<Post>
A relation of Posts that are with comments count.
Instance Method Summary collapse
- #author_bio ⇒ Object
-
#content_for_embedding(_content_type = :primary, locale: 'en-US') ⇒ Object
Override Article's content_for_embedding for blog post specifics.
- #deep_dup ⇒ Object
-
#embeddable_locales ⇒ Object
Generate per-locale embeddings so locale-conditional Liquid content (Canada vs. USA copy, localised pricing) is encoded separately.
- #embedding_type_name ⇒ Object
-
#liquid_variable_context(_locale, _preview_mode) ⇒ Object
Expose a PostDrop as the 'post' variable in Liquid templates rendered inside this post's solution/description fields.
- #locale_for_embedding ⇒ Object
- #main_public_tag ⇒ Object
- #next_post ⇒ Object
- #previous_post ⇒ Object
- #primary_tag ⇒ Object
-
#purge_edge_cache(include_indirect_associations: true, extra_urls: [], prepare_sitemap: true) ⇒ Object
Public: invoked from controllers to purge cache.
- #to_s ⇒ Object
- #user_name(_skip_link = false) ⇒ Object
Methods inherited from Article
all_product_lines, #applicable_product_category_urls, #applicable_product_line_urls, archived, #article_type_human, #author, available_schema_types, #breadcrumbs_hash, drafts, due_for_publishing, #effective_published_date, #effective_revised_date, #email_url, embeddable_content_types, embedded_in_posts, #events_for_select, #faq_word_count, faqs, #file_name, for_item_sku, for_product_category_url_without_order, for_product_line_url, for_product_line_url_with_ancestors, for_product_line_url_with_descendants, for_product_line_url_without_order, for_product_line_urls_with_descendants, for_sales_portal, for_support_portal, #full_localized_solution, #generate_pdf, #get_img_src_urls, has_comments, has_schema, has_video, #items, #latest_revision, #link_checks, #localize_price, #localized_solution, #localized_solution_html_doc, most_recent_first, no_schema, #normalize_friendly_id, not_for_public, #ok_to_delete?, options_for_select, orphan_faqs, over_word_limit, #page_title, #possible_events, #possible_events_for_select, press_industry_reports, press_releases, #primary_site_map, #procedure?, procedure_types, #process_product_embeds, #product_categories, #product_lines, publish_posts_due_for_publishing, published, ransackable_scopes, #reading_time_in_minutes, reading_time_options, #related_articles_for_display, #related_articles_for_select, #restore_revision!, #sanitize_urls, #schema_dot_org_images, #schema_dot_org_structure, #schema_dot_org_structure_faq, schema_types_in, #send_creation_email, #send_update_email, #seo_clicks, #seo_keywords_count, #seo_last_synced_at, #seo_metrics_synced?, #seo_top_keyword, #seo_top_position, #seo_traffic, #seo_traffic_value, #should_generate_new_friendly_id?, #slug_candidates, slug_find, #snapshot_revision!, #solution_text, sorted_by_most_recent_first, sorted_with_booster, states_for_select, #subject_with_type, #to_json_ld, #to_json_raw, #topics, types_options, types_options_for_search, #uploads, #uses_revisions?, #visit_count_30d, waiting_to_publish, with_votes
Methods included from Models::CrossLinkable
#content_links_count, #linked_content, #linked_posts, #linked_publications, #linked_showcases, #linked_videos
Methods included from Models::Taggable
#add_tag, all_tags, #has_tag?, normalize_tag_names, not_tagged_with, #remove_tag, #tag_list, #tag_list=, #taggable_type_for_tagging, tagged_with, #tags, #tags=, tags_cloud, tags_exclude, tags_include, with_all_tags, with_any_tags, without_all_tags, without_any_tags
Methods included from Models::Embeddable
embeddable_content_types, #embedding_content_hash, embedding_partition_class, #embedding_stale?, #embedding_vector, #find_content_embedding, #find_similar, #generate_all_embeddings!, #generate_chunked_embeddings!, #generate_embedding!, #has_embedding?, #needs_chunking?, regenerate_all_embeddings, semantic_search
Methods included from Models::SchemaMarkup
#add_schema, #clear_schema_markup, #consolidated_faq_page_schema, #content_has_embedded_faq_schema?, #embedded_faq_ids_from_content, #has_schema_type?, #render_schema_markup, #schema_count, #schema_types, #schemas_by_type
Methods included from Models::Auditable
#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record
Methods included from Models::Lineage
#ancestors, #ancestors_ids, #children_and_roots, #descendants, #descendants_ids, #ensure_non_recursive_lineage, #family_members, #generate_full_name, #generate_full_name_array, #lineage, #lineage_array, #lineage_simple, #root, #root_id, #self_ancestors_and_descendants, #self_ancestors_and_descendants_ids, #self_and_ancestors, #self_and_ancestors_ids, #self_and_children, #self_and_descendants, #self_and_descendants_ids, #self_and_siblings, #self_and_siblings_ids, #siblings, #siblings_ids
Methods inherited from ApplicationRecord
ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation
Methods included from Models::EventPublishable
Instance Attribute Details
#auto_update_revised_at ⇒ Object
Returns the value of attribute auto_update_revised_at.
103 104 105 |
# File 'app/models/post.rb', line 103 def auto_update_revised_at @auto_update_revised_at end |
#revision_change_notes ⇒ Object
Returns the value of attribute revision_change_notes.
103 104 105 |
# File 'app/models/post.rb', line 103 def revision_change_notes @revision_change_notes end |
Class Method Details
.breadcrumbs_select_option ⇒ Object
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'app/models/post.rb', line 186 def self. list = ['/floor-heating', '/floor-heating/tile-marble-or-stone', '/floor-heating/carpet', '/floor-heating/concrete', '/floor-heating/underlayment', '/snow-melting', '/snow-melting/heated-driveway', '/snow-melting/accessibility-ramp-deicing', '/snow-melting/asphalt-design-guide', '/snow-melting/outdoor-stairs', '/snow-melting/heated-concrete-patio', '/roof-and-gutter-deicing', '/pipe-freeze-protection', '/towel-warmer', '/radiant-heat-panels', '/led-mirror', '/mirror-defogger', '/countertop-heater'] list += Post.distinct.pluck('unnest(breadcrumbs)'.sql_safe) list.uniq.compact.sort end |
.by_date_range ⇒ ActiveRecord::Relation<Post>
A relation of Posts that are by date range. Active Record Scope
111 |
# File 'app/models/post.rb', line 111 scope :by_date_range, ->(date_start, date_end) { where(Post[:published_at].gteq(date_start)).where(Post[:published_at].lteq(date_end)) } |
.category_tags_for_select ⇒ Object
163 164 165 166 167 168 169 170 171 172 173 174 |
# File 'app/models/post.rb', line 163 def self. # Get all tags from published posts that are in the allowed POST_TAGS list # Use explicit join with taggable_type = 'Post' to handle STI properly = Post.published .joins("INNER JOIN taggings ON taggings.taggable_id = articles.id AND taggings.taggable_type = 'Post'") .joins('INNER JOIN tags ON tags.id = taggings.tag_id') .where('LOWER(tags.name) IN (?)', POST_TAGS.map(&:downcase)) .distinct .pluck('tags.name') = .map(&:downcase).uniq & POST_TAGS .sort.map { |tag| [tag_title(tag), tag] } end |
.purge_all_posts_by_tag ⇒ Object
Class method to purge all posts using tag-based purging
This is more efficient than URL-based purging when you want to purge all posts
since it makes a single API call to Cloudflare instead of multiple calls for each URL
212 213 214 |
# File 'app/models/post.rb', line 212 def self.purge_all_posts_by_tag EdgeCacheWorker.perform_async('tags' => ['post']) end |
.tag_title(tag) ⇒ Object
182 183 184 |
# File 'app/models/post.rb', line 182 def self.tag_title(tag) tag == 'led-mirrors' ? 'LED Mirrors' : tag.titleize end |
.with_comments_count ⇒ ActiveRecord::Relation<Post>
A relation of Posts that are with comments count. Active Record Scope
112 113 114 |
# File 'app/models/post.rb', line 112 scope :with_comments_count, -> { all.select_append("(#{COMMENT_COUNT_SUBQUERY}) as comments_count, (#{COMMENT_COUNT_SUBQUERY_LAST7D}) as comments_count_last7d") } |
Instance Method Details
#author_bio ⇒ Object
159 160 161 |
# File 'app/models/post.rb', line 159 def &.employee_record&.&.presence end |
#content_for_embedding(_content_type = :primary, locale: 'en-US') ⇒ Object
Override Article's content_for_embedding for blog post specifics.
Content is rendered through Liquid (resolving image %, faq %, video %,
conditionals like if canada %, etc.) before being stripped to plain text.
This ensures the embedding reflects what visitors actually read, including
locale-specific copy.
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
# File 'app/models/post.rb', line 231 def (_content_type = :primary, locale: 'en-US') locale_str = locale.to_s canada = %w[en-CA fr-CA].include?(locale_str) usa = locale_str == 'en-US' parts = [] parts << 'Type: Blog Post' parts << "Title: #{subject}" if subject.present? parts << "Subtitle: #{title}" if title.present? # Render description through Liquid so any locale-conditional copy is resolved if description.present? rendered_desc = Liquid::ParseEnvironment.parse(description).render( 'locale' => locale_str, 'canada' => canada, 'usa' => usa ) parts << parse_text_from_html(rendered_desc) end # Use localized_solution (already in Article) which runs Liquid rendering, # price localization, and product-embed resolution for the given locale. # This is the same path the frontend uses, so the embedding matches what # visitors actually read. rendered_body = localized_solution(locale_str) parts << parse_text_from_html(rendered_body) if rendered_body.present? # Product line hierarchy for SEO/topic context if product_lines.any? pl_names = product_lines.map(&:lineage_expanded).join(', ') parts << "Topics: #{pl_names}" end # Tags for blog categorisation parts << "Categories: #{.join(', ')}" if .present? # Author attribution parts << "Author: #{&.full_name}" if &.full_name.present? # Related articles — rendered as a "Related posts" widget at the bottom of the page. # Including their titles helps the embedding understand the semantic neighbourhood # of this post, which in turn improves cross-link recommendations. = if .any? parts << "Related articles: #{.map(&:subject).compact.join(', ')}" end parts.compact.join("\n\n") end |
#deep_dup ⇒ Object
126 127 128 129 130 131 132 133 134 |
# File 'app/models/post.rb', line 126 def deep_dup deep_clone(except: %i[published_at created_at updated_at slug_custom]) do |original, copy| if copy.is_a?(Post) copy.title = "Copy of #{original.title}" copy.state = 'draft' copy.subject = "Copy of #{original.subject}" end end end |
#embeddable_locales ⇒ Object
Generate per-locale embeddings so locale-conditional Liquid content
(Canada vs. USA copy, localised pricing) is encoded separately.
282 283 284 |
# File 'app/models/post.rb', line 282 def %w[en-US en-CA] end |
#embedding_type_name ⇒ Object
302 303 304 |
# File 'app/models/post.rb', line 302 def 'Post' end |
#legacy_source ⇒ Source
108 |
# File 'app/models/post.rb', line 108 has_one :legacy_source, primary_key: :subject, foreign_key: :name, class_name: 'Source', inverse_of: :post, dependent: :nullify |
#liquid_variable_context(_locale, _preview_mode) ⇒ Object
Expose a PostDrop as the 'post' variable in Liquid templates rendered
inside this post's solution/description fields.
Allows blog content to reference post.subject }, post.tags }, etc.
139 140 141 |
# File 'app/models/post.rb', line 139 def liquid_variable_context(_locale, _preview_mode) { 'post' => Liquid::PostDrop.new(self) } end |
#locale_for_embedding ⇒ Object
286 287 288 |
# File 'app/models/post.rb', line 286 def 'en-US' end |
#main_public_tag ⇒ Object
176 177 178 179 180 |
# File 'app/models/post.rb', line 176 def main_public_tag return if .blank? .detect { |t| t.in?(POST_TAGS) } end |
#next_post ⇒ Object
147 148 149 |
# File 'app/models/post.rb', line 147 def next_post self.class.published.where(Post[:published_at].gt(published_at)).order(:published_at).first end |
#post_comments ⇒ ActiveRecord::Relation<PostComment>
109 |
# File 'app/models/post.rb', line 109 has_many :post_comments, -> { order(:created_at) }, dependent: :destroy, inverse_of: :post |
#preview_image ⇒ Image
105 |
# File 'app/models/post.rb', line 105 belongs_to :preview_image, class_name: 'Image', optional: true |
#previous_post ⇒ Object
143 144 145 |
# File 'app/models/post.rb', line 143 def previous_post self.class.published.where(Post[:published_at].lt(published_at)).order(:published_at).reverse_order.first end |
#primary_tag ⇒ Object
151 152 153 |
# File 'app/models/post.rb', line 151 def primary_tag &.first end |
#purge_edge_cache(include_indirect_associations: true, extra_urls: [], prepare_sitemap: true) ⇒ Object
Public: invoked from controllers to purge cache
291 292 293 294 295 296 297 298 299 300 |
# File 'app/models/post.rb', line 291 def purge_edge_cache(include_indirect_associations: true, extra_urls: [], prepare_sitemap: true) # Ensures the sitemap is populated with this post insert_in_sitemap if prepare_sitemap # Blog post will also reset the post index extra_urls ||= [] extra_urls += SiteMap.where("path LIKE '%/posts'").map(&:url) # And all the tag pages extra_urls += .flat_map { |tag| SiteMap.where(SiteMap[:path].matches("%/posts/#{tag}/tag")).map(&:url) } super(include_indirect_associations:, extra_urls:) end |
#to_s ⇒ Object
216 217 218 |
# File 'app/models/post.rb', line 216 def to_s subject end |
#user_name(_skip_link = false) ⇒ Object
155 156 157 |
# File 'app/models/post.rb', line 155 def user_name(_skip_link = false) &.full_name || 'N/A' end |