Class: AudiencesController
- Inherits:
-
CrmController
- Object
- ActionController::Base
- ApplicationController
- CrmController
- AudiencesController
- Includes:
- Controllers::Destroyable
- Defined in:
- app/controllers/audiences_controller.rb
Overview
== Schema Information
Table name: audiences
id :integer not null, primary key
name :string
creator_id :integer
updater_id :integer
created_at :datetime not null
updated_at :datetime not null
customer_search_params :text
list_type :string
add_all_emails :boolean
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
-
#backfill_parties ⇒ Object
Kick off the on-demand contact/customer-by-email backfill for a static email list.
-
#create ⇒ void
POST /audiences — creates an audience.
-
#delete_audience_member ⇒ void
DELETE /audiences/:id/delete_audience_member — removes (or archives, if it has delivery history) a single member from the audience.
-
#destroy ⇒ void
DELETE /audiences/:id — destroys an audience.
-
#edit ⇒ void
GET /audiences/:id/edit — edit form for an existing audience.
-
#index ⇒ void
GET /audiences — filterable, paginated audience list.
-
#new ⇒ void
GET /audiences/new — blank audience form.
-
#new_audience_members ⇒ void
GET /audiences/:id/new_audience_members — form for adding members to an existing static audience.
-
#openai_export ⇒ Object
Kick off the ChatGPT (OpenAI) Ads CSV export.
-
#save_new_audience_members ⇒ void
PATCH /audiences/:id/save_new_audience_members — saves the members added via #new_audience_members.
-
#show ⇒ void
GET /audiences/:id — a single audience's filterable, paginated member list.
-
#update ⇒ void
PATCH/PUT /audiences/:id — updates an audience.
Methods included from Controllers::Destroyable
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
#backfill_parties ⇒ Object
Kick off the on-demand contact/customer-by-email backfill for a static email
list. Enqueues a Sidekiq::Status worker and routes the user to its live job
page (the global Jobs tracker also picks it up); falls back to the list on a
uniqueness-lock conflict.
193 194 195 196 197 198 199 200 201 202 203 |
# File 'app/controllers/audiences_controller.rb', line 193 def backfill_parties @audience = Audience.find(params[:id]) unless @audience.list_type == 'static' redirect_to(@audience, alert: 'Party backfill is available for static email lists only.') return end jid = AudiencePartyBackfillWorker.perform_async(@audience.id) redirect_to_job_or_fallback(jid, audience_path(@audience)) end |
#create ⇒ void
This method returns an undefined value.
POST /audiences — creates an audience.
118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'app/controllers/audiences_controller.rb', line 118 def create @audience = Audience.new(params[:audience]) begin if @audience.save flash[:info] = "Audience #{view_context.link_to(@audience.name, audience_path(@audience.id))}." redirect_to_return_path_or_default(audience_path(@audience)) else render action: 'new', status: :unprocessable_content end rescue ActiveRecord::RecordNotUnique => e flash.now[:error] = " !! Duplicate email address detected, #{e}" render action: 'new', status: :unprocessable_content end end |
#delete_audience_member ⇒ void
This method returns an undefined value.
DELETE /audiences/:id/delete_audience_member — removes (or archives, if it
has delivery history) a single member from the audience.
219 220 221 222 223 224 225 226 |
# File 'app/controllers/audiences_controller.rb', line 219 def delete_audience_member @audience = Audience.find(params[:id]) audience_member = @audience.audience_members.find(params[:audience_member_id]) audience_member.remove! # archives if it has delivery history, else deletes flash[:info] = audience_member.archived? ? 'AudienceMember archived (has delivery history)' : 'AudienceMember removed' redirect_to_return_path_or_default(audience_path(@audience)) end |
#destroy ⇒ void
This method returns an undefined value.
DELETE /audiences/:id — destroys an audience. Members with delivery
activity are archived rather than deleted (see Audience); on a
dependent-record failure, reports and shows a friendly error instead of
a raw 500.
157 158 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 184 185 186 187 |
# File 'app/controllers/audiences_controller.rb', line 157 def destroy @audience = Audience.find(params[:id]) # AudienceMembers with delivery activity are archived (kept) rather than deleted # when the list goes — surface that to the admin. archived_count = @audience.audience_members.with_campaign_activity.count if @audience.destroy flash[:info] = if archived_count.positive? "List deleted. #{archived_count} audience_member(s) with delivery history were archived for record-keeping." else 'List deleted.' end else flash[:error] = "This list couldn't be deleted: #{@audience.errors..to_sentence.presence || 'please try again or contact support.'}" end redirect_to_return_path_or_default(audiences_url) rescue ActiveRecord::InvalidForeignKey, ActiveRecord::RecordNotDestroyed => e # Audience#dispose_audience_members (a before_destroy) archives audience_members # with delivery history and hard-deletes the rest via AudienceMember#remove!, # which calls destroy! — so two failure modes can propagate out of this # (non-bang) list destroy and would otherwise be a raw 500 (AppSignal #5964): # • InvalidForeignKey — a audience_member/delivery still referenced by another row. # • RecordNotDestroyed — a no-activity audience_member that gains a sent/bounced # delivery between remove!'s check and the prevent_hard_delete_with_activity # invariant; destroy! turns that :abort into RecordNotDestroyed. # Report for visibility, but hand the admin a friendly message, not an error page. Appsignal.report_error(e) { |transaction| transaction.(audience_id: @audience&.id.to_s) } flash[:error] = 'This list could not be deleted because some of its audience_members are still referenced by other records. Please contact support if this keeps happening.' redirect_to_return_path_or_default(audiences_url) end |
#edit ⇒ void
This method returns an undefined value.
GET /audiences/:id/edit — edit form for an existing audience.
111 112 113 |
# File 'app/controllers/audiences_controller.rb', line 111 def edit @audience = Audience.find(params[:id]) end |
#index ⇒ void
This method returns an undefined value.
GET /audiences — filterable, paginated audience list.
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 |
# File 'app/controllers/audiences_controller.rb', line 23 def index scope = Audience.all # Containment lookups: which lists hold a audience_member with this email / # customer id. Scoped via subquery (not a join) so we don't duplicate # list rows. Only matches materialized audience_members — an `dynamic` # list whose audience hasn't been generated yet won't surface here. @email = params.dig(:filter, :email).presence @customer_id = params.dig(:filter, :customer_id).presence scope = scope.where(id: AudienceMember.where('email_address ilike ?', "%#{@email}%").select(:audience_id)) if @email.present? scope = scope.where(id: AudienceMember.where(customer_id: @customer_id).select(:audience_id)) if @customer_id.present? # Ad-platform filter: 'any' = synced to any platform, else a specific one. @ad_platform = params.dig(:filter, :ad_platform).presence if @ad_platform == 'any' scope = scope.ad_synced elsif @ad_platform.present? scope = scope.for_ad_platform(@ad_platform) end @q = scope.ransack(params[:q]) @q.sorts = 'updated_at desc' if @q.sorts.empty? # distinct because a campaign filter (campaigns_id_eq) joins the HABTM table. @pagy, @audiences = pagy(@q.result.distinct, limit: 25) @campaign_options = Campaign.order(:name).pluck(:name, :id) @list_type_options = Audience::LIST_TYPES.map { |t| [t.humanize, t] } @ad_platform_options = [['Any ad platform', 'any']] + Audience::AD_PLATFORMS.map { |p| [Audience::AD_PLATFORM_LABELS[p], p] } end |
#new ⇒ void
This method returns an undefined value.
GET /audiences/new — blank audience form.
77 78 79 |
# File 'app/controllers/audiences_controller.rb', line 77 def new @audience = Audience.new end |
#new_audience_members ⇒ void
This method returns an undefined value.
GET /audiences/:id/new_audience_members — form for adding members to an
existing static audience.
85 86 87 |
# File 'app/controllers/audiences_controller.rb', line 85 def new_audience_members @audience = Audience.find(params[:id]) end |
#openai_export ⇒ Object
Kick off the ChatGPT (OpenAI) Ads CSV export. OpenAI has no audience-ingest
API, so members are resolved + hashed off-request (~1 min for large lists).
The worker uploadifies the CSV and the Jobs flow serves the download — see
AudienceOpenaiExportWorker. Routes to the live job page.
209 210 211 212 213 |
# File 'app/controllers/audiences_controller.rb', line 209 def openai_export @audience = Audience.find(params[:id]) jid = AudienceOpenaiExportWorker.perform_async(@audience.id) redirect_to_job_or_fallback(jid, audience_path(@audience)) end |
#save_new_audience_members ⇒ void
This method returns an undefined value.
PATCH /audiences/:id/save_new_audience_members — saves the members added
via #new_audience_members.
93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
# File 'app/controllers/audiences_controller.rb', line 93 def save_new_audience_members @audience = Audience.find(params[:id]) begin if @audience.update(params[:audience]) redirect_to @audience, notice: 'Audience was successfully updated.' else render action: 'new_audience_members', status: :unprocessable_content end rescue ActiveRecord::RecordNotUnique => e flash.now[:error] = " !! Duplicate email address detected, #{e}" render action: 'new_audience_members', status: :unprocessable_content end end |
#show ⇒ void
This method returns an undefined value.
GET /audiences/:id — a single audience's filterable, paginated member list.
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'app/controllers/audiences_controller.rb', line 57 def show @audience = Audience.find(params[:id]) scope = @audience.audience_members .with_customer .select('audience_members.*, contacts.id as contact_id, contacts.full_name as contact_name, customers.id as customer_id, customers.full_name as customer_name') @email = params.dig(:audience_member, :email).presence @customer_id = params.dig(:audience_member, :customer_id).presence scope = scope.where('audience_members.email_address ilike ?', "%#{@email}%") if @email.present? scope = scope.where(audience_members: { customer_id: @customer_id }) if @customer_id.present? @q = scope.ransack(params[:q]) @q.sorts = 'created_at desc' if @q.sorts.empty? @pagy, @audience_members = pagy(@q.result, limit: 50) end |
#update ⇒ void
This method returns an undefined value.
PATCH/PUT /audiences/:id — updates an audience.
136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'app/controllers/audiences_controller.rb', line 136 def update @audience = Audience.find(params[:id]) begin if @audience.update(params[:audience]) redirect_to @audience, notice: 'Audience was successfully updated.' else render action: 'edit', status: :unprocessable_content end rescue ActiveRecord::RecordNotUnique => e flash.now[:error] = " !! Duplicate email address detected, #{e}" render action: 'edit', status: :unprocessable_content end end |