Class: ArticleTechnical
- Inherits:
-
Article
- Object
- ActiveRecord::Base
- ApplicationRecord
- Article
- ArticleTechnical
- Defined in:
- app/models/article_technical.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)
Defined Under Namespace
Classes: Consolidator, PromoteAttachment
Constant Summary collapse
- APPROVED_STATES =
States considered approved (can be replaced by a draft).
%w[published internal].freeze
- ACTIVE_REPLACEMENT_STATES =
States in which a replacement draft is considered active.
%w[draft pending_review].freeze
- CONSOLIDATION_SOURCE_CONTEXT =
Context string for consolidation source marker links.
'Technical Article consolidation source; archive when this draft is approved.'- CONSOLIDATED_SOURCE_CONTEXT =
Context string for consolidated source marker links.
'Consolidated source; superseded by this Technical Article.'- REVIEW_TRANSITION_EVENTS =
Mapping of review state transitions to the events that trigger them.
{ [nil, 'draft'] => nil, %w[draft pending_review] => 'submit_for_review', %w[pending_review draft] => 'request_changes', %w[pending_review published] => 'publish', %w[pending_review internal] => 'approve_internal', %w[published archived] => 'archive', %w[internal archived] => 'archive', %w[archived draft] => 'redraft', %w[scheduled draft] => 'redraft' }.freeze
Constants inherited from Article
Article::EMBEDDABLE_STATES, Article::FAQ_MAX_WORDS, Article::FAQ_TARGET_WORDS, Article::FAQ_WARN_WORDS, Article::FAQ_WORD_COUNT_SQL, Article::KNOWLEDGE_CORPUS_ROLES, Article::KNOWLEDGE_ROLES, Article::STI_TYPES, Article::UNREVIEWED_KNOWLEDGE_ROLE
Constants included from Models::Embeddable
Models::Embeddable::MAX_CONTENT_LENGTH
Constants included from Models::Auditable
Models::Auditable::ALWAYS_IGNORED
Constants included from Models::Schedulable
Models::Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary
Attributes inherited from Article
#preserve_revision_slug, #schema_markup, #serial_number_high_range, #serial_number_low_range, #subject
Class Method Summary collapse
-
.active_replacement_drafts_by_source_id(source_ids) ⇒ Hash{Integer => Array<ArticleTechnical>}
Batch-loads every active direct or consolidation successor draft for the requested approved sources.
Instance Method Summary collapse
-
#activate_reviewed_successor ⇒ Boolean
Called inside the state-machine transaction immediately before approval.
-
#active_consolidation_draft ⇒ ArticleTechnical?
Returns a draft that intends to consolidate this approved source.
- #active_replacement_draft ⇒ ArticleTechnical?
- #article_type_human ⇒ String
- #clear_state_transition_validation ⇒ void
-
#consolidation_source_links ⇒ ActiveRecord::Relation<ContentLink>
Returns the pending consolidation marker links owned by this draft.
-
#consolidation_sources ⇒ Array<ArticleTechnical>
Returns approved articles that this draft will supersede upon approval.
-
#create_replacement_draft!(author:) ⇒ ArticleTechnical
Creates one in-progress successor without changing the approved source.
-
#prepare_state_transition_validation(event) ⇒ String
Records the state-machine event so the validation can reject direct state assignment that bypasses the review workflow.
- #requires_editorial_review? ⇒ Boolean
-
#review_assignee ⇒ Employee?
+Article#user+ is the mutable current owner, while +original_author+ keeps immutable authorship.
-
#review_assignee_id ⇒ Integer?
Returns the effective review assignee without loading the association.
Methods inherited from Article
all_product_lines, #applicable_product_category_urls, #applicable_product_line_urls, archived, #article_pages, #author, available_schema_types, #breadcrumbs_hash, #build_revision_draft, #content_for_embedding, #create_revision_draft!, delete_content_embeddings_for, drafts, due_for_publishing, #effective_published_date, #effective_revised_date, #email_url, embeddable_content_types, #embedded_assets, embedded_in_posts, embedding_eligible, #embedding_eligible?, #embedding_type_name, #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, #item_product_lines, #items, knowledge_corpus, #knowledge_corpus?, #knowledge_role_human, knowledge_roles_for_select, #latest_revision, #link_checks, #liquid_variable_context, #localize_price, #localized_solution, #localized_solution_html_doc, most_recent_first, no_schema, #normalize_friendly_id, not_for_public, #ok_to_delete?, #open_revision, options_for_select, #original_author, orphan_faqs, over_word_limit, #page_title, pending_review, #possible_events, #possible_events_for_select, #post_comments, #precursor, press_industry_reports, press_releases, #preview_image, #primary_site_map, #procedure?, procedure_types, #process_product_embeds, #product_categories, #product_lines, publish_posts_due_for_publishing, published, #purge_edge_cache, ransackable_scopes, #reading_time_in_minutes, reading_time_options, #related_articles_for_display, #related_articles_for_select, #revision_article_data, #revision_content_attributes, #revision_content_digest, #revision_content_link_data, #revision_page_attributes, #revision_proposable?, #revisions, #rma_reason_code, #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?, #site_maps, #slug_candidates, slug_find, #snapshot_revision!, #solution_text, sorted_by_most_recent_first, sorted_with_booster, states_for_select, #subject_with_type, #successor_article, #successors, #to_json_ld, #to_json_raw, #topics, types_options, types_options_for_search, #uploads, #user, #uses_revisions?, #visit_count_30d, #votes, waiting_to_publish, with_votes
Methods included from Models::EdgeCachePurgeable
#edge_cache_urls, #enqueue_edge_cache_purge, #purge_edge_cache
Methods included from Models::CrossLinkable
#case_evidence, #content_links_count, #inbound_content_links, #linked_articles, #linked_content, #linked_faqs, #linked_posts, #linked_publications, #linked_showcases, #linked_videos, #outbound_content_links
Methods included from Models::Taggable
#add_tag, all_tags, #has_tag?, normalize_tag_names, not_tagged_with, #remove_tag, #tag_list, #tag_list=, #tag_records, #taggable_type_for_tagging, tagged_with, #taggings, #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
#content_embeddings, #content_for_embedding, embeddable_content_types, #embeddable_locales, #embedding_content_hash, #embedding_current?, #embedding_eligible?, embedding_partition_class, #embedding_stale?, #embedding_type_name, #embedding_vector, #find_content_embedding, #find_similar, #generate_all_embeddings!, #generate_chunked_embeddings!, #generate_embedding!, #has_embedding?, #locale_for_embedding, #needs_chunking?, regenerate_all_embeddings, semantic_search, with_shape_lock
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, #creator, #should_not_save_version, #stamp_record, #updater
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::Schedulable
Methods included from Models::AfterCommittable
Methods included from Models::EventPublishable
Class Method Details
.active_replacement_drafts_by_source_id(source_ids) ⇒ Hash{Integer => Array<ArticleTechnical>}
Batch-loads every active direct or consolidation successor draft for the
requested approved sources. Direct replacements sort before consolidation
drafts, preserving +active_replacement_draft+'s existing preference.
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'app/models/article_technical.rb', line 127 def self.active_replacement_drafts_by_source_id(source_ids) ids = Array(source_ids).map(&:to_i).select(&:positive?).uniq return {} if ids.empty? marker_rows = ContentLink .where( target_article_id: ids, link_type: ContentLink::TECHNICAL_ARTICLE_WORKFLOW_LINK_TYPE, context: CONSOLIDATION_SOURCE_CONTEXT ) .order(:target_article_id, :source_article_id) .pluck(:target_article_id, :source_article_id) consolidation_draft_ids = marker_rows.pluck(1).uniq candidate_predicate = arel_table[:precursor_id].in(ids) candidate_predicate = candidate_predicate.or(arel_table[:id].in(consolidation_draft_ids)) if consolidation_draft_ids.any? active_drafts = where(state: ACTIVE_REPLACEMENT_STATES) .where(candidate_predicate) .order(:id) .to_a drafts_by_id = active_drafts.index_by(&:id) grouped = Hash.new { |hash, source_id| hash[source_id] = [] } active_drafts.each do |draft| grouped[draft.precursor_id] << draft if draft.precursor_id.in?(ids) end marker_rows.each do |source_id, draft_id| draft = drafts_by_id[draft_id] grouped[source_id] << draft if draft && grouped[source_id].exclude?(draft) end grouped end |
Instance Method Details
#activate_reviewed_successor ⇒ Boolean
Called inside the state-machine transaction immediately before approval.
The predecessor stays live throughout review, then is archived and pointed
at this successor atomically with the approval transition.
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 |
# File 'app/models/article_technical.rb', line 295 def activate_reviewed_successor return activate_consolidation_sources unless precursor precursor.with_lock do if precursor.successor_article_id == id return true elsif precursor.successor_article_id.present? errors.add(:precursor, 'already has an approved successor') return false elsif !precursor.is_a?(ArticleTechnical) || !precursor.state.in?(APPROVED_STATES) errors.add(:precursor, 'must be an approved Technical Article') return false end precursor.archive! precursor.update!(successor_article: self) end true end |
#active_consolidation_draft ⇒ ArticleTechnical?
Returns a draft that intends to consolidate this approved source.
207 208 209 210 211 212 |
# File 'app/models/article_technical.rb', line 207 def active_consolidation_draft self.class .active_replacement_drafts_by_source_id([id]) .fetch(id, []) .find { |draft| draft.precursor_id != id } end |
#active_replacement_draft ⇒ ArticleTechnical?
200 201 202 |
# File 'app/models/article_technical.rb', line 200 def active_replacement_draft self.class.active_replacement_drafts_by_source_id([id]).fetch(id, []).first end |
#article_type_human ⇒ String
160 161 162 |
# File 'app/models/article_technical.rb', line 160 def article_type_human 'Technical' end |
#clear_state_transition_validation ⇒ void
This method returns an undefined value.
195 196 197 |
# File 'app/models/article_technical.rb', line 195 def clear_state_transition_validation @review_transition_event = nil end |
#consolidation_source_links ⇒ ActiveRecord::Relation<ContentLink>
Returns the pending consolidation marker links owned by this draft.
217 218 219 220 221 222 |
# File 'app/models/article_technical.rb', line 217 def consolidation_source_links outbound_content_links.where( link_type: ContentLink::TECHNICAL_ARTICLE_WORKFLOW_LINK_TYPE, context: CONSOLIDATION_SOURCE_CONTEXT ) end |
#consolidation_sources ⇒ Array<ArticleTechnical>
Returns approved articles that this draft will supersede upon approval.
227 228 229 230 231 |
# File 'app/models/article_technical.rb', line 227 def consolidation_sources consolidation_source_links.with_targets.filter_map do |content_link| content_link.target if content_link.target.is_a?(ArticleTechnical) end end |
#create_replacement_draft!(author:) ⇒ ArticleTechnical
Creates one in-progress successor without changing the approved source.
Repeated requests return the existing draft so the review workflow cannot
accidentally fork parallel replacements for the same live article.
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 279 280 281 282 283 284 285 286 287 288 289 |
# File 'app/models/article_technical.rb', line 240 def create_replacement_draft!(author:) with_lock do unless state.in?(APPROVED_STATES) errors.add(:state, 'must be published or internal before creating a replacement') raise ActiveRecord::RecordInvalid, self end existing = active_replacement_draft return existing if existing replacement = dup replacement.assign_attributes( state: 'draft', published_at: nil, publish_on: nil, archived_at: nil, revised_at: nil, slug: nil, slug_custom: nil, precursor: self, successor_article_id: nil, brafton_id: nil, updater_id: nil, original_author: , user: , creator_id: &.id, version: version.to_i + 1 ) replacement.product_lines = product_lines.to_a replacement.product_categories = product_categories.to_a replacement.items = items.to_a replacement.uploads = uploads.to_a replacement. = replacement.save! outbound_content_links.ordered.with_targets.each do |content_link| next if content_link.workflow_managed? replacement.outbound_content_links.create!( target: content_link.target, link_type: content_link.link_type, context: content_link.context, created_by_type: content_link.created_by_type, position: content_link.position ) end replacement end end |
#prepare_state_transition_validation(event) ⇒ String
Records the state-machine event so the validation can reject direct state
assignment that bypasses the review workflow.
190 191 192 |
# File 'app/models/article_technical.rb', line 190 def prepare_state_transition_validation(event) @review_transition_event = event.to_s end |
#requires_editorial_review? ⇒ Boolean
165 166 167 |
# File 'app/models/article_technical.rb', line 165 def requires_editorial_review? true end |
#review_assignee ⇒ Employee?
+Article#user+ is the mutable current owner, while +original_author+ keeps
immutable authorship. Technical Articles use that existing owner as the
review assignee; older/unassigned records fall back to their author.
174 175 176 |
# File 'app/models/article_technical.rb', line 174 def review_assignee user || end |
#review_assignee_id ⇒ Integer?
Returns the effective review assignee without loading the association.
181 182 183 |
# File 'app/models/article_technical.rb', line 181 def review_assignee_id user_id || end |