Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions app/controllers/workshop_ideas_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ def destroy

# Optional hooks for setting variables for forms or index
def set_form_variables
@age_ranges = Category.includes(:category_type).where("category_types.name = 'AgeRange'").pluck(:name)
@potential_series_workshops = authorized_scope(Workshop.published).includes(:windows_type).order(:title)
@sectors = Sector.published
@windows_types = WindowsType.all
Expand Down Expand Up @@ -111,7 +110,6 @@ def workshop_idea_params
:time_hours, :time_intro, :time_minutes,
:time_opening, :time_opening_circle, :time_warm_up,

:age_range, :age_range_spanish,
:closing, :closing_spanish,
:creation, :creation_spanish,
:demonstration, :demonstration_spanish,
Expand Down Expand Up @@ -149,7 +147,6 @@ def workshop_idea_params
:rhino_objective_spanish,
:rhino_materials_spanish,
:rhino_optional_materials_spanish,
:rhino_age_range_spanish,
:rhino_setup_spanish,
:rhino_introduction_spanish,
:rhino_opening_circle_spanish,
Expand Down
4 changes: 0 additions & 4 deletions app/controllers/workshops_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,6 @@ def set_show


def set_form_variables
@age_ranges = Category.includes(:category_type)
.where("category_types.name = 'AgeRange'").pluck(:name)
potential_series = authorized_scope(Workshop.published).includes(:windows_type)
potential_series = potential_series.where.not(id: @workshop.id) if @workshop.persisted?
@potential_series_workshops = authorized_scope(potential_series).order(:title)
Expand Down Expand Up @@ -228,7 +226,6 @@ def workshop_params
:time_intro, :time_closing, :time_creation, :time_demonstration,
:time_warm_up, :time_opening, :time_opening_circle,

:age_range, :age_range_spanish,
:closing, :closing_spanish,
:creation, :creation_spanish,
:demonstration, :demonstration_spanish,
Expand Down Expand Up @@ -267,7 +264,6 @@ def workshop_params
:rhino_objective_spanish,
:rhino_materials_spanish,
:rhino_optional_materials_spanish,
:rhino_age_range_spanish,
:rhino_setup_spanish,
:rhino_introduction_spanish,
:rhino_opening_circle_spanish,
Expand Down
8 changes: 4 additions & 4 deletions app/decorators/workshop_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,21 +108,22 @@ def spanish_field_values

def display_fields
[ :objective, :materials, :optional_materials, :timeframe,
:age_range, :setup, :introduction, :demonstration,
:setup, :introduction, :demonstration,
:opening_circle, :warm_up,
:visualization, :creation, :closing, :notes, :tips, :misc1, :misc2
]
end

