Class: Crm::CoursesController

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

Overview

Controller: courses.

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

#createObject

Creates a course from course_params.



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

def create
  @course = Course.new(course_params)
  authorize! :create, @course
  if @course.save
    redirect_to @course, notice: 'Course created'
  else
    render :new, status: :unprocessable_content
  end
end

#destroyObject

Destroys the course.



83
84
85
86
87
88
89
90
91
92
# File 'app/controllers/crm/courses_controller.rb', line 83

def destroy
  authorize! :destroy, @course
  @course.destroy
  if @course.errors.present?
    flash[:error] = @course.errors_to_s
  else
    flash[:info] = "#{@course.id} deleted"
  end
  redirect_to url_for(action: :index)
end

#editObject

Renders the form to edit an existing course.



50
51
52
# File 'app/controllers/crm/courses_controller.rb', line 50

def edit
  authorize! :update, @course
end

#indexObject

Doubles as the training dashboard: program-wide stats plus per-course
enrollment breakdowns.



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'app/controllers/crm/courses_controller.rb', line 11

def index
  @courses = Course.all.ransack(params[:q]).result.order(:name)

  graded_exams = CourseExam.where(state: %w[passed failed])
  @stats = {
    enrollments: CourseEnrollment.count,
    active_learners: CourseEnrollment.where(state: 'active').count,
    completed: CourseEnrollment.where(state: 'completed').count,
    graded_exams: graded_exams.count,
    passed_exams: graded_exams.where(state: 'passed').count,
    active_certifications: Certification.active.count,
    requested_certifications: Certification.requested.count,
    expiring_soon: Certification.active.where(expires_at: ..30.days.from_now).count
  }
  # { [course_id, state] => count } — one query for the whole table.
  @enrollment_counts = CourseEnrollment.group(:course_id, :state).count
  # Precompute per-course totals to avoid scanning and summing the hash per course in the view.
  @enrollment_totals = CourseEnrollment.group(:course_id).count
  # Precompute per-course exam counts to avoid N+1 queries in the view.
  @graded_counts = CourseExam.joins(:course_enrollment).where(state: %w[passed failed]).group(:course_id).count
  @passed_counts = CourseExam.joins(:course_enrollment).where(state: 'passed').group(:course_id).count
end

#newObject

Renders the form for a new course.



44
45
46
47
# File 'app/controllers/crm/courses_controller.rb', line 44

def new
  @course = Course.new
  authorize! :create, @course
end

#showObject

Shows a course's dashboard stats and recent enrollments.



35
36
37
38
39
40
41
# File 'app/controllers/crm/courses_controller.rb', line 35

def show
  dashboard = Course::DashboardStats.call(@course)
  @topic_categories = dashboard.topic_categories
  @topic_exams = dashboard.topic_exams
  @recent_enrollments = dashboard.recent_enrollments
  @stats = dashboard.stats
end

#sync_enrollmentsObject

Content sync: push active lesson topics added after learners enrolled
into their active enrollments (per-learner CustomerTopic copies are
snapshotted at enroll time, so later course edits don't reach them).
GET renders a preview with the exact counts; POST applies it.

For bulk operations >BULK_SYNC_THRESHOLD, requires two explicit
confirmations separated by user interaction to prevent accidental
mass assignments.



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'app/controllers/crm/courses_controller.rb', line 102

def sync_enrollments
  authorize! :update, @course
  @additions = missing_topic_assignments
  @total = @additions.values.sum(&:size)
  return unless request.post?

  # Validate batch ID and confirmation state for two-stage confirmation.
  @batch = CourseSyncBatch.find(params.expect(:batch_id))
  unless @batch.course_id == @course.id && @batch.pending?
    flash[:error] = 'Invalid or expired sync batch. Please try again.'
    redirect_to sync_enrollments_course_path(@course)
    return
  end

  # For large batches, require a second confirmation.
  confirmation_state = params.dig(:confirmation_state)
  if @batch.large_batch? && confirmation_state != 'confirmed'
    # Pass confirmation_state through URL so the view knows we're awaiting final confirmation.
    redirect_to sync_enrollments_course_path(@course, confirmation_state: confirmation_state || 'awaiting_final'),
                status: :see_other
    return
  end

  # Store affected records before applying changes (for potential rollback).
  @batch.store_affected_records!(@additions)
  @batch.mark_processing!

  created = 0
  @additions.each do |enrollment, topics|
    topics.each do |topic|
      created += 1 if assign_missing_topic(enrollment, topic)
    end
  end

  @batch.mark_completed!(assigned: created, failed: @batch.total_topics_to_assign - created)
  flash[:info] = "Content sync complete — #{created} #{'topic'.pluralize(created)} assigned across #{@additions.size} #{'enrollment'.pluralize(@additions.size)}."
  redirect_to course_path(@course)
rescue ActiveRecord::RecordNotFound
  flash[:error] = 'Sync batch not found. Please try again.'
  redirect_to sync_enrollments_course_path(@course)
end

#updateObject

Updates a course from course_params, or clears its image when
params[:remove_image] is present.



67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'app/controllers/crm/courses_controller.rb', line 67

def update
  authorize! :update, @course
  if params[:remove_image].present?
    if @course.update(image_uid: nil, image_name: nil)
      redirect_to edit_course_path(@course), notice: 'Image removed'
    else
      render :edit, status: :unprocessable_content
    end
  elsif @course.update(course_params)
    redirect_to @course, notice: 'Course updated'
  else
    render :edit, status: :unprocessable_content
  end
end