Commit graph

614 commits

Author SHA1 Message Date
c78d51ab01 Don't hardcode the series name in AltStyle thumbnail_url
This is just a bit of future-proofing! We also add a default thumbnail
URL of the cute "Neopets Circle Background", for cases where the series
name isn't known yet.
2024-05-29 18:36:30 -07:00
758b62e7d5 Improve performance of Owls values in Item Getting Guide
Now we preload them all concurrently, instead of in sequence when the
template gets around to asking for them!
2024-05-27 16:21:22 -07:00
d34bebc336 Use a pet face when there's no paint brush, in Item Getting Guide
Now, for colors like Mutant or Magma where there's no paint brush
image to show, we use a sample pet image instead, to help it have equal
visual weight and clarity as the cases with the paint brushes.

We do some cleverness in here to make sure to always show the relevant
species, if possible!
2024-05-22 17:53:52 -07:00
1b03c2caed Add more PB item info and links to Item Getting Guide
I add some infrastructural support for inferring an item's paintbrush
color (if any), and a field to the database to manually track an item's
paint brush item name! This is both useful for tracking which colors
are even *available* via paint brush, and also for working with colors
with unusual paint brush names, like the "Get Off My Lawn Paint Brush"
(for Elderly pets).

Here's the script I ran to backfill this for current colors and their
paint brushes!

