Compare commits

...

9 commits

Author SHA1 Message Date
10301900e2 Credit the Owls team for Dyeworks extra info in the Item Getting Guide 2024-06-18 16:49:09 -07:00
ec476f4c65 List the specific Dyeworks end date in Item Getting Guide, if we know 2024-06-18 16:47:05 -07:00
a9f44c0aa6 Configure the application's default time zone to NST (US Pacific)
This doesn't affect a lot right now, it was previously defaulting to
UTC I think? And I've confirmed that, while timestamps are stored in
the database as UTC, they're not interpreted any differently with this
setting. (Or, rather, it's loaded as a `DateTime` object for the same
moment in time, but in the NST time zone. Good!)

But this feels like a more useful default for displaying things for
development etc, and moreover, I'm working on some logic for things
like "when do limited-time Dyeworks items expire exactly?", and that
logic is made *much* simpler if we can just compare dates in NST by
default rather than fudge around with the zones on them and figuring
out the correct midnight!

(Although, as I type this, I think I maybe have thought of an easier
way to do it? So maybe this change won't actually be necessary for
that, but it still feels like a more sensible default for us
regardless!)
2024-06-18 16:39:37 -07:00
4e2110bf25 Add comment to Item#inferred_dyeworks_base_item 2024-06-18 15:26:28 -07:00
c6fbb9b797 Reorder Item::Dyeworks methods a bit 2024-06-18 15:23:39 -07:00
98ecbf029d Be case-insensitive when checking Owls values for Dyeworks status
Just to be a bit more resilient! I'm not aware of any issues rn but
this seems wise!
2024-06-18 15:22:15 -07:00
015010345a Extract Dyeworks methods into Item::Dyeworks module
There's just starting to be a lot going on, so I pulled them out into
here!

I also considered a like, `Item::DyeworksStatus` class, and then you'd
go like, `item.dyeworks.buyable?`. But idk, I think it's nice that the
current API is simple for callers, and being able to do things like
`items.filter(&:dyeworks_buyable?)` is pretty darn convenient.

This solution lets us keep the increasing number of Dyeworks methods
from polluting the main `item.rb`, while still keeping the API
identical!
2024-06-18 15:21:43 -07:00
26dfe13b0e Parse "Limited Dyeworks" items from Owls
Yay thank you Owls team! I might also try to parse the dates too, the
format seems to be "Owls: Limited Dyeworks - Dyeable Thru July 15"
2024-06-18 14:59:09 -07:00
4d24a9577f Only run public_data:pull if there are no pending migrations
Oh this was a fun little dev environment bug: I ran `public_data:pull`
on my laptop before migrating my database, so the `items` table pulled
as the latest production version, which included the migrations, but
they hadn't been marked as "run" yet.

So Rails was still telling me I needed to run them, but the migrations
themselves were crashing, with stuff like "there's already a column
with this name!"

This change ensures that `public_data:pull` won't run until migrations
are done, to prevent silly accidents like that.
2024-06-18 14:52:54 -07:00
6 changed files with 126 additions and 46 deletions

View file

@ -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
@ -198,49 +199,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+(?<color>.+?:)\s*(?<base>.+)
|
# But sometimes they omit it. If so, assume the first word is the color!
Dyeworks\s+(?<color>\S+)\s*(?<base>.+)
)$
}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

106
app/models/item/dyeworks.rb Normal file
View file

@ -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*(?<month>[a-z]+)\s*(?<day>[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 "<Month> <Day>" 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+(?<color>.+?:)\s*(?<base>.+)
|
# But sometimes they omit it. If so, assume the first word is the color!
Dyeworks\s+(?<color>\S+)\s*(?<base>.+)
)$
}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

View file

@ -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: ""

View file

@ -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

View file

@ -0,0 +1 @@
Date::DATE_FORMATS[:month_and_day] = "%B %e"

View file

@ -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."