Class: Crm::TagsController

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

Overview

CRUD, merge, autocomplete, and taggable-browsing actions for Tag
records.

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

#autocompletevoid

This method returns an undefined value.

GET /crm/tags/autocomplete
Remote tag lookup for the TomSelect tag inputs (see TagsInput) — SQL-filtered
and paginated so a form never embeds the full ~5k tag table. Embedding them
made every other TomSelect on the page pay a full-document forced reflow
(~500ms each, ~8s per page load).

id is the tag NAME, not the record id: Taggable#tags= takes names, and
TomSelect submits the value field.

Params:
q - substring filter (case-insensitive)
taggable_type - optional; restrict to tags applied to that type (e.g. 'Image').
Omitted means "all tags", which is what the generic
as: :tags inputs want — a tag is valid on any record even
if it has never been applied to this type before.
exclude - comma-separated tag names to omit (e.g. popular tags already
offered as checkboxes)
page/per_page - pagination



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'app/controllers/crm/tags_controller.rb', line 168

def autocomplete
  query = params[:q].to_s.strip
  page = [params[:page].to_i, 1].max
  per_page = (params[:per_page].presence || 20).to_i.clamp(1, 100)
  excluded = params[:exclude].to_s.split(',').map { |t| t.strip.downcase }.compact_blank

  scope = Tag.ordered
  scope = scope.by_taggable_type(params[:taggable_type]) if params[:taggable_type].present?
  scope = scope.search(query) if query.present?
  scope = scope.where.not(name: excluded) if excluded.any?

  names = scope.offset((page - 1) * per_page).limit(per_page + 1).pluck(:name)
  more = names.size > per_page
  results = names.first(per_page).map { |name| { id: name, text: name } }

  # Synthetic CMS page tags are derived from PagesController.page_ids and may not
  # exist in `tags` yet, so they can't come from the query above. Surface them on
  # the first page only, and only when unscoped — a taggable_type scope asks for
  # "tags actually applied to this type", which these are not.
  #
  # Only while searching: on a blank preload-on-focus they'd be ~145 entries of
  # noise sitting ahead of the real tags, and a page tag is only useful once you
  # are actually looking for a page.
  if query.present? && page == 1 && params[:taggable_type].blank?
    results = page_tag_results(query, excluded, names) + results
    # Page tags occupy slots on this page, so honour the advertised page size
    # rather than returning up to 2 * per_page, and report the overflow.
    more ||= results.size > per_page
    results = results.first(per_page)
  end

  render json: { results: results, pagination: { more: more } }
end

#batch_createvoid

This method returns an undefined value.

POST /crm/tags/batch_create
Creates multiple tags at once



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'app/controllers/crm/tags_controller.rb', line 206

def batch_create
  tag_names = params[:tag_names].to_s.split(/[,\n]/).map(&:strip).compact_blank.uniq
  created_count = 0
  existing_count = 0

  tag_names.each do |name|
    tag = Tag.find_or_create_by_name(name)
    if tag.previously_new_record?
      created_count += 1
    else
      existing_count += 1
    end
  end

  redirect_to tags_path, notice: "Created #{created_count} new tags. #{existing_count} already existed."
end

#cleanup_unusedvoid

This method returns an undefined value.

POST /crm/tags/cleanup_unused
Deletes all tags with no associated records



227
228
229
230
# File 'app/controllers/crm/tags_controller.rb', line 227

def cleanup_unused
  count = Tag.destroy_by(taggings_count: 0).size
  redirect_to tags_path, notice: "Deleted #{count} unused tags."
end

#createvoid

This method returns an undefined value.

Creates a tag.



81
82
83
84
85
86
87
88
89
# File 'app/controllers/crm/tags_controller.rb', line 81

def create
  @tag = Tag.new(tag_params)

  if @tag.save
    redirect_to tag_path(@tag), notice: 'Tag was successfully created.'
  else
    render :new, status: :unprocessable_content
  end
end

#destroyvoid

This method returns an undefined value.

Deletes the tag loaded by #set_tag, refusing when it still has
associated records.



106
107
108
109
110
111
112
113
# File 'app/controllers/crm/tags_controller.rb', line 106

def destroy
  if @tag.taggings_count.to_i.positive?
    redirect_to tag_path(@tag), alert: "Cannot delete tag with #{@tag.taggings_count} associated records. Merge it into another tag first."
  else
    @tag.destroy
    redirect_to tags_path, notice: 'Tag was successfully deleted.'
  end
end

#editvoid

This method returns an undefined value.

Loads the tag to edit via #set_tag.



76
# File 'app/controllers/crm/tags_controller.rb', line 76

def edit; end

#indexvoid

This method returns an undefined value.

Lists tags with usage stats, Ransack-searchable and filterable by
taggable type and public flag.