```rb
Color.find_by_name("Baby").update!(pb_item_name: "Baby Paint Brush")
Color.find_by_name("Biscuit").update!(pb_item_name: "Biscuit Paint Brush")
Color.find_by_name("Blue").update!(pb_item_name: "Blue Paint Brush")
Color.find_by_name("Brown").update!(pb_item_name: "Brown Paint Brush")
Color.find_by_name("Camouflage").update!(pb_item_name: "Camouflage Paint Brush")
Color.find_by_name("Candy").update!(pb_item_name: "Candy Paint Brush")
Color.find_by_name("Checkered").update!(pb_item_name: "Checkered Paint Brush")
Color.find_by_name("Christmas").update!(pb_item_name: "Christmas Paint Brush")
Color.find_by_name("Cloud").update!(pb_item_name: "Cloud Paint Brush")
Color.find_by_name("Darigan").update!(pb_item_name: "Darigan Paint Brush")
Color.find_by_name("Dimensional").update!(pb_item_name: "Dimensional Paint Brush")
Color.find_by_name("Disco").update!(pb_item_name: "Disco Fever Paint Brush")
Color.find_by_name("Electric").update!(pb_item_name: "Electric Blue Paint Brush")
Color.find_by_name("Eventide").update!(pb_item_name: "Eventide Paint Brush")
Color.find_by_name("Faerie").update!(pb_item_name: "Faerie Paint Brush")
Color.find_by_name("Fire").update!(pb_item_name: "Fire, Fire, Your Pants On Fire Paint Brush")
Color.find_by_name("Elderlyboy").update!(pb_item_name: "Get Off My Lawn Paint Brush")
Color.find_by_name("Elderlygirl").update!(pb_item_name: "Get Off My Lawn Paint Brush")
Color.find_by_name("Ghost").update!(pb_item_name: "Ghost Paint Brush")
Color.find_by_name("Glowing").update!(pb_item_name: "Glowing Paint Brush")
Color.find_by_name("Gold").update!(pb_item_name: "Golden Paint Brush")
Color.find_by_name("Green").update!(pb_item_name: "Green Paint Brush")
Color.find_by_name("Grey").update!(pb_item_name: "Grey Paint Brush")
Color.find_by_name("Halloween").update!(pb_item_name: "Halloween Paint Brush")
Color.find_by_name("Invisible").update!(pb_item_name: "Invisible Paint Brush")
Color.find_by_name("Desert").update!(pb_item_name: "Lost Desert Paint Brush")
Color.find_by_name("Maractite").update!(pb_item_name: "Maractite Paint Brush")
Color.find_by_name("Maraquan").update!(pb_item_name: "Maraquan Paint Brush")
Color.find_by_name("Marble").update!(pb_item_name: "Marble Paint Brush")
Color.find_by_name("Island").update!(pb_item_name: "Mystery Island Paint Brush")
Color.find_by_name("Oil Paint").update!(pb_item_name: "Oil Paint Brush")
Color.find_by_name("Orange").update!(pb_item_name: "Orange Paint Brush")
Color.find_by_name("Origami").update!(pb_item_name: "Origami Paint Brush")
Color.find_by_name("Pastel").update!(pb_item_name: "Pastel Paint Brush")
Color.find_by_name("Pink").update!(pb_item_name: "Pink Paint Brush")
Color.find_by_name("Pirate").update!(pb_item_name: "Pirate Paint Brush")
Color.find_by_name("Plushie").update!(pb_item_name: "Plushie Paint Brush")
Color.find_by_name("Polka Dot").update!(pb_item_name: "Polka Dot Paint Brush")
Color.find_by_name("Purple").update!(pb_item_name: "Purple Paint Brush")
Color.find_by_name("Rainbow").update!(pb_item_name: "Rainbow Paint Brush")
Color.find_by_name("Red").update!(pb_item_name: "Red Paint Brush")
Color.find_by_name("Relic").update!(pb_item_name: "Relic Paint Brush")
Color.find_by_name("Royalboy").update!(pb_item_name: "Royal Paint Brush")
Color.find_by_name("Royalgirl").update!(pb_item_name: "Royal Paint Brush")
Color.find_by_name("Sketch").update!(pb_item_name: "Scritchy Sketchy Paint Brush")
Color.find_by_name("Shadow").update!(pb_item_name: "Shadow Paint Brush")
Color.find_by_name("Silver").update!(pb_item_name: "Silver Paint Brush")
Color.find_by_name("Skunk").update!(pb_item_name: "Skunk Paint Brush")
Color.find_by_name("Snow").update!(pb_item_name: "Snow Paint Brush")
Color.find_by_name("Speckled").update!(pb_item_name: "Speckled Paint Brush")
Color.find_by_name("Split").update!(pb_item_name: "Split Paint Brush")
Color.find_by_name("Spotted").update!(pb_item_name: "Spotted Paint Brush")
Color.find_by_name("Starry").update!(pb_item_name: "Starry Paint Brush")
Color.find_by_name("Stealthy").update!(pb_item_name: "Stealth Paint Brush")
Color.find_by_name("Steampunk").update!(pb_item_name: "Steampunk Paint Brush")
Color.find_by_name("Strawberry").update!(pb_item_name: "Strawberry Fields Forever Paint Brush")
Color.find_by_name("Striped").update!(pb_item_name: "Striped Paint Brush")
Color.find_by_name("Swamp Gas").update!(pb_item_name: "Swamp Gas Paint Brush")
Color.find_by_name("Toy").update!(pb_item_name: "Toy Paint Brush")
Color.find_by_name("Transparent").update!(pb_item_name: "Transparent Paint Brush")
Color.find_by_name("Tyrannian").update!(pb_item_name: "Tyrannian Paint Brush")
Color.find_by_name("Usuki Boy").update!(pb_item_name: "Usuki Paint Brush")
Color.find_by_name("Usuki Girl").update!(pb_item_name: "Usuki Paint Brush")
Color.find_by_name("Valentine").update!(pb_item_name: "Valentine Paint Brush")
Color.find_by_name("Water").update!(pb_item_name: "Water Paint Brush")
Color.find_by_name("White").update!(pb_item_name: "White Paint Brush")
Color.find_by_name("Woodland").update!(pb_item_name: "Woodland Paint Brush")
Color.find_by_name("Wraith").update!(pb_item_name: "Wraith Paint Brush")
Color.find_by_name("Yellow").update!(pb_item_name: "Yellow Paint Brush")
Color.find_by_name("Zombie").update!(pb_item_name: "Zombie Paint Brush")

```
2024-05-22 16:09:49 -07:00
23ad52a8db Oops, don't crash calling current_nc_price on non-NC-Mall items
If `nc_mall_record` is `nil`, return `nil`! (This is the nil method
chaining operator!)
2024-05-21 17:24:55 -07:00
d8b577aab1 Add more info to NC Mall section of Item Getting Guide
NC prices, some CSS, and also a new application-level helper that adds
a feature I've long wanted and been working around for Turbo: the
ability to specific that a stylesheet is specific to the current page,
and should be unloaded when removed!

