Class: AudiencesController

Inherits:
CrmController show all
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

Methods included from Controllers::Destroyable

#perform_destroy

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

#backfill_partiesObject

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

#createvoid

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_membervoid

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

#destroyvoid

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.full_messages.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.set_tags(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

#editvoid

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

#indexvoid

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

#newvoid

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_membersvoid

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_exportObject

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_membersvoid

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

#showvoid

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

#updatevoid

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