Class: GeneratedPdfsController

Inherits:
CrmController show all
Defined in:
app/controllers/generated_pdfs_controller.rb

Overview

PDF Studio — generate and iterate PDFs, then import them into the publication
library. The PDF analogue of GeneratedImagesController (the Image Variation
pattern). Access is limited to marketing reps and managers (admins inherit).

Constant Summary

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

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

#device_detector, #is_ie?

Methods included from Controllers::SubdomainDetection

#is_crm_request?, #is_www_request?, #json_request?

Methods included from Controllers::TurboSafeRedirect

#redirect_to

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

#destroyObject

DELETE /pdf_studio/:id — discard.



114
115
116
117
118
119
120
# File 'app/controllers/generated_pdfs_controller.rb', line 114

def destroy
  return unless guard_pending

  @generated_pdf.update!(status: 'rejected')
  flash[:info] = 'PDF discarded.'
  redirect_to generated_pdfs_path
end

#indexObject

GET /pdf_studio



11
12
13
14
15
16
17
18
19
20
21
22
23
# File 'app/controllers/generated_pdfs_controller.rb', line 11

def index
  @status = params[:status].presence_in(GeneratedPdf::STATUSES) || 'pending' # rubocop:disable Rails/StrongParametersExpect
  @pagy, @generated_pdfs = pagy(
    GeneratedPdf.includes({ created_by: :party }, :source_publication).where(status: @status).order(created_at: :desc),
    limit: params[:per_page]
  )
  @stats = {
    total:    GeneratedPdf.count,
    pending:  GeneratedPdf.pending.count,
    imported: GeneratedPdf.imported.count,
    rejected: GeneratedPdf.rejected.count
  }
end

#new_sessionObject

POST /pdf_studio/new_session — start a conversational PDF session with Sunny
(the studio's primary entry point: describe the PDF in plain language and
Sunny generates/edits it, staging the result back here for review/import).



34
35
36
37
38
39
40
41
42
# File 'app/controllers/generated_pdfs_controller.rb', line 34

def new_session
  result = Pdf::StudioSession.start(user: current_user)
  if result.success?
    redirect_to assistant_path(result.conversation)
  else
    flash[:error] = "Could not start a PDF session: #{result.error}"
    redirect_to generated_pdfs_path
  end
end

#rawObject

GET /pdf_studio/:id/raw — stream the staged PDF inline for the embedded viewer.



45
46
47
48
49
50
51
# File 'app/controllers/generated_pdfs_controller.rb', line 45

def raw
  bytes = @generated_pdf.pdf_bytes
  head(:not_found) and return if bytes.blank?

  send_data bytes, type: 'application/pdf', disposition: 'inline',
                   filename: @generated_pdf.file_name.presence || 'document.pdf'
end

#showObject

GET /pdf_studio/:id



26
27
28
29
# File 'app/controllers/generated_pdfs_controller.rb', line 26

def show
  @source_publication = @generated_pdf.source_publication
  @source_conversation = @generated_pdf.source_record if @generated_pdf.source_record.is_a?(AssistantConversation)
end

#source_rawObject

GET /pdf_studio/:id/source_raw — stream the SOURCE publication's current PDF
inline: the left pane of the side-by-side review for translated staged PDFs.



55
56
57
58
59
60
61
# File 'app/controllers/generated_pdfs_controller.rb', line 55

def source_raw
  literature = @generated_pdf.source_publication&.literature
  head(:not_found) and return unless literature&.attachment_stored?

  send_data literature.attachment.data, type: 'application/pdf', disposition: 'inline',
                                        filename: literature.attachment_name.presence || 'source.pdf'
end

#typst_previewObject

POST /pdf_studio/:id/typst_preview — compile edited Typst source WITHOUT
persisting anything (the side-by-side editor's live preview). Returns the
PDF bytes, or 422 + typst diagnostics for the editor's error pane.



66
67
68
69
70
71
72
# File 'app/controllers/generated_pdfs_controller.rb', line 66

def typst_preview
  source = params.require(:source)
  result = Pdf::Typst.compile(source)
  send_data result.bytes, type: 'application/pdf', disposition: 'inline', filename: 'preview.pdf'
rescue Pdf::Typst::Error => e
  render json: { error: e.message }, status: :unprocessable_content
end

#typst_updateObject

POST /pdf_studio/:id/typst_update — compile edited Typst source AND make it
the new staged PDF: replace the staged bytes, update the master source in
the layout, and refresh the page count. This is what the reviewer imports.



77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'app/controllers/generated_pdfs_controller.rb', line 77

def typst_update
  return unless guard_pending

  source = params.require(:source)
  result = Pdf::Typst.compile(source)
  @generated_pdf.assign_pdf(result.bytes, filename: @generated_pdf.file_name.presence || 'document.pdf')
  @generated_pdf.layout = @generated_pdf.layout.to_h.merge('typst_source' => source)
  @generated_pdf.page_count = result.meta[:pages]
  @generated_pdf.save!
  render json: { success: true, pages: @generated_pdf.page_count }
rescue Pdf::Typst::Error => e
  render json: { error: e.message }, status: :unprocessable_content
end

#updateObject

PATCH /pdf_studio/:id — import into the publication library.



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'app/controllers/generated_pdfs_controller.rb', line 92

def update
  return unless guard_pending

  result = GeneratedPdfImporter.call(
    @generated_pdf,
    mode:      params[:mode].presence,
    base_name: params[:base_name].presence || @generated_pdf.title,
    sku:       params[:sku].presence,
    locales:   Array(params[:locales]).compact_blank.presence,
    notes:     params[:notes].presence
  )

  if result.success?
    flash[:success] = result.notice
    redirect_to generated_pdfs_path(status: 'imported')
  else
    flash[:error] = result.error
    redirect_to generated_pdf_path(@generated_pdf)
  end
end