I use this to write `sources.sass` without the usual
`body.items-sources` scoping that we've historically used to control
what pages a stylesheet applies to. (In the long past, this was because
a lot of stylesheets were—and still are–routed through the
`application.sass` stylesheet! But even for more recent standalone page
stylesheets, I've done the scoping, to avoid issues with styles leaking
beyond the page they're meant for when Turbo does a navigation.)
2024-05-14 16:04:40 -07:00
d3b3a3060c Split Item Getting Guide between NC Mall items and Other NC items
This'll affect the recommended acquisition method by a lot!

NC Mall info like current price isn't surfaced anywhere else in the app
right now. It'd probably be good to add to the item page, and maybe
some other places too!
2024-05-14 00:09:27 -07:00
b6e18e10a5 Add bare-bones rails nc_mall:sync task, incl. NCMallRecord model
Currently we only load the homepage, so there's only actually one
wearable item to sync up! But here's the task to do it!

To do this, we also created the backing model NCMallRecord, where we'll
save the current NC Mall state!
2024-05-07 17:40:14 -07:00
9733ceae25 Add bare-bones Item Getting Guide page
TNT requested that we figure out ways to connect the dots between
people's intentions on DTI to their purchases in the NC Mall.

But rather than just slam ad links everywhere, our plan is to design an
actually useful feature about it: the "Item Getting Guide". It'll break
down items by how you can actually get them (NP economy, NC Mall,
retired NC, Dyeworks, etc), and we're planning some cute actions you
can take, like shortcuts for getting them onto trade wishlists or into
your NC Mall cart.

This is just a little demo version of the page, just breaking down
items specified in the URL into NC/NP/PB! Later we'll do more granular
breakdown than this, with more info and actions—and we'll also like,
link to it at all, which isn't the case yet! (The main way we expect
people to get here is by a "Get these items" button we'll add to the
outfit editor, but there might be other paths, too.)
2024-05-06 20:37:59 -07:00
0943e2dbba Fix broken default value in schema for item description
Idk how we got into this state, or if it's environment-dependent or
MySQL-version-dependent or what, but setting up the dev environment on
my macOS machine is complaining that `TEXT` columns can't have default
values.

Well, in that case, let's just have it be a non-nullable field, and add
a note to our code that missing fields *can* cause item saving to fail!
(This was always true, but I'm just extra-noting it because it's
becoming *more* true.)
2024-05-02 13:00:10 -07:00
156cabbab4 Add shadowban mechanism for closet lists
Simple enough to start! If `shadowbanned: true` gets set on a user,
then we show a 404 instead of the actual list page, *unless* you're
logged in as that user, or coming from a known IP of that user.

This isn't a very strong mechanism! Just something to hopefully
increase the costs of messing around with list spam.
2024-04-20 20:57:15 -07:00
4ae5acfdc3 Disallow email addresses in closet list descriptions
Just another attempt to communicate the rules!
2024-04-16 17:04:31 -07:00
fa202af26d Time out if manifest loading takes too long
This hasn't been causing issues as far as I know, I just noticed
*months ago* that I forgot to do this, and have had a sticky note about
it on my desk since then lol.

I tested this by temporarily setting the timeout to `0.5`, and watching
it fail!
2024-04-16 16:18:51 -07:00
f8e4e83723 To "fetch" the image hash of an image hash name, just take off the @!
A further optimization, this lets us use the image hash as the new hash
for the pet type if it would be useful! (whereas before this change,
we'd dip into `fetch_metadata` and just get back `nil`, which was okay
too but a little bit less helpful!)
2024-04-16 15:57:39 -07:00
3ea0842f00 Skip loading image hashes for "pets" that start with @
This is an optimization on top of 9d8f035, in which we skip the network
request altogether in this case that we know will and should fail!
2024-04-16 10:10:28 -07:00
9d8f035360 Oops, stop crashing when modeling "pets" that start with @
Ahh, we recently added a step to pet loading that sends a metadata
request to `PetService.getPet`, which is now (in a sense, correctly!)
raising a `PetNotFound` error when we try modeling with a "pet" that
starts with `@` (a trick we use in situations where we can get an image
hash for a modeling situation, but not an irl pet itself).

In this change, we make it no longer a crashing issue if the pet
metadata request fails: it's not a big deal to have a `PetType` have no
image hash or not have it be up-to-date!

In the next change, I'll also add an optimization to skip fetching it
altogether in this case—but I wanted to see this work first, because
the more general resilience is more important imo!
2024-04-16 10:03:36 -07:00
40bfd42af6 NeoPass launch message on homepage
Some cute logged-in-user differentiation uwu
2024-04-12 07:22:25 -07:00
644b181ed0 Use Neopets username as base name for new NeoPass accounts, if possible
Yay, we got the API endpoint for this! The `linkage` scope is the key.