def display_spanish_fields
[
fields = [
:objective_spanish, :materials_spanish, :optional_materials_spanish,
:age_range_spanish, :setup_spanish,
:setup_spanish,
:introduction_spanish, :demonstration_spanish, :opening_circle_spanish,
:warm_up_spanish, :visualization_spanish, :creation_spanish,
:closing_spanish, :notes_spanish, :tips_spanish, :misc1_spanish,
:misc2_spanish, :extra_field_spanish # :timeframe_spanish,
]
fields
end

def labels_spanish
Expand All @@ -131,7 +132,6 @@ def labels_spanish
materials_spanish: "Materiales",
optional_materials_spanish: "Materiales Opcionales",
timeframe_spanish: "Periodo de tiempo",
age_range_spanish: "Rango de edad",
setup_spanish: "Preparativos",
introduction_spanish: "Introducción",
demonstration_spanish: "Demostración",
Expand Down
6 changes: 3 additions & 3 deletions app/decorators/workshop_idea_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ def default_display_image
end

def display_spanish_fields
[
fields = [
:objective_spanish, :materials_spanish, :optional_materials_spanish,
:age_range_spanish, :setup_spanish,
:setup_spanish,
:introduction_spanish, :demonstration_spanish, :opening_circle_spanish,
:warm_up_spanish, :visualization_spanish, :creation_spanish,
:closing_spanish, :notes_spanish, :tips_spanish # :timeframe_spanish,
]
fields
end

def labels_spanish
Expand All @@ -23,7 +24,6 @@ def labels_spanish
materials_spanish: "Materiales",
optional_materials_spanish: "Materiales Opcionales",
timeframe_spanish: "Periodo de tiempo",
age_range_spanish: "Rango de edad",
setup_spanish: "Preparativos",
introduction_spanish: "Introducción",
demonstration_spanish: "Demostración",
Expand Down
4 changes: 1 addition & 3 deletions app/models/workshop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def self.mentionable_rich_text_fields
:rhino_introduction, :rhino_opening_circle, :rhino_demonstration, :rhino_warm_up,
:rhino_visualization, :rhino_creation, :rhino_closing, :rhino_notes, :rhino_tips,
:rhino_misc1, :rhino_misc2, :rhino_extra_field, :rhino_objective_spanish,
:rhino_materials_spanish, :rhino_optional_materials_spanish, :rhino_age_range_spanish,
:rhino_materials_spanish, :rhino_optional_materials_spanish,
:rhino_setup_spanish, :rhino_introduction_spanish, :rhino_opening_circle_spanish,
:rhino_demonstration_spanish, :rhino_warm_up_spanish, :rhino_visualization_spanish,
:rhino_creation_spanish, :rhino_closing_spanish, :rhino_notes_spanish,
Expand Down Expand Up @@ -97,7 +97,6 @@ def self.mentionable_rich_text_fields
has_rich_text :rhino_objective_spanish
has_rich_text :rhino_materials_spanish
has_rich_text :rhino_optional_materials_spanish
has_rich_text :rhino_age_range_spanish
has_rich_text :rhino_setup_spanish
has_rich_text :rhino_introduction_spanish
has_rich_text :rhino_opening_circle_spanish
Expand All @@ -123,7 +122,6 @@ def self.mentionable_rich_text_fields
# Validations
validates_presence_of :title
# validates_presence_of :month, :year, if: Proc.new { |workshop| workshop.legacy }
validates_length_of :age_range, maximum: 16
validates :rating, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 5 }

# Nested attributes
Expand Down
1 change: 0 additions & 1 deletion app/models/workshop_idea.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ class WorkshopIdea < ApplicationRecord
has_rich_text :rhino_objective_spanish
has_rich_text :rhino_materials_spanish
has_rich_text :rhino_optional_materials_spanish
has_rich_text :rhino_age_range_spanish
has_rich_text :rhino_setup_spanish
has_rich_text :rhino_introduction_spanish
has_rich_text :rhino_opening_circle_spanish
Expand Down
3 changes: 1 addition & 2 deletions app/services/workshop_from_idea_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def call

def attributes_from_idea
workshop_idea.attributes.slice(
"title", "windows_type_id", "age_range", "author_credit_preference",
"title", "windows_type_id", "author_credit_preference",
"time_intro", "time_closing", "time_creation",
"time_demonstration", "time_warm_up",
"time_opening", "time_opening_circle"
Expand All @@ -42,7 +42,6 @@ def attributes_from_idea
rhino_objective_spanish: workshop_idea.rhino_objective_spanish,
rhino_materials_spanish: workshop_idea.rhino_materials_spanish,
rhino_optional_materials_spanish: workshop_idea.rhino_optional_materials_spanish,
rhino_age_range_spanish: workshop_idea.rhino_age_range_spanish,
rhino_setup_spanish: workshop_idea.rhino_setup_spanish,
rhino_introduction_spanish: workshop_idea.rhino_introduction_spanish,
rhino_opening_circle_spanish: workshop_idea.rhino_opening_circle_spanish,
Expand Down
2 changes: 0 additions & 2 deletions app/views/workshops/_show_body.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@
<% end %>

<% workshop_sections = { "Objective" => workshop.rhino_objective,
"Age range" => workshop.age_range,
"Materials" => workshop.rhino_materials,
"Optional materials" => workshop.rhino_optional_materials,
"Suggested time frame" => workshop.time_frame_total,
Expand Down Expand Up @@ -155,7 +154,6 @@
<!-- Spanish -->
<div id="spanish-content" data-tabs-target="tabContent" class="hidden">
<% workshop_sections_spanish = { "Objetivo" => workshop.rhino_objective_spanish,
"Rango de edad" => workshop.age_range_spanish,
"Materiales" => workshop.rhino_materials_spanish,
"Materiales opcionales" => workshop.rhino_optional_materials_spanish,
"Setup" => workshop.rhino_setup_spanish,
Expand Down
2 changes: 1 addition & 1 deletion db/seeds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ def find_or_create_by_name!(klass, name, **attrs, &block)
category_type_categories = [
[ "AgeRange", "3-5" ],
[ "AgeRange", "6-12" ],
[ "AgeRange", "Teen" ],
[ "AgeRange", "13-17" ],
[ "AgeRange", "18+" ],
[ "AgeRange", "Mixed-age groups" ],
[ "AgeRange", "Family Windows" ],
Expand Down
205 changes: 205 additions & 0 deletions lib/tasks/convert_age_ranges.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# frozen_string_literal: true

namespace :data do
desc "Convert workshop age_range/age_range_spanish free-text into AgeRange categorizable_items"
task convert_age_ranges: :environment do
age_range_type = CategoryType.find_by!(name: "AgeRange")
all_categories = age_range_type.categories.to_a
categories = all_categories.index_by { |c| c.name.downcase }

cat_3_5 = categories["3-5"]
cat_6_12 = categories["6-12"]
cat_13_17 = categories["13-17"]
cat_18 = categories["18+"]
cat_mixed = categories["mixed-age groups"]
cat_family = categories["family windows"]

unless [ cat_3_5, cat_6_12, cat_13_17, cat_18, cat_mixed, cat_family ].all?
abort "Missing AgeRange categories. Run seeds first. Need: 3-5, 6-12, 13-17, 18+, Mixed-age groups, Family Windows"
end

# Normalize free-text to plain lowercase string
normalize = lambda do |raw|
return nil if raw.blank?

text = raw.strip
text = CGI.unescapeHTML(text) # &ntilde; -> ñ etc.
text = text.gsub(/<[^>]+>/, "") # strip HTML tags
text = text.gsub(/\r\n|\r|\n/, " ") # collapse newlines
text = text.strip.squeeze(" ")
text.downcase.presence
end

# Map a normalized age_range string to one or more Category records.
classify = lambda do |raw|
text = normalize.call(raw)
return [] if text.nil? || %w[0 x n/a].include?(text)

# Extract numeric bounds if present (e.g. "5-12", "6 and up", "10+")
low = nil
high = nil

if text =~ /(\d+)\s*[-–—]\s*(\d+)/
low = $1.to_i
high = $2.to_i
elsif text =~ /(\d+)\s*(?:to|a)\s+(\d+)/
low = $1.to_i
high = $2.to_i
elsif text =~ /(\d+)\s*(\+|and\s*(up|above|older)|&\s*up|years? old on up)/
low = $1.to_i
high = 99
elsif text =~ /(\d+)\s*(?:en adelante|para arriba|y (?:más|mas|hasta|mayores))/
low = $1.to_i
high = 99
elsif text =~ /(\d+)\s*(?:o más|o mas)/
low = $1.to_i
high = 99
elsif text =~ /(\d+)\s*up\b/
low = $1.to_i
high = 99
elsif text =~ /(\d+)\s*(?:años? en adelante|años? y mayores)/
low = $1.to_i
high = 99
end

# Keyword-based detection
has_adult = text.match?(/\badult|women|18\s*\+|18 and (up|older)/i)
has_teen = text.match?(/\bteen|tween|13\s*[-–&]\s*(18|19|up)|14-17|13\/18/i)
has_child = text.match?(/\bchild|elementary|preschool|school\s*age/i)
has_all = text.match?(/\ball\s*age|\bany\s*age|\bany\b|\ball\b|\bmixed|family/i)

result = Set.new

result << cat_mixed if has_all

if has_adult && !low
result << cat_18
end

if has_teen && !low
result << cat_13_17
end

if has_child && !low
result << cat_6_12
result << cat_3_5
end

result << cat_3_5 if text.match?(/\bpreschool\b/)

# Numeric range mapping
if low && high
result << cat_3_5 if low <= 5 && high >= 3
result << cat_6_12 if low <= 12 && high >= 6
result << cat_13_17 if low <= 17 && high >= 13
result << cat_18 if high >= 18 || (high == 99 && low >= 18)
end

# "X and up" fallback if nothing matched above
if result.empty? && low && high == 99
result << cat_3_5 if low <= 5
result << cat_6_12 if low <= 12
result << cat_13_17 if low <= 17
result << cat_18 if low <= 18
end

result.to_a
end

# Build a set of exact-match strings that should nil out the column.
# Matches any category name (case-insensitive, stripped).
category_names = Set.new(all_categories.map { |c| c.name.downcase })

# Check if a raw value, once cleaned, exactly matches a category name.
exact_match = lambda do |raw|
text = normalize.call(raw)
return false if text.nil?
category_names.include?(text)
end

comment_tag = "[AGE_RANGE_DATA]"

total = 0
skipped = 0
already_tagged = 0
commented = 0
nilled_en = 0
nilled_es = 0
unmatched = []

Workshop.where.not(age_range: [ nil, "" ]).or(
Workshop.where.not(age_range_spanish: [ nil, "" ])
).find_each do |workshop|
# --- Classify from both fields (union of matches) ---
matched_from_en = classify.call(workshop.age_range)
matched_from_es = classify.call(workshop.age_range_spanish)
matched_categories = (matched_from_en + matched_from_es).uniq

raw_en = workshop.age_range
raw_es = normalize.call(workshop.age_range_spanish)
source_parts = []
source_parts << "age_range: '#{raw_en}'" if raw_en.present?
source_parts << "age_range_spanish: '#{raw_es}'" if raw_es.present?
source_label = source_parts.join(", ")

if matched_categories.empty?
unmatched << { id: workshop.id, age_range: raw_en, age_range_spanish: raw_es }
skipped += 1

# Leave a comment so staff can manually review
workshop.comments.create!(
body: "#{comment_tag} Could not auto-apply age range categories from #{source_label}. Please review and assign manually."
)
commented += 1
else
# Skip categories already assigned
existing_ids = workshop.categorizable_items
.joins(:category)
.where(categories: { category_type_id: age_range_type.id })
.pluck(:category_id)

new_categories = matched_categories.reject { |c| existing_ids.include?(c.id) }

if new_categories.empty?
already_tagged += 1
else
new_categories.each do |cat|
workshop.categorizable_items.create!(category: cat)
end

applied_names = new_categories.map(&:name).join(", ")
workshop.comments.create!(
body: "#{comment_tag} Auto-applied age range categories: #{applied_names} from #{source_label}."
)
commented += 1
total += 1
end
end

# --- Nil out columns that exactly match a category name ---
updates = {}
if workshop.age_range.present? && exact_match.call(workshop.age_range)
updates[:age_range] = nil
nilled_en += 1
end
if workshop.age_range_spanish.present? && exact_match.call(workshop.age_range_spanish)
updates[:age_range_spanish] = nil
nilled_es += 1
end
workshop.update_columns(updates) if updates.any?
end

puts "Done! Tagged #{total} workshops."
puts "Comments created: #{commented}" if commented > 0
puts "Already tagged: #{already_tagged}" if already_tagged > 0
puts "Skipped (unmatched): #{skipped}" if skipped > 0
puts "Nilled age_range: #{nilled_en}" if nilled_en > 0
puts "Nilled age_range_spanish: #{nilled_es}" if nilled_es > 0
if unmatched.any?
puts "\nUnmatched values (#{unmatched.size}):"
unmatched.each do |u|
puts " Workshop ##{u[:id]}: en=#{u[:age_range].inspect} es=#{u[:age_range_spanish].inspect}"
end
end
end
end
1 change: 0 additions & 1 deletion spec/models/workshop_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
subject { build(:workshop, created_by: create(:user), windows_type: create(:windows_type)) }

it { should validate_presence_of(:title) }
it { should validate_length_of(:age_range).is_at_most(16) }

# Conditional presence validation for legacy workshops (month, year)
context 'when legacy is true' do
Expand Down
Loading
Loading