Class: Crm::PublicationsController
- Inherits:
-
CrmController
- Object
- ActionController::Base
- ApplicationController
- CrmController
- Crm::PublicationsController
- Includes:
- Controllers::RequireTurboFrame, Crm::Publications::PdfActions
- Defined in:
- app/controllers/crm/publications_controller.rb
Overview
Controller: publications.
Constant Summary collapse
- TAB_ACTIONS =
Tab fragments never render standalone — non-frame hits redirect to show ?tab=.
%i[tab_overview tab_attributes tab_description tab_ai_embedding tab_revisions tab_store_items tab_catalogs tab_specific_items].freeze
Constants included from Controllers::ReferenceFindable
Controllers::ReferenceFindable::ID_EMBEDDED_PATTERNS
Constants included from Controllers::AnalyticsEvents
Controllers::AnalyticsEvents::MAX_QUEUED_EVENTS, Controllers::AnalyticsEvents::SESSION_KEY
Constants included from Controllers::ErrorRendering
Controllers::ErrorRendering::NON_CONTENT_PATH_PREFIXES
Instance Method Summary collapse
-
#analyze_images ⇒ Object
Analyze the PDF: Vision AI image descriptions AND a docling text re-extraction (refreshing uploads.extracted_markdown → items.search_text → embedding via UploadMarkdownExtractionWorker), so this one button reruns the whole AI pipeline for the publication.
-
#create ⇒ Object
Creates a publication via Publication::Saver.
-
#destroy ⇒ Object
Destroys the publication.
-
#edit ⇒ Object
Renders the form to edit an existing publication.
-
#edit_stock ⇒ Object
Renders the form to adjust the publication's US/Canada stock counts.
-
#generate_cover_image ⇒ Object
Generates/refreshes the publication cover via the AI variation generator.
-
#index ⇒ Object
Lists publications, ransack-filtered and searched (keyword/AI/hybrid mode).
-
#new ⇒ Object
Renders the form for a new publication.
-
#purge_cache ⇒ Object
Manual counterpart to Publication::CachePurgeHandler: drops every edge-cached page that surfaces this publication, its own /publications/.pdf, and the ImageKit copy of the cover.
-
#refresh_embedding ⇒ Object
Refresh embedding for the publication.
-
#revise ⇒ Object
Clones the publication into a new revision for editing.
-
#show ⇒ Object
Shows a single publication.
-
#tab_ai_embedding ⇒ Object
In-pane tab: AI embedding status.
-
#tab_attributes ⇒ Object
In-pane tab: attributes.
-
#tab_catalogs ⇒ Object
In-pane tab: linked catalogs.
-
#tab_description ⇒ Object
In-pane tab: description.
-
#tab_overview ⇒ Object
Tab actions for turbo frame navigation.
-
#tab_revisions ⇒ Object
In-pane tab: revision history.
-
#tab_specific_items ⇒ Object
In-pane tab: items this publication is specifically attached to.
-
#tab_store_items ⇒ Object
In-pane tab: linked store items.
-
#update ⇒ Object
Updates a publication via Publication::Saver.
-
#update_stock ⇒ Object
Applies a manual inventory adjustment to bring US/Canada stock counts to the submitted values.
Methods included from Controllers::RequireTurboFrame
require_in_pane_tab_frames, require_turbo_frame
Methods inherited from CrmController
#access_denied, #context_id, #context_object, #crm_home_path, #current_ability, #default_url_options, #download_temp, #get_tempfile_path_for_download, #init_status_job_collector, #initialize_crm_lazy_chunks, #persist_enqueued_status_jobs, #record_not_found, #redirect_to_job_or_fallback, #render_edit_action, #set_context, #set_download_path, #stash_file_for_temp_download, #sync_admin_presence_cookie, #touch_employee_last_seen
Methods inherited from ApplicationController
#account_impersonated?, #add_to_flash, #after_sign_in_path_for, #bypass_forgery_protection?, #chat_enabled?, #cloudflare_cleared?, #default_catalog, #default_url_options, #enable_turbo_frames, #find_publication, #fix_invalid_accept_header, #init_js_utils, #is_globals_call?, #layout_by_resource, #locale_store, #redirect_to, #require_employee_for_crm, #set_base_host, #set_real_ip, #set_report_errors_for, #should_render_layout?, #skip_layout_for_turbo_frame?, #stamp_impersonation_context, #tab_frame_breakout_request?, #warmlyyours_canada_ip?, #warmlyyours_ip?, #y
Methods included from Controllers::ReturnPathHandling
#check_for_return_path, #redirect_to_return_path_or_default
Methods included from Controllers::AnalyticsEvents
#consume_queued_analytics_events, #registration_lead_type, #track_event
Methods included from Controllers::DeviceDetection
Methods included from Controllers::SubdomainDetection
#is_crm_request?, #is_www_request?, #json_request?
Methods included from Controllers::TurboSafeRedirect
Methods included from Controllers::TrackingDetection
#bot_request?, #gdpr_country?, #gdpr_country_data, #prevent_bots, #set_tracking_cookie, #track_visitor?
Methods included from Controllers::AcceleratedFileSending
#send_file_accelerated, #send_upload_accelerated
Methods included from Controllers::ErrorRendering
#excp_string, #mail_to_for_error_reporting, #render_400, #render_404, #render_406, #render_410, #render_500, #render_invalid_authenticity_token, #render_ip_spoof_error, #render_unpermitted_parameters, #safe_referer_or_fallback
Methods included from Controllers::TurnstileVerification
#load_turnstile_script_tag, #turnstile_lazy_widget, #turnstile_script_tag, #turnstile_widget, #validate_turnstile!
Methods included from Controllers::CloudflareCaching
edge_cached, #edge_cached_action?, #reset_cloudflare_cache, #set_cloudflare_cache, #skip_edge_cache!, #skip_session
Methods included from Controllers::Webpackable
#preload_webpack_fonts, #webpack_css_include, #webpack_css_url, #webpack_js_include, #wpd_is_running?
Methods included from Controllers::Localizable
#cloudflare_country_locale, #determine_request_locale, #geocoder_locale, #guest_user_locale_check, #locale_optional_www_auth_path?, #param_locale, #set_locale, #set_request_locale, #skip_localization?, #warmlyyours_ip_locale
Methods included from Controllers::Authenticable
#access_denied, #authenticate_account, #authenticate_account!, #authenticate_account_from_login_token!, #check_is_a_manager, #check_is_a_sales_manager, #check_is_an_admin, #check_is_an_employee, #check_party, #clear_mismatched_guest_user, #create_guest_user, #credentials?, #current_or_guest_user, #current_or_guest_user_id_read_only, #current_user, #devise_mapping, #fully_logged_in?, #generate_bot_id, #guest_user, #identifiable?, #init_current_user, #initialize_guest, #load_context_user, #logging_in, #resource, #resource_name, #restrict_access_for_non_employees, #scrubbed_request_path, #user_object, #warn_on_session_guest_id_leak
Methods included from UrlsHelper
#catalog_breadcrumb_links, #catalog_link, #catalog_link_for_product_line, #catalog_link_for_sku, #cms_link, #delocalized_path, #path_to_sales_product_sku, #path_to_sales_product_sku_for_product_line, #path_to_sales_product_sku_for_product_line_slug, #product_line_from_catalog_link, #protocol_neutral_url, #sanitize_external_url, #valid_external_url?
Instance Method Details
#analyze_images ⇒ Object
Analyze the PDF: Vision AI image descriptions AND a docling text
re-extraction (refreshing uploads.extracted_markdown → items.search_text →
embedding via UploadMarkdownExtractionWorker), so this one button reruns
the whole AI pipeline for the publication.
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 |
# File 'app/controllers/crm/publications_controller.rb', line 233 def analyze_images load_publication (:update, @publication) unless @publication.literature&. flash[:error] = 'No PDF attached to this publication' redirect_to_return_path_or_default publication_path(@publication, tab: 'ai_embedding') return end # Worker also redirects here after completion; use return_path when provided fallback_path = publication_path(@publication, tab: 'ai_embedding') redirect_path = @return_path || fallback_path jid = PublicationVisionWorker.perform_async(@publication.id, { force: true, redirect_to: redirect_path }) extraction_jid = UploadMarkdownExtractionWorker.perform_async(@publication.literature_id, { 'force' => true }) ErrorReporting.info('Analyze PDF queued', { publication_id: @publication.id, vision_jid: jid, extraction_jid: }) redirect_to_job_or_fallback(jid, redirect_path) end |
#create ⇒ Object
Creates a publication via Publication::Saver.
87 88 89 90 91 92 93 94 95 96 97 |
# File 'app/controllers/crm/publications_controller.rb', line 87 def create (:create, Item) res = Publication::Saver.call(publication_params: params[:publication], user: current_user) if res.saved? redirect_to publication_path(res.publication), notice: 'Publication was created' else @publication = res.publication set_image_ransack render :new, status: :unprocessable_content end end |
#destroy ⇒ Object
Destroys the publication.
127 128 129 130 131 132 133 |
# File 'app/controllers/crm/publications_controller.rb', line 127 def destroy load_publication (:destroy, @publication) @publication.destroy flash[:info] = "#{@publication.name} destroyed" redirect_to publications_path end |
#edit ⇒ Object
Renders the form to edit an existing publication.
80 81 82 83 84 |
# File 'app/controllers/crm/publications_controller.rb', line 80 def edit load_publication set_image_ransack (:update, @publication) end |
#edit_stock ⇒ Object
Renders the form to adjust the publication's US/Canada stock counts.
136 137 138 139 140 |
# File 'app/controllers/crm/publications_controller.rb', line 136 def edit_stock load_publication (:update, @publication) load_stock end |
#generate_cover_image ⇒ Object
Generates/refreshes the publication cover via the AI variation generator.
Ensures a base cover exists (extracts the PDF's first page when missing),
then hands off to the image generator pre-set to US Letter ratio with that
cover as a reference image. Accepting the variation sets it as the new cover
(see GeneratedImageImporter's Item callback) — nothing is applied until the
user reviews and imports.
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'app/controllers/crm/publications_controller.rb', line 166 def generate_cover_image load_publication (:update, @publication) cover = @publication.primary_image || @publication.create_primary_image_from_pdf if cover redirect_to new_generated_image_path( source_image_id: cover.id, aspect_ratio: 'doc:17:22', source_record_type: 'Item', source_record_id: @publication.id, prompt: publication_cover_prompt(@publication) ) else flash[:error] = 'No cover image, and none could be generated from the PDF.' redirect_to publication_path(@publication) end end |
#index ⇒ Object
Lists publications, ransack-filtered and searched (keyword/AI/hybrid mode).
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'app/controllers/crm/publications_controller.rb', line 14 def index (:read, Item) # Unified search: search_mode determines how search_query is processed # - keyword: Traditional Ransack keyword_search (default) # - ai: Semantic search using text embeddings from PDF content # - hybrid: Both AI semantic + keyword search combined @search_mode = params[:search_mode].presence || 'keyword' @search_query = params[:search_query].to_s.strip.presence # Route search query to appropriate Ransack scope based on mode params[:q] ||= {} case @search_mode when 'ai' params[:q][:ai_search_publications] = @search_query if @search_query.present? when 'hybrid' params[:q][:hybrid_search_publications] = @search_query if @search_query.present? when 'keyword' params[:q][:keywords_search] = @search_query if @search_query.present? end # Set default for discontinued filter params[:q][:is_discontinued_true] = false if params[:q][:is_discontinued_true].blank? @q = Item.publications .joins(:literature) .includes(:literature, :primary_image, :product_category) .ransack(params[:q]) # AI/hybrid modes preserve similarity order from ai_search_publications scope # Keyword mode uses default created_at desc @q.sorts = 'created_at DESC' if @q.sorts.blank? && @search_mode == 'keyword' @pagy, @publications = pagy(@q.result, limit: 50) # Check if AI search had issues and warn the user if @search_mode.in?(%w[ai hybrid]) && Item.ai_search_warning? flash.now[:warning] = Item.last_ai_search_warning Item.clear_ai_search_warning! end # Precompute most recent revisions to avoid N+1 queries @most_recent_revisions = Publication::MostRecentRevisionLookup.new(@publications).lookup_hash # Precompute cover image URLs to avoid expensive URL generation in view @cover_image_urls = @publications.to_h do |pub| [pub.id, pub.primary_image&.image_url(format: 'jpeg', width: 200)] end end |
#new ⇒ Object
Renders the form for a new publication.
71 72 73 74 75 76 77 |
# File 'app/controllers/crm/publications_controller.rb', line 71 def new defaults = { product_category_id: ProductCategory.publications_root_id, cycle_count_grouping: 'not_set' } defaults.merge!(params[:publication]) if params[:publication].present? @publication = Item.new(defaults) set_image_ransack (:create, @publication) end |
#purge_cache ⇒ Object
Manual counterpart to Publication::CachePurgeHandler: drops every edge-cached
page that surfaces this publication, its own /publications/.pdf, and the
ImageKit copy of the cover. Needed when the automatic purge off
Events::PublicationUpdated didn't reach a page (or an edit landed through a
path that skips callbacks) — otherwise the stale copy sits there for its TTL.
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'app/controllers/crm/publications_controller.rb', line 189 def purge_cache load_publication (:update, @publication) begin urls = @publication.purge_publication_cache flash[:info] = if urls.is_a?(Array) "Purging #{urls.size} cached URL(s) and the cover image for #{@publication.sku}." else "Edge cache is disabled in #{Rails.env} — nothing to purge." end rescue StandardError => e # The page purge is already enqueued when this raises; only the cover # failed. Say so rather than 500 — the operator needs to know the cover is # still stale, which a generic error page would hide. flash[:error] = "Pages queued for purging, but the cover image was not purged: #{e.}" end redirect_to_return_path_or_default publication_path(@publication) end |
#refresh_embedding ⇒ Object
Refresh embedding for the publication
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'app/controllers/crm/publications_controller.rb', line 210 def load_publication (:update, @publication) begin if @publication.respond_to?(:needs_chunking?) && @publication.needs_chunking? @publication.(force: true) flash[:success] = "Generated #{@publication..unified_primary.count} embedding chunks for publication" else @publication.(:primary, force: true) flash[:success] = 'Embedding refreshed successfully' end rescue StandardError => e flash[:error] = "Failed to generate embedding: #{e.}" end redirect_to_return_path_or_default publication_path(@publication, tab: 'ai_embedding') end |
#revise ⇒ Object
Clones the publication into a new revision for editing.
113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'app/controllers/crm/publications_controller.rb', line 113 def revise (:create, Item) @original_publication = Item.publications.find(params[:id]) res = Publication::Reviser.new.process(@original_publication) if res.success? flash[:info] = 'Publication was revised, please make the necessary updates to the revision and save.' redirect_to edit_publication_path(res.new_publication) else flash[:error] = "Publication cannot be cloned due to the following errors. #{res.errors.to_sentence}" redirect_to publication_path(@original_publication) end end |
#show ⇒ Object
Shows a single publication.
65 66 67 68 |
# File 'app/controllers/crm/publications_controller.rb', line 65 def show load_publication (:read, @publication) end |
#tab_ai_embedding ⇒ Object
In-pane tab: AI embedding status.
278 279 280 281 282 |
# File 'app/controllers/crm/publications_controller.rb', line 278 def load_publication (:read, @publication) render partial: 'crm/publications/tab_ai_embedding', locals: { publication: @publication } end |
#tab_attributes ⇒ Object
In-pane tab: attributes.
264 265 266 267 268 |
# File 'app/controllers/crm/publications_controller.rb', line 264 def tab_attributes load_publication (:read, @publication) render partial: 'crm/publications/tab_attributes', locals: { publication: @publication } end |
#tab_catalogs ⇒ Object
In-pane tab: linked catalogs.
299 300 301 302 303 |
# File 'app/controllers/crm/publications_controller.rb', line 299 def tab_catalogs load_publication (:read, @publication) render partial: 'crm/publications/tab_catalogs', locals: { publication: @publication } end |
#tab_description ⇒ Object
In-pane tab: description.
271 272 273 274 275 |
# File 'app/controllers/crm/publications_controller.rb', line 271 def tab_description load_publication (:read, @publication) render partial: 'crm/publications/tab_description', locals: { publication: @publication } end |
#tab_overview ⇒ Object
Tab actions for turbo frame navigation
255 256 257 258 259 260 261 |
# File 'app/controllers/crm/publications_controller.rb', line 255 def tab_overview load_publication (:read, @publication) render partial: 'crm/publications/tab_overview', locals: { publication: @publication, traffic: Publication::CloudflareTrafficSummary.for(@publication) } end |
#tab_revisions ⇒ Object
In-pane tab: revision history.
285 286 287 288 289 |
# File 'app/controllers/crm/publications_controller.rb', line 285 def tab_revisions load_publication (:read, @publication) render partial: 'crm/publications/tab_revisions', locals: { publication: @publication } end |
#tab_specific_items ⇒ Object
In-pane tab: items this publication is specifically attached to.
306 307 308 309 310 |
# File 'app/controllers/crm/publications_controller.rb', line 306 def tab_specific_items load_publication (:read, @publication) render partial: 'crm/publications/tab_specific_items', locals: { publication: @publication } end |
#tab_store_items ⇒ Object
In-pane tab: linked store items.
292 293 294 295 296 |
# File 'app/controllers/crm/publications_controller.rb', line 292 def tab_store_items load_publication (:read, @publication) render partial: 'crm/publications/tab_store_items', locals: { publication: @publication } end |
#update ⇒ Object
Updates a publication via Publication::Saver.
100 101 102 103 104 105 106 107 108 109 110 |
# File 'app/controllers/crm/publications_controller.rb', line 100 def update load_publication (:update, @publication) res = Publication::Saver.call(publication: @publication, publication_params: params[:publication], user: current_user) if res.saved? redirect_to publication_path(res.publication), notice: 'Publication was updated' else set_image_ransack render :edit, status: :unprocessable_content end end |
#update_stock ⇒ Object
Applies a manual inventory adjustment to bring US/Canada stock counts to
the submitted values.
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'app/controllers/crm/publications_controller.rb', line 144 def update_stock load_publication (:update, @publication) load_stock if params[:usa_count].present? && @usa_stock && (params[:usa_count].to_i != @usa_stock) adjustment = -(@usa_stock - params[:usa_count].to_i) ItemLedgerEntry.inventory_adjustment(Store.find(Store::WARMLYYOURS_US_ID), { 0 => { store_item_id: @usa_store_item.id, qty: adjustment } }, Date.current, 'Adjust publication stock') end if params[:canada_count].present? && @canada_stock && (params[:canada_count].to_i != @canada_stock) adjustment = -(@canada_stock - params[:canada_count].to_i) ItemLedgerEntry.inventory_adjustment(Store.find(Store::WARMLYYOURS_CA_ID), { 0 => { store_item_id: @canada_store_item.id, qty: adjustment } }, Date.current, 'Adjust publication stock') end flash[:info] = 'Stock adjusted.' redirect_to publication_path(@publication) end |