diff --git a/app/models/item.rb b/app/models/item.rb index b9b0acaa..ee06ec8e 100644 --- a/app/models/item.rb +++ b/app/models/item.rb @@ -3,6 +3,7 @@ require "async/barrier" class Item < ApplicationRecord include PrettyParam + include Item::Dyeworks # We use the `type` column to mean something other than what Rails means! self.inheritance_column = nil @@ -196,49 +197,6 @@ class Item < ApplicationRecord nc_mall_record.present? end - def dyeworks? - dyeworks_base_item.present? - end - - # Whether this is a Dyeworks item whose base item can currently be purchased - # in the NC Mall. It may or may not currently be *dyeable* in the NC Mall, - # because Dyeworks eligibility is often a limited-time event. - def dyeworks_base_buyable? - dyeworks_base_item.present? && dyeworks_base_item.currently_in_mall? - end - - # Whether this is one of the few Dyeworks items that can be dyed in the NC - # Mall at any time, rather than as part of a limited-time event. (Owls tracks - # this, not us!) - def dyeworks_permanent? - return false if nc_trade_value.nil? - nc_trade_value.value_text.include?("Permanent Dyeworks") - end - - # Whether this is a Dyeworks item whose base item can currently be purchased - # in the NC Mall, then dyed via Dyeworks. (Owls tracks this last part!) - def dyeworks_buyable? - # TODO: Add support for limited-time Dyeworks items. Does Owls offer this - # info too? (At time of writing, there are no active Dyeworks events.) - dyeworks_base_buyable? && dyeworks_permanent? - end - - DYEWORKS_NAME_PATTERN = %r{ - ^( - # Most Dyeworks items have a colon in the name. - Dyeworks\s+(?.+?:)\s*(?.+) - | - # But sometimes they omit it. If so, assume the first word is the color! - Dyeworks\s+(?\S+)\s*(?.+) - )$ - }x - def inferred_dyeworks_base_item - name_match = name.match(DYEWORKS_NAME_PATTERN) - return nil if name_match.nil? - - Item.find_by_name(name_match["base"]) - end - def source if dyeworks_buyable? :dyeworks diff --git a/app/models/item/dyeworks.rb b/app/models/item/dyeworks.rb new file mode 100644 index 00000000..5a458580 --- /dev/null +++ b/app/models/item/dyeworks.rb @@ -0,0 +1,106 @@ +class Item + module Dyeworks + def dyeworks? + dyeworks_base_item.present? + end + + # Whether this is a Dyeworks item whose base item can currently be purchased + # in the NC Mall, then dyed via Dyeworks. (Owls tracks this last part!) + def dyeworks_buyable? + dyeworks_base_buyable? && dyeworks_dyeable? + end + + # Whether this is a Dyeworks item whose base item can currently be purchased + # in the NC Mall. It may or may not currently be *dyeable* in the NC Mall, + # because Dyeworks eligibility is often a limited-time event. + def dyeworks_base_buyable? + dyeworks_base_item.present? && dyeworks_base_item.currently_in_mall? + end + + # Whether this is a Dyeworks item that can be dyed in the NC Mall ~right now, + # either at any time or as a limited-time event. (Owls tracks this, not us!) + def dyeworks_dyeable? + dyeworks_permanent? || dyeworks_limited_active? + end + + # Whether this is one of the few Dyeworks items that can be dyed in the NC + # Mall at any time, rather than as part of a limited-time event. (Owls tracks + # this, not us!) + DYEWORKS_PERMANENT_PATTERN = /Permanent\s*Dyeworks/i + def dyeworks_permanent? + return false if nc_trade_value.nil? + nc_trade_value.value_text.match?(DYEWORKS_PERMANENT_PATTERN) + end + + # Whether this is a Dyeworks item that can be dyed in the NC Mall ~right + # now, as part of a limited-time event. (Owls tracks this, not us!) + # + # If we aren't sure of the final date, this will still return `true`, on + # the assumption it *is* dyeable right now and we just don't understand the + # details of what Owls told us. + def dyeworks_limited_active? + return false unless dyeworks_limited? + return true if dyeworks_limited_final_date.nil? + + # NOTE: The application is configured to NST, so this should be + # equivalent to `Date.today`, but this is clearer and more correct imo! + today_in_nst = Time.find_zone("Pacific Time (US & Canada)").today + + today_in_nst <= dyeworks_limited_final_date + end + + # Whether this is a Dyeworks item that can only be dyed as part of a + # limited-time event. (This may return true even if the end date has + # passed, see `dyeworks_limited_active?`.) (Owls tracks this, not us!) + DYEWORKS_LIMITED_PATTERN = /Limited\s*Dyeworks/i + def dyeworks_limited? + return false if nc_trade_value.nil? + nc_trade_value.value_text.match?(DYEWORKS_LIMITED_PATTERN) + end + + # If this is a limited-time Dyeworks item, this is the date we think the + # event will end on. Even if `dyeworks_limited?` returns true, this could + # still be `nil`, if we fail to parse this. (Owls tracks this, not us!) + DYEWORKS_LIMITED_FINAL_DATE_PATTERN = + /Dyeable\s*Thru\s*(?[a-z]+)\s*(?[0-9]+)/i + def dyeworks_limited_final_date + return nil unless dyeworks_limited? + + match = nc_trade_value.value_text. + match(DYEWORKS_LIMITED_FINAL_DATE_PATTERN) + return nil if match.nil? + + # Parse this " " date as the *next* such date: parse it as + # this year at first, then add a year if it turns out to be in the past. + # + # NOTE: This could return strange results if the Owls date contains + # something surprising! But the heuristic nature helps with e.g. + # flexibility if they abbreviate months, so let's lean into `Date.parse`. + match => {month:, day:} + date = Date.parse("#{month} #{day}, #{Date.today.year}") + date += 1.year if date < Date.today + + date + end + + # Infer what base item this Dyeworks item probably relates to, based on + # their names. We only use this when a new item is modeled to initialize + # the `dyeworks_base_item` relationship in the database; after that, we + # just use whatever the database says. (This allows manual overrides!) + DYEWORKS_NAME_PATTERN = %r{ + ^( + # Most Dyeworks items have a colon in the name. + Dyeworks\s+(?.+?:)\s*(?.+) + | + # But sometimes they omit it. If so, assume the first word is the color! + Dyeworks\s+(?\S+)\s*(?.+) + )$ + }x + def inferred_dyeworks_base_item + name_match = name.match(DYEWORKS_NAME_PATTERN) + return nil if name_match.nil? + + Item.find_by_name(name_match["base"]) + end + end +end \ No newline at end of file diff --git a/app/views/items/sources.html.haml b/app/views/items/sources.html.haml index 8cf9b9be..a583cf10 100644 --- a/app/views/items/sources.html.haml +++ b/app/views/items/sources.html.haml @@ -77,9 +77,24 @@ title: "This recipe is NOT currently scheduled to be removed " + "from Dyeworks. It might not stay forever, but it's also " + "not part of a known limited-time event, like most " + - "Dyeworks items are." + "Dyeworks items are. (Thanks Owls team!)" } (Permanent) + - elsif item.dyeworks_limited_final_date.present? + %span.dyeworks-timeframe{ + title: "This recipe is part of a limited-time Dyeworks " + + "event. The last day you can dye this is " + + "#{item.dyeworks_limited_final_date.to_fs(:month_and_day)}. " + + "(Thanks Owls team!)" + } + (Thru #{item.dyeworks_limited_final_date.to_fs(:month_and_day)}) + - elsif item.dyeworks_limited? + %span.dyeworks-timeframe{ + title: "This recipe is part of a limited-time Dyeworks " + + "event, and is scheduled to be removed from the NC Mall " + + "soon. (Thanks Owls team!)" + } + (Limited-time) %button{onclick: "alert('Todo!')"} = cart_icon alt: "" diff --git a/config/application.rb b/config/application.rb index d2bf815a..74392543 100644 --- a/config/application.rb +++ b/config/application.rb @@ -44,7 +44,7 @@ module OpenneoImpressItems # These settings can be overridden in specific environments using the files # in config/environments, which are processed later. # - # config.time_zone = "Central Time (US & Canada)" + config.time_zone = "Pacific Time (US & Canada)" # config.eager_load_paths << Rails.root.join("extras") config.i18n.fallbacks = true diff --git a/config/initializers/date_formats.rb b/config/initializers/date_formats.rb new file mode 100644 index 00000000..bf383589 --- /dev/null +++ b/config/initializers/date_formats.rb @@ -0,0 +1 @@ +Date::DATE_FORMATS[:month_and_day] = "%B %e" diff --git a/lib/tasks/public_data.rake b/lib/tasks/public_data.rake index 7079c58e..789611da 100644 --- a/lib/tasks/public_data.rake +++ b/lib/tasks/public_data.rake @@ -63,7 +63,7 @@ namespace :public_data do end desc "Pull and import the latest public data from production (dev only)" - task :pull => :environment do + task :pull => ["db:abort_if_pending_migrations", :environment] do unless Rails.env.development? raise "Can only pull public data in development mode! This helps us " + "ensure we won't overwrite the production database accidentally."