Rather than pulling back the specific fallback behavior we had wrote
for usernames before, which was slightly different and involved
appending `neopass` in there too (e.g. `matchu-neopass-1234`), I
figured let's just use a lot of the same logic, and just use the
preferred name as the base name. (I figure the `neopass` suffix isn't
that useful anyway, `matchu-1234` kinda looks better tbh! And it's all
fallback stuff that I expect serious users to replace, anyway.)
2024-04-09 07:48:13 -07:00
d50672fd73 Add User-Agent header to our AMFPHP requests
Oh right, I never did catch this when setting up User-Agent in the app!

(I noticed this because I'm making a new request now, and went to look
how we set it in previous stuff, and was like. Oh. We don't anywhere
right now. Interesting LOL)
2024-04-09 06:55:41 -07:00
58d86cf3ac Prevent user from removing all their login methods
Oh right, if you can remove your email, there's a way to fully lock out
your account:

1. Create account via NeoPass, so no password is set.
2. Ensure you have an email saved, then disconnect NeoPass.
3. Remove the email.
4. Now you have no NeoPass, no email, and no password!

In this change, we add a validation that requires an account to always
have at least one login method. This works well for the case described
above, and also helps offer server-side validation to the "can't
disconnect NeoPass until you have an email and password" stuff that
previously was only enforced by disabling the button.

That is, the following procedure could also lock you out before,
whereas now it raises the "Whoops, there was an error disconnecting
your NeoPass from your account, sorry." message:

1. Create account via NeoPass, so no password is set.
2. Ensure you have an email saved, so "Disconnect" button is enabled.
3. Open a new browser tab, and remove the email.
4. In the original browser tab, click "Disconnect".
2024-04-09 06:40:56 -07:00
f450937952 Oops, fix error when saving user settings with no password set
Ah okay, if you leave the password field blank but don't have one set,
our simple `update` method gets annoyed that you left it blank.

In this change, we simplify the model API by just overriding
`update_with_password` with our own special behavior for the
no-password case.
2024-04-09 06:20:13 -07:00
d10c11e261 Oops, fix tracking neopass_email on account creation.
My bad!
2024-04-09 05:45:39 -07:00
5cc219c795 Connect a NeoPass to an existing account
including validation logic to make sure it's not already connected to
another one!

The `intent` param on the NeoPass form is part of the key! Thanks
OmniAuth for making it easy to pass that data through!
2024-04-08 05:33:58 -07:00
09bccd41da Oops, stop saying "Welcome back" for new NeoPass users!
Ahh I see, if you do a no-op update, it still clears the
`previously_new_record?` state, so our NeoPass controller thinks this
account already existed. Instead, let's only do this update if it's an
account that already exists, instead of depending on the no-op-iness!
2024-04-08 05:00:27 -07:00
3e92d89765 Fix error when multiple accounts have a blank email address 2024-04-08 03:46:41 -07:00
ed89380152 Oops, allow NeoPass to be disconnected if you have no email address
That is, you're required to add a password *or* an email before
disconnecting your NeoPass, but idk, I think it's rude to demand an
email from someone for the sake of *disconnection*. Email is no longer
required for accounts that already exist!
2024-04-07 08:42:41 -07:00
54a052848a Disable disconnecting NeoPass if no password/email is set
Just as a precautionary thing! Seems polite.
2024-04-07 08:27:02 -07:00
b827727102 Rename AuthUser#neopass? -> AuthUser#uses_neopass?
This is more consistent with the `uses_omniauth?` we already have, and
it also will help for the next change, where I want a `uses_password?`
method (and using the name `password?` breaks some of Devise's
validation code).
2024-04-07 08:12:38 -07:00
88a2688ac8 Add form to disconnect NeoPass
Can't connect it back yet! But you can disconnect it! :3
2024-04-07 07:52:23 -07:00
21b967f83d Add some NeoPass info to the Settings page, if you have one
No buttons to change it or anything, or to link if you don't! Just a
basic display and explanation!
2024-04-07 07:17:33 -07:00
d5c3bc087e Track neopass_email when logging in with NeoPass
Gonna use this in the Settings UI to communicate what NeoPass you're
connected to!
2024-04-07 07:17:07 -07:00
bd4b67316c Refactor image hash loading to use PetService.getPet, not CPN redirs
Previously, the way we loaded the image hash for a given pet was to
navigate to `https://pets.neopets.com/cpn/<pet_name>/1/1.png`, but
*not* follow the redirect, and extract the image hash from the URL
where it redirected us to.