12
13
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
# File 'app/controllers/crm/tags_controller.rb', line 12

def index
  @q = Tag.ransack(params[:q])
  @q.sorts = 'taggings_count DESC' if @q.sorts.blank?
  @tags = @q.result

  # Apply search if provided
  @tags = @tags.search(params[:search]) if params[:search].present?

  # Filter by taggable type
  @tags = @tags.by_taggable_type(params[:taggable_type]) if params[:taggable_type].present?

  # Filter by public flag
  @tags = @tags.where(public: true) if params[:public] == '1'
  @tags = @tags.where(public: false) if params[:public] == '0'

  # Stats
  @total_tags = Tag.count
  @public_tags = Tag.public_tags.count
  @unused_tags = Tag.where(taggings_count: 0).count

  # Usage by type - count distinct tags per type (not taggings)
  @usage_by_type = Tagging.select(:taggable_type, :tag_id)
                          .distinct
                          .group(:taggable_type)
                          .count(:tag_id)

  respond_to do |format|
    format.html do
      @pagy, @tags = pagy(@tags, items: 50)
    end
    format.json do
      @tags = @tags.limit(50)
      render json: @tags.map { |t| { id: t.id, name: t.name, slug: t.slug } }
    end
    format.csv do
      send_data generate_csv(@tags), filename: "tags-#{Date.current}.csv"
    end
  end
end

#merge_intovoid

This method returns an undefined value.

POST /crm/tags/:id/merge_into
Merges the current tag into the target tag



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'app/controllers/crm/tags_controller.rb', line 119

def merge_into
  target_tag = Tag.find(params.expect(:target_tag_id))

  if target_tag == @tag
    redirect_to tag_path(@tag), alert: 'Cannot merge a tag into itself.'
    return
  end

  # Move all taggings to the target tag
  count = @tag.taggings.count
  old_name = @tag.name
  Tag.transaction do
    Tagging.connection.execute(<<~SQL.squish)
      INSERT INTO taggings
        (tag_id, taggable_id, taggable_type, tagger_id, tagger_type, context, created_at)
      SELECT
        #{target_tag.id}, taggable_id, taggable_type, tagger_id, tagger_type, context, created_at
      FROM taggings
      WHERE tag_id = #{@tag.id}
      ON CONFLICT (taggable_id, taggable_type, tag_id) DO NOTHING
    SQL
    @tag.taggings.delete_all
    Tag.reset_counters(target_tag.id, :taggings)
    @tag.destroy!
  end

  redirect_to tag_path(target_tag), notice: "Successfully merged '#{old_name}' into '#{target_tag.name}'. #{count} records updated."
end

#newvoid

This method returns an undefined value.

Builds a new tag.



69
70
71
# File 'app/controllers/crm/tags_controller.rb', line 69

def new
  @tag = Tag.new
end

#showvoid

This method returns an undefined value.

Shows the tag loaded by #set_tag, with recent taggings grouped by
type.



56
57
58
59
60
61
62
63
64
# File 'app/controllers/crm/tags_controller.rb', line 56

def show
  @usage_by_type = @tag.usage_by_type

  # Get recent taggings grouped by type
  @recent_taggings = {}
  @tag.taggings.includes(:taggable).group_by(&:taggable_type).each do |type, taggings|
    @recent_taggings[type] = taggings.first(10).filter_map(&:taggable)
  end
end

#taggablesvoid

This method returns an undefined value.

GET /crm/tags/:id/taggables
Returns paginated records of a specific taggable type for a tag



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'app/controllers/crm/tags_controller.rb', line 236

def taggables
  type = params[:type]
  unless type.present? && Tagging::TAGGABLE_TYPES.include?(type)
    head :bad_request
    return
  end

  taggings = @tag.taggings.where(taggable_type: type).includes(:taggable)
  @pagy, @taggings = pagy(taggings, items: 20, page: params[:page])
  @records = @taggings.filter_map(&:taggable)
  @type = type

  respond_to do |format|
    format.html { render partial: 'taggables_table', locals: { records: @records, type: @type, tag: @tag, pagy: @pagy } }
    format.json do
      render json: {
        records: @records.map { |r| taggable_to_json(r, type) },
        pagination: {
          current_page: @pagy.page,
          total_pages: @pagy.pages,
          total_count: @pagy.count,
          has_more: @pagy.page < @pagy.pages
        }
      }
    end
  end
end

#updatevoid

This method returns an undefined value.

Updates the tag loaded by #set_tag.



94
95
96
97
98
99
100
# File 'app/controllers/crm/tags_controller.rb', line 94

def update
  if @tag.update(tag_params)
    redirect_to tag_path(@tag), notice: 'Tag was successfully updated.'
  else
    render :edit, status: :unprocessable_content
  end
end