In this change, we refactor to use the AMFPHP RPC `PetService.getPet`
instead. I don't think it had this data last time I looked at it, but
now it does! Much prefer to use an actual RPC than our weird hacky
thing!

(We might also be able to use this call for other stuff, like
auto-labeling gender & mood for pet states, maybe?? That's in this data
too! We used to load petlookups for this, long long ago, before the
petlookup captchas got added.)
2024-04-06 02:56:40 -07:00
1d3aac436b Fix detecting "pet not found" case
Oh huh, now instead of returning an AMF error message, the service
returns a blank pet object if the pet doesn't exist. Okay!
2024-04-06 02:49:08 -07:00
ebc01518bd Remove unused Pet::WARDROBE_PATH constant
Huh, weird! Goodbye!
2024-04-06 02:38:20 -07:00
848e71f16d Remove unused Pet.from_viewer_data constructor
I guess this was like, we had some call site that was handling loading
the viewer data itself, and didn't want to have to reload it?

But whatever, not used now, let's simplify! We can rebuild this easily
if we need it again.
2024-04-06 02:33:28 -07:00
f0ac2adc78 Remove unused options when loading pets
Locale is the big one that's not really relevant anymore (I don't want
to be loading non-English item names anymore, now that we've simplified
to only support English like TNT has!), but there was also `item_scope`
and stuff.

The timeout option is technically not used in any call sites, but I
think that one's useful to leave around; timeout stuff is important,
and I don't want to rewrite it sometime if we need it again!
2024-04-06 02:31:24 -07:00
57dcc88b27 Refactor pet image hash loading into the Pet model, not PetType
Just a small thing, I guess when I was a kid I did a weird thing where
I attached `origin_pet` to `PetType`, then upon saving `PetType` I
loaded the image hash for the pet to save as the pet type's new image
hash.

I guess this does have the nice property of not bothering to load that
stuff until we need it? But whatever, I'm moving this into `Pet` both
to simplify the relationship between the models, and to prepare for
another potential refactor: using `PetService.getPet` for this instead!
2024-04-06 02:25:22 -07:00
6618651fcb Use completely random NeoPass usernames for now
Ahh, I had assumed the `uid` provided by NeoPass would be the user's
Neopets username, but in hindsight that was never gonna work out since
NeoPass doesn't think of things in terms of usernames at all!

For now, we create 100% random NeoPass usernames, of the form
"neopass-shoyru-5812" or similar. This will be an important fallback
anyway, because it's possible to have a NeoPass with *no* Neopets.com
account attached.

But hopefully we'll be able to work with TNT to request the user's main
Neopets account's username somehow, to use that as the default when
possible!
2024-04-01 05:57:06 -07:00
b03d9b264a Increase maximum username length to 30
I'm writing some code for default NeoPass usernames, and they can get
kinda long, so I want to clear some extra space for them!
2024-04-01 05:53:38 -07:00
7f4c34ff6a Oops, stop requiring a new password whenever AuthUser is changed
Ah right, I went and checked the Devise source code, and the default
implementation for `password_required?` is a bit trickier than I
expected:

```ruby
def password_required?
  !persisted? || !password.nil? || !password_confirmation.nil?
end
```

Looks like `super` does a good enough job here, though! (I'm actually
kinda surprised, I wasn't sure how Ruby's `super` rules worked, and
this isn't a subclass thing—or maybe it is, maybe the `devise` method
adds a mixin? Idk! But it does what I expect, so, great!)

So now, we require the password if 1) Devise doesn't see a UI reason
not to, *and* 2) the user isn't using OmniAuth (i.e. NeoPass).

This had caused a bug where it was impossible to use the Settings page
*without* changing your password! (The form says it's okay to leave it
blank, which stopped being true! But now it's fixed!)
2024-03-14 19:20:33 -07:00
3eeb5d1065 Actually create user from NeoPass authentication! <3 <3
Whew, exciting! Still done nothing against the live NeoPass server, but
we've got this fully working with the development server, it seems!
Wowie!!

This is all still hidden behind secret flags, so it's fine to deploy
live. (And it's not actually a problem if someone gets past to the
endpoints behind it, because we haven't actually set up real
credentials for our NeoPass client yet, so authentication will fail!)

Okay time to lie down lol.
2024-03-14 19:11:06 -07:00
f483722af4 NeoPass strategy interacts with dev NeoPass server, which is still WIP
In this change, we wire up a new NeoPass OAuth2 strategy for OmniAuth,
and hook up the "Log in with NeoPass" button to use it!

The authentication currently fails with `invalid_credentials`, and
shows the `owo` response we hardcoded into the NeoPass server's token
response. We need to finally follow up on the little `TODO` written in
there!
2024-03-14 16:13:31 -07:00
77057fe6a2 Add hidden "Log in with NeoPass" button, to placeholder login strategy
If you pass `?neopass=1` (or a secret value in production), you can see
the "Log in with NeoPass" button, which currently takes you to
OmniAuth's "developer" login page, where you can specify a name and
email and be redirected back. (All placeholder UI!)

We're gonna strip the whole developer strategy out pretty fast and
replace it with one that uses our NeoPass test server. This is just me
checking my understanding of the wiring!
2024-03-14 15:34:24 -07:00
08b1b9e83b Add OmniAuth plugin to AuthUser
This is setting us up for NeoPass, but first we're just gonna try stuff
with the "developer" strategy that's built in for testing, rather than
using the NeoPass dev server!
2024-03-14 15:06:13 -07:00
cf6921329d Oops, choose the *first* PNG from the manifest, to avoid reference art
When we moved more logic into the main app, we made some assumptions
about manifest art that were different than Impress 2020's, in hopes
that they would be More Correct for potential future edge cases.

Turns out, they were actually *less* correct for *current* edge cases!
Chips linked us to a few examples, including this Reddit post:

https://www.reddit.com/r/neopets/comments/1b8fd72/i_dont_think_thats_the_correct_image/

Fixed now!
2024-03-07 14:16:05 -08:00
522287ed53 Fix MissingAttributeError in ClosetHanger#merge_quantities
Oh rough, when moving an item list entry from one list to another, our
logic to merge their quantities if it's already in that list was just
fully crashing!

That is, moves without anything to merge were working, but moves that
required a merge were raising Internal Server Error 500, because the
`list_id` attribute wasn't present.

I'm not sure why this ever worked, I'm assuming using `list_id` in the
`where` condition would include it in the `select` implicitly in a
previous version of Rails? Or maybe Rails used to have fallback
behavior to run a second query, instead of raising
`MissingAttributeError` like it does now?

Well, in any case, this seems to fix it! Whew!
2024-02-28 13:30:55 -08:00
87782767f8 Filter search in wardrobe-2020 by alt styles!
Yay, we finally added it, the part where we include the appearance data
for the items based on both the species/color and the alt style! Now,
switching to Faerie Acara correctly filters the search only to items
that would fit (I think literally just only body_id=0 items right now,
but we're not banking on that!)
2024-02-27 16:11:06 -08:00
421f2ce39f Use fits:nostalgic-faerie-draik filter format when we can
This only *really* shows up right now in the case where you construct
an Advanced Search form query (which only the wardrobe-2020 app does
now, and in limited form), and we return the query back (which only
gets used by the HTML view for item search, which doesn't have any way
to build one of these requests against it).

This is because, if you just type in `fits:alt-style-87305`, we always
keep your search string the same when outputting it back to you, to
avoid the weirdness of canonicalizing it and changing it up on you in
surprising ways!

But idk, this is just looking forward a bit, and keeping the system's
semantics in place. I hope someday we can bring robust text filter
and Advanced Search stuff back into the main app again, maybe!
2024-02-27 15:51:27 -08:00
18c7a34b8f Update series_name for alt styles to be null, with a fallback string
I considered this at first, but decided to keep it simple until it
turned out to matter. Oops, it already matters, lol!

I want the item search code to be able to easily tell if the series
name is real or a placeholder, so we can decide whether to build the
filter text in `fits:$series-$color-$species` form or
`fits:alt-style-$id` form.

So in this change, we keep it that `AltStyle#series_name` returns the
placeholder string if none is set, but callers can explicitly ask
whether it's a real series name or not. Will use this in our next
change!
2024-02-27 15:48:28 -08:00
9243193bc8 Add support for fits:alt-style-87305 item searches
Easy peasy!
2024-02-27 15:36:45 -08:00