1
0
Fork 0
forked from OpenNeo/impress

Compare commits

...

227 commits

Author SHA1 Message Date
c32a495780 Announce the temporary server upgrade (and refactor announcement helper) 2024-12-01 19:26:42 -08:00
ea5c315c2a Oops, fix typo in "Glitched?" label in pet appearance edit form 2024-12-01 11:27:18 -08:00
ab238ab2a6 Add "Support summary" section to Rainbow Pool
This both gives us a sense of progress on labeling, and adds a link to
get started in bulk labeling mode!
2024-12-01 11:13:21 -08:00
0d2648d030 Move support_staff? method into ApplicationController
I want to use it in a controller in our next change!
2024-12-01 11:12:24 -08:00
d9bf4f745b Skip glitched appearances in bulk-labeling mode
It's not as important to label glitched states, and sometimes the glitch
prevents it from being visually identifiable. Don't sweat 'em!
2024-12-01 10:39:19 -08:00
407c4b38a5 Add link to reference pet type when labeling pet appearances
Sometimes I forget like, what the masc/fem variants of a given pet
actually look like? Some are super obvious about things like eyelashes,
and others use more subtle eye differences.

This is a cheap lil hack to make it easier to open a reference! Ideally
I think it would be neat to like, when you hover over an option, have
it show you the reference variant of that pose? But this is good enough
I think!
2024-12-01 10:28:58 -08:00
6dc5aa28a4 When labeling pet appearances on mobile, give pose options equal height
If the screen is narrow, many of the bubbles will wrap their text onto
two lines, but "Unconverted" won't. Give it equal height to the rest
anyway, for visual consistency!
2024-12-01 10:27:38 -08:00
b656ccd982 Add bulk labeling mode for pet appearances
We copy the same feature from alt styles, now that the UI is shared via
support form helpers! Easy peasy!

This adds a "Then: Go to unlabeled appearance" checkbox next to the
submit button on the pet appearance edit form. If checked, it takes you
to the first unlabeled appearance in the database, and keeps the box
checked for next time. Slam through 'em!
2024-12-01 10:10:33 -08:00
02836494ae Add debug gem in development
This helped me debug a thing in the upcoming change! It lets you drop a
`debugger` line into the app, then run `rdbg --attach` in another
terminal to get into a debug session. Neat!
2024-12-01 10:05:54 -08:00
b6e6f27fdf Minor refactor to support_form_with implementation
Realizing that, with the keyword argument spread syntax, I don't need
to do merging, I can just. spread at the right place!

My rationale for the ordering here is: if the caller theoretically tried
to override the builder (even though I don't see why), I think we would
want to respect that. Whereas the `class` argument should be overridden
because we're safely *merging* our `.support-form` class into it.
2024-12-01 09:45:26 -08:00
aeb00f73cf Extract alt style's "go to next" field into a support form helper
I want to reuse this for unlabeled pet styles is why! (That's been the
immediate motivation for this refactor, but also I do just like that
it'll make support forms easier to build.)
2024-12-01 09:42:19 -08:00
06a301e6d7 Add actions helper to support form builder 2024-12-01 09:30:17 -08:00
1119bbb292 Extract the more complex support form helpers into templates
I think helpers are fine for the simpler ones that are basically *just*
wrapper tags, but once it starts getting into `concat`, I think that's
too unfamiliar of a syntax for developers; let's bail into our usual
templating system!

I'm not sure about putting them in `application/support_form` like this.
That's cute for one-offs like `application/hanger_spinner`, because
`render partial: "hanger_spinner"` assumes the `application` view folder
by default, but that doesn't work once it's nested: it looks for a
`views/support_form` folder.

I think maybe it could soon be time to bail from the strict "view
folders belong to controllers" thing, similar to how we did for
`SupportFormHelper`, and add a `components` folder or similar? Idk, not
sure yet!
2024-12-01 09:26:40 -08:00
fdbfa3c03f Fix styles in support form when field is errored
Ah right, `> label` doesn't work with how Rails will wrap broken labels
and inputs each in a `.field_with_errors` element. Fixed, and added
some basic coloring!
2024-11-30 11:50:31 -08:00
252f4f1df1 Add errors helper to support form builder
It still has no good CSS to it, but that's okay, this is just to DRY it
up.
2024-11-30 11:46:19 -08:00
2d3d4051fe Oops, return HTTP Bad Request when item editing fails
Just a subtle thing, but Turbo can be picky about return types, and
won't reload the page with the errors in it if the status is 200.
2024-11-30 11:45:35 -08:00
3cd02baa09 Add thumbnail_input method to support form builder
Just to clean up this relatively common input type!
2024-11-30 11:34:02 -08:00
8347633a84 Add SupportFormBuilder to make the support form templates nicer
Instead of hand-rolling HTML, this offers helpers like `f.field`, to
help ensure the HTML is consistent, and to keep the templates more
focused on the unique form elements.
2024-11-30 11:26:23 -08:00
661a5385f4 Migrate pet state edit form to .support-form class
Most notable change here is extracting the pose option bubbles into a
`data-type="radio-grid"`, and pulling that into the `.support-form`
CSS. My rationale is that, unlike most fields, this field benefits from
being 100%-width, and I don't want to specify that as an override if I
can avoid it, because that's fragile-y.

Instead, I extract this into a generic type of field that
`.support-form` can use (it feels pretty reusable anyway!), and require
the caller to specify how many columns they want as `--num-columns`.
2024-11-30 10:49:47 -08:00
c27477fabe Refactor support-form CSS to be more reusable layout
Specifically, I'm going for a more-vertical layout, cuz I want to bring
PetState over to it, and the weird grid situation wasn't gonna fit the
big pose label radios.
2024-11-30 10:33:58 -08:00
c7bea666c9 Add the "body fit" field to the item editing page 2024-11-20 12:23:30 -08:00
f49f9f386d Add item editing fields to manually override an item to be an NC item 2024-11-20 12:13:33 -08:00
3937ba354f Add edit page for items, to set modeling status for done/glitchy items 2024-11-20 12:07:25 -08:00
388bb9a251 Oops, fix mistakes when extracting support-form class
I didn't include the alt styles call site when making the commit, and
also I named the file differently than other nearby files are named!
2024-11-20 11:24:56 -08:00
e846a75f7a Use modeling hints to remove some items from is:modeling searches
There's still plenty left, but we have 213 we "manually" marked as
"done" (I think I ran a batch job on everything Chips told me was on
the page and already done), and that should help a lot!
2024-11-20 11:22:33 -08:00
270b27c1d2 Extract alt style form CSS into a new "support-form" class
Gonna use this for item editing too, I think!
2024-11-20 11:16:46 -08:00
4cbac13df1 Remove careful SQL-selecting on homepage
This keeps causing missing-attribute crashes when I change things, and
I don't think the performance benefit is a big deal for how the page
currently runs, esp as we keep gathering more attributes? I feel like
`description` is the main "large" one we're omitting, and like. Shrug!
2024-11-20 10:44:33 -08:00
0261d02137 Downgrade most item validations to be *not nil*, rather than present
Been running into the item "Hanging Plushie Grundo Background Item" not
being modelable, because TNT seem to have left its description blank!

Let's be less picky about what data we take in, but keep the intention
of these validations: to ensure that *we* don't make a mistake and
forget a field when importing items!
2024-11-19 17:00:47 -08:00
e82c606ee8 Ah beans, fix a homepage crash from the modeling logic changes
Tbh I'm not sure how much this select clause is helping us, but w/e.
2024-11-19 16:49:20 -08:00
ed5b62e161 Use PetType's created_at to predict who an item might be compatible with
This is a basic attempt at the Vandagyre logic, but also things like
"Maraquan items released before the Maraquan X was released"!

I also added a new task, `rails items:update_cached_fields`, which needs
to be run after this change, because it affects the value of
`Item#predicted_fully_modeled?`.

Eyeballing the updated search results for `-is:modeled`, this feels
pretty close? I'm guessing it's not perfect (e.g. maybe a pet type we
got modeled late into its existence, or some items that just never did
fit a certain pet), but feels pretty good.

I also know we had the "modeling hints" override in Impress 2020, which
we aren't reading yet. We should probably take that into account here
too!
2024-11-19 16:41:50 -08:00
5472ccebef Add is:modeled query to items 2024-11-19 15:54:55 -08:00
f6f618c9d5 Add Item.is{_not}_modeled scopes, for use in search later
We're now caching `predicted_fully_modeled?` on the database record, so
we can query by it in the database!

I'm moving on from the model I did in Impress 2020, of writing really
big fancy single-source-of-truth queries based on the assets themselves.

I see the merit of that in terms of theoretical reliability, but in
practice I think it will be *more* reliable to have one *in-code*
definition of modeling status (which we need anyway for generating the
homepage modeling requests), and just save that in a queryable way.
2024-11-19 15:52:52 -08:00
39bed6b157 Make Item's update_cached_fields callback more reliable
In our tests, I discovered an unexpected behavior where calling
`item.swf_assets << swf_asset` wasn't updating computed fields
correctly.

This isn't something we actually do in-app, I think the modeling system
happened to trigger the callbacks in a way that still worked fine?

But I think this is a good idea for reliability, since caching is such
a notoriously difficult thing to get right anyway! And it makes our
tests simpler and clearer.

Specifically, `compatible_body_ids` references `swf_assets`, which, I'm
kinda surprised, *doesn't* include the newly-added asset yet when the
`ParentSwfAssetRelationship.after_save` hook runs while calling
`item.swf_assets << swf_asset`. Reloading it fixes this!
2024-11-19 14:26:06 -08:00
af5187edb6 Refactor item modeling spec, in anticipation of Item.is_modeled tests
I'm grouping some shared behaviors to pull into the different cases, so
that we can check the behaviors of a fully-modeled item vs a
not-fully-modeled item in *all* of the relevant cases.

Specifically, I'm planning to add `is:modeled` search filters, and
creating pending placeholder tests for them!
2024-11-19 13:44:31 -08:00
21eaf7b266 Fix silly variable scoping issue in item spec
Oh right, `@remote_id` is an instance variable so we can auto-increment
it over time, but `url` is just a derived value, and can just be local!
Silly!
2024-11-19 13:32:47 -08:00
91851bc340 Add tests for Maraquan item modeling predictions
I also added a test I forgot for the standard case: when you've modeled
each individual species.
2024-11-19 13:31:34 -08:00
3e7d27eaa3 Add a reminder to write modeling prediction tests for special colors 2024-11-19 12:18:17 -08:00
f7109e398a Better handle modeling predictions for items with *no* data
This doesn't generally happen, but did the other day when I rolled back
some of the database's SWF asset records but kept the items—and it was
a bit confusing that the homepage marked them as fully modeled!
2024-11-19 12:15:21 -08:00
f90380c4e6 Add tests for item modeling predictions
The stuff on the homepage! I'm also thinking about how to use these for
better discovery of items that need more modeling.
2024-11-19 12:10:49 -08:00
218dc5b6f9 Improve Solargraph LSP in our spec files
The main thing is that I was getting "RequireNotFound" warnings for
`require 'rails_helper'`, because the LSP seems unaware of how RSpec
offers `spec/` as a root for requires.

I think the `require_relative` is clearer anyway, I'm decently
satisfied with it. And if I decide it's too much ugly, we can try
something else in the Solargraph config or something sometime!
2024-11-19 11:28:36 -08:00
bc0097850d Say "NC Style" instead of "Alt Style" in contribution descriptions
Just for increased consistency!
2024-11-16 12:21:43 -08:00
ec0b8d9cb9 Only prompt for Neologin cookie once when running rails neopets:import 2024-11-16 12:11:13 -08:00
a57b3629db Refactor Neopets import tasks all into a neopets:import namespace
and with a `rails neopets:import` task you can call to do them all at
once!

I'm gonna do some other stuff here too to make `neopets:import` easier
to call all in one go, like prompting for the Neologin cookie just
once at the start.

Note that this changes the cron setup, so you gotta run
`bin/deploy:setup` after this deploys!
2024-11-16 11:58:43 -08:00
1d1dc15320 Remove some more incorrect limits on ID fields in the database
I'm running into this with the automated tests and the fixtures I think
sometimes using large auto-generated IDs?

But the point is, our tables generally use Rails's default `:integer`
size for its IDs, and then columns that reference them are *smaller*,
which is… not correct stuff, y'know?

So I figure, let's just expand the columns. We don't have enough data
that being real picky about the integer sizes matters, so let's keep it
simple and more obviously correct.
2024-11-15 20:39:38 -08:00
b6c21dfe40 Oops, fix sort order for alt styles
Oh huh, when doing Rainbow Pool stuff, I put the ordering in the wrong
place! It's a sensible ordering for the Rainbow Pool page, but not so
much for the JSON view!
2024-11-15 20:28:38 -08:00
c4a7e7916f Fall back to blank image if alt style has no preview image
This is currently crashing the Rainbow Pool when the Anniversary Techo
would appear, because the asset seems to be missing? The SWF doesn't
seem to exist, nor does its manifest.
2024-11-15 20:04:45 -08:00
217d25edab Handle new colors/species in the Rainbow Pool
Oh right, yeah, we like to do things gracefully around here when
there's no corresponding color/species record yet!

Paying more attention to this, I'm thinking like… it could be a cool
idea to, in modeling, *create* the new color/species record, and just
not have all the attributes filled in yet? Especially now that we're
less dependent on attributes like `standard` to be set for correct
functioning.

But for now, we follow the same strategy we do elsewhere in the app: a
pet type can have `color_id` and `species_id` that don't correspond to
a real record, and we cover over that smoothly.
2024-11-15 19:56:07 -08:00
dd213e8078 Increase the maximum value for pet types' color ID and species ID
Oh dang, we're on color #120 now, and looks like our maximum value is
127. Let's expand that!

I noticed this because I'm writing tests for some stuff, and used "456"
as a placeholder ID number, and it just fully did not work, and I'm
like. Oh.
2024-11-15 19:29:13 -08:00
c5995a2bd1 Oops, turn modeling back on
Huh, I guess when I reapplied my refactors to modeling disabling the
other day, I didn't notice that I turned it off in production. And I
guess I didn't deploy this at the time cuz it's just refactors, but
when I deployed other changes yesterday this came with it. Whoops!
2024-11-15 17:37:54 -08:00
1ad3ea8f96 Add rails alt_styles:import task to import info from Styling Studio
Yay whew! Magic time!
2024-11-14 19:03:44 -08:00
b245690a60 Oops, fix bug when loading species with no alt styles in the Studio 2024-11-14 19:03:06 -08:00
3ed1c46b64 Add NCMall.load_styles method, not yet used
I only now thought through that I can scrape these instead of enter
them manually, similar to how we did our Rainbow Pool scraper… hooray!

I'm actually writing tests for stuff too, wowie!
2024-11-14 18:45:08 -08:00
9e3ce74ed5 Auto-focus series name when labeling alt styles
Helpful when doing a bunch in a row (like today's big release!)
2024-11-14 17:14:03 -08:00
5f31e38428 Reapply "Extract modeling ViewerData class into new Pet::ModelingSnapshot file"
This change was modified a bit after cherry-picking, to no longer
include the broken changes to item modeling in 9eaee4a.

(cherry picked from commit 90407403ba)
2024-11-10 11:43:54 -08:00
8f9daf4d52 Reapply "Use our IntegerSet serializer for PetState#swf_asset_ids"
(cherry picked from commit 242b85470d)
2024-11-10 11:41:06 -08:00
3242981eb2 Reapply changes to how disabling modeling works
```shell
git cherry-pick d82c7f817a --no-commit
```
2024-11-10 11:39:51 -08:00
54b25ef08e Reintroduce some of our modeling refactors, without touching items
Okay so, when we reverted a buncha stuff in e3d196f, it was in response
to a bug where item modeling data was getting deleted. And I was tired,
and just took a big simple hammer to it of reverting all the modeling
refactors.

Here, we reintroduce *some* of them: the biology ones before the item
bug. And tests still pass, and in fact I can un-pending some of them!

I might also try to reapply the change where we extract it all into a
new file, but without the item parts.

```shell
git cherry-pick --no-commit 13ceec8fcc
git cherry-pick --no-commit f81415d327
git cherry-pick --no-commit c03e7446e3
git cherry-pick --no-commit 52ca41dbff
```
2024-11-10 11:36:23 -08:00
e4e81f0694 Update modeling bug announcement, now that things are working again
Also, while we're here! To restore the lost data, I:

1. Downloaded this scheduled public data backup, which was taken
   thankfully the day before we updated modeling code!
   https://impress.openneo.net/public-data/2024-11-03T08_15_02Z-scheduled.sql.gz
2. Trimmed it just to the section about the `parents_swf_assets` table:
   dropping it, then rebuilding it from scratch.
3. Ran this modified backup SQL dump on the production server.
4. Ran the code from `db/migrate/20241001052510_add_cached_fields_to_items.rb`
   to bring items' cached fields back into the correct state.

I also had to fix some errors in the item data that prevented some
items from passing the latest validations:

```rb
Item.where(rarity: "").update_all(rarity: "???")
Item.where(description: "").update_all(description: "???")
Item.where(zones_restrict: "").update_all(zones_restrict: "00000
00000000000000000000000000000000000000000000000")
```
2024-11-06 14:34:15 -08:00
e3d196fe87 Revert modeling refactors to the old modeling that worked!
Because we ended up with such a big error, and it doesn't have an easy
fix, I'm wrapping up today by reverting the entire set of refactors
we've done lately, so modeling in production can continue while we
improve this code further over time.

I generated this commit by hand-picking the refactor-y commits
recently, running `git revert --no-commit <hash>` in reverse order,
then manually updating `pet_spec.rb` to reflect the state of the code:
passing the most important behavioral tests, but no longer passing one
of the kinds of annoyances I *did* fix in the new code.

```shell
git revert --no-commit 48c1a58df9
git revert --no-commit 42e7eabdd8
git revert --no-commit d82c7f817a
git revert --no-commit 5264947608
git revert --no-commit 90407403ba
git revert --no-commit 242b85470d
git revert --no-commit 9eaee4a2d4
git revert --no-commit 52ca41dbff
git revert --no-commit c03e7446e3
git revert --no-commit f81415d327
git revert --no-commit 13ceec8fcc
```
2024-11-06 14:31:16 -08:00
0b3dd02323 Add failing test for modeling bug where we break existing connections
As I'm writing out my solution for this, I'm almost wondering if it's
time for the refactor I've been Theoretically Planning Someday, to move
items to a real `ItemAppearance` model in the database similar to
`PetState`… Hmm hmm hmm…

For now though, I'm taking a break!
2024-11-06 14:08:32 -08:00
48c1a58df9 Fix new bug where re-modeling a background would reset it from ID 0
This bug never made it into production I think, it was a consequence of
some of how I refactored stuff in the recent changes? I think??

But yeah, I refactor how we manage `SwfAsset#body_id`, to be a bit more
explicit about when and how it can change, instead of the weird
callbacks that tbqh have bit us too often…
2024-11-06 13:48:01 -08:00
42e7eabdd8 Fix modeling bug where compatible_body_ids field was not updating
Ah right, the callbacks in `ParentSwfAssetRelationship` don't get
called when Rails does automatic join-model management stuff. We need
the `Item` to call its `update_cached_fields` callback itself, too!

When fixing this, I found a new bug that arose, in how we infer
`body_id` for assets that fit all pets. Fixing that next!
2024-11-06 13:39:32 -08:00
a208fca8d2 Improve modeling tests for records that shouldn't change
This gives better output when they fail, and also avoids spurious
failures like when an array for `cached_compatible_body_ids` is replaced
by an identical one! (I'm running into this right now, and yeah, it
helps a lot lol)
2024-11-06 13:04:50 -08:00
3ac89e830e Announcement about modeling being broken 2024-11-06 11:55:05 -08:00
d82c7f817a Disable modeling in production, while we investigate errors
Hmm, I think I made a mistake on `modeling_snapshot.rb:69`: I'm
assigning the *entire* `item.swf_assets` relation to *just* the assets
for the new model of it, which breaks all the other connections.

First, I'm disabling modeling. Then, I'll restore a backup. Then, I'll
write tests for that case, and fix it up!
2024-11-06 11:54:28 -08:00
5264947608 Minor tweaks to modeling private methods 2024-11-03 12:24:54 -08:00
90407403ba Extract modeling ViewerData class into new Pet::ModelingSnapshot file
Both extracted and renamed!
2024-11-03 12:23:51 -08:00
242b85470d Use our IntegerSet serializer for PetState#swf_asset_ids 2024-11-03 12:16:27 -08:00
43717e2535 Remove unused PetState#reassign_duplicates! 2024-11-03 12:07:57 -08:00
bc1f7152bf Remove unused SwfAsset.from_wardrobe_link_params 2024-11-03 12:07:23 -08:00
9eaee4a2d4 Refactor item modeling
Simpler, more encapsulated, and fixes the pending jank stuff in the
tests!
2024-11-03 12:05:37 -08:00
52ca41dbff Extract biology processing from AltStyle into Pet
I'm trying to pull more of the modeling code out of the individual
classes, and into this encapsulated preprocessing, so it's a lot more
in-one-place!
2024-11-03 11:46:29 -08:00
c03e7446e3 Refactor out biology assets in modeling code a bit 2024-11-03 11:41:18 -08:00
6402e5abc3 Remove the pending marker on some modeling tests 2024-11-02 21:39:34 -07:00
f81415d327 Refactor modeling viewer data handling into a new ViewerData class 2024-11-02 21:34:19 -07:00
13ceec8fcc Simplify modeling code for biology data
We're leaning more into Rails collection management and autosave stuff!
2024-11-02 21:15:12 -07:00
40765c729e Remove unused Pet.with_pet_type_color_ids scope 2024-11-02 20:21:59 -07:00
d26f3a7598 Add tests for modeling alt style assets 2024-11-02 20:15:35 -07:00
06721f77e9 Add alt style modeling tests
I didn't do the assets part of it yet, just the style object!
2024-10-29 22:20:43 -07:00
f9be3dceb1 Oh right, use Blue Jetsam "original biology" for modded Majal_Kita 2024-10-26 14:32:47 -07:00
c9c080e74d Merge branch 'main' into modeling-tests 2024-10-25 17:33:39 -07:00
e65634d8bc Set up tests for Majal_Kita and its alt style
Not actually touching alt style yet, just the very basic stuff about
how alt style can cause loading to fail in certain extremely rare cases
(specifically, if it's our first time seeing the underlying
color/species combo too, which… isn't gonna happen irl on DTI for a long
time if ever, I would guess, but hey!)
2024-10-25 14:47:51 -07:00
4c5d14c591 Add tests for re-modeling Thyassa, in a different mood 2024-10-25 14:15:25 -07:00
28bd6ecca4 Test that modeling items twice doesn't affect them or their assets 2024-10-25 13:47:23 -07:00
7a837edaf6 Use alias they in specs instead of it, when it reads better 2024-10-25 13:38:02 -07:00
f3894759d6 Add tests for modeling item assets 2024-10-25 13:34:02 -07:00
30ada0b7e1 For biology assets, make clearer the weirdness with saving first 2024-10-25 13:04:46 -07:00
8a38ce90dc Refactor tests for saving things when pet is saved
This got more complicated with the items case, and I think this is a
stronger idiom for communicating it!
2024-10-25 12:55:31 -07:00
6d25b3424f Add tests for matts_bat's items
Not their assets yet, just the items
2024-10-24 15:58:16 -07:00
8902527438 Basic modeling spec for my pet, matts_bat
No items yet, just putting in the very basics of biology, more
shortform than the Thyassa case
2024-10-24 15:37:57 -07:00
044dface14 Add RSpec to the commit hook 2024-10-24 15:22:39 -07:00
b1890d4f6f Refactor existing modeling tests to follow more RSpec-y syntax
I'm learning how to group things and stuff!
2024-10-24 15:16:55 -07:00
3a5f33fd56 Add tests that modeling a pet a second time doesn't affect its records
That is, if everything is the same as before, we don't need to change
anything in our database!

I also learned a bit more about RSpec syntax sugars, it's cute!
2024-10-24 15:04:25 -07:00
a54a844e03 Refactor modeling spec to use a new "a_record_matching" matcher 2024-10-24 14:42:05 -07:00
c78d45a0b5 Copy edit on pet styles
The NC Pet Styles sentence getting broken across two lines I think
makes it too hard to notice.

Design-wise, it would be nice to just call better attention to this
feature altogether in some higher-level design-language-y way, but!
Whatever!
2024-10-22 16:48:37 -07:00
930bfca028 Add creation date to alt styles listings
Copied from how pet types do it!
2024-10-22 16:46:55 -07:00
29aa769bda Oops, tiny copy edit for alt styles page
Right, missed this when renaming them, lol!
2024-10-22 16:44:42 -07:00
66438eae1a Add mode to keep labeled alt styles until they're all labeled
If you check this box, it'll keep you in a mode where saving an alt
style redirects you to the *next* one that needs labeling, until
they're all done. Useful for big drops!
2024-10-22 16:39:25 -07:00
3b5b13c172 Fewer slowest examples in RSpec, cuz these are fast and it's fine
I want to not turn it off entirely, so that if there's a nasty one it
becomes visible, but we don't need all that vertical space for this
small test suite rn!
2024-10-21 16:47:51 -07:00
5b1d1f0695 Add assets to modeling tests, and also uhh some other fixes
I forget, there was some tricky debugging about getting the fixtures
right, I think the previous commit doesn't *actually* pass from a clean
setting. Ah well, looks good now!
2024-10-21 16:46:10 -07:00
e92e315743 Move modeling tests to RSpec
Also note the jsbundling bump, that was so I can us the relatively-new
`SKIP_YARN_INSTALL=1` flag when running tests, to be a bit faster.
2024-10-21 16:03:58 -07:00
eb2fb125b9 Basic modeling test!
Just getting a basic foothold here. I'm thinking about moving this to
RSpec, cuz I feel like the assertions are gonna get pretty specific
and groupable.
2024-10-21 15:29:22 -07:00
d8ff99475e Add the Rails tests to the pre-commit hook
I'm gonna work on adding modeling tests, and I want to not be breaking
them without realizing! The trade history ones are good to be checking
more often like this, too.
2024-10-21 14:35:26 -07:00
9726ecb1a5 Fix trade activity tests: use a valid item fixture, not a placeholder
In 540ce08caa, I updated the Item class
to be more explicit about what fields are required, so this test would
fail in a more helpful way, instead of just crashing from `name` being
`nil` when trying to infer the Dyeworks info.

Now, we update the test to use Rails's standard "fixture" system to set
up a more-correct placeholder item, instead!
2024-10-21 14:26:36 -07:00
540ce08caa Handle invalid Item state a bit better
Catch missing fields in validation before sending it to the DB, and
skip the Dyeworks stuff if the name is missing.

I ran into this looking into `test/trade_activity_test.rb`, which fails
right now because we try to create a boring placeholder item with
minimal fields, which Dyeworks can't call `name.match()` on!

Now, the test fails with a more helpful error about the item being
invalid. Next, I'll fix that!
2024-10-21 14:24:45 -07:00
881e63cfbd Output JSON from rails pets:load[pet_name]
Gonna use this for making mock data for automatic testing!
2024-10-21 14:03:56 -07:00
09e5a39b4c Whoops, fix alt styles when modeling real pets wearing them
Just never did this, I guess!! 😅
2024-10-18 19:16:41 -07:00
bf20c9bb31 Ah beans, I goofed alt style modeling *again*
Feeling for-real about getting a test suite set up because oh my god
2024-10-18 19:01:26 -07:00
7607c2c015 Oops, fix sloppiness about pet service refactor
I guess I like super didn't test this end-to-end, oops!!
2024-10-18 18:14:01 -07:00
abfe1e6df7 Extract neopets_origin into a config value 2024-10-18 18:00:48 -07:00
e36e273d50 Extract Neopets::CustomPets service from the Pet class
Just getting this stuff out of Pet, in part because I want to start
being able to unit test modeling, and that will require stubbing out
what this service returns!
2024-10-18 17:40:31 -07:00
83e5ad6bcc Update alt styles copy to adjust for them not all being Nostalgic now 2024-10-18 17:29:48 -07:00
acb52cb870 Move NCMall and NeoPass services into a Neopets module
Just a bit more clarity of grouping! I'm also thinking about extracting
modeling APIs into a service file like this too, in which case I think
this would help clarify what it is.
2024-10-18 17:27:15 -07:00
7ef689d658 Remove unused ostruct import
Only noticed it cuz there's a deprecation warning, and so I was like,
do we use it? I think not anymore!
2024-10-18 17:20:02 -07:00
23c083ff1d Use "real" series name field when editing alt styles
Just a little improvement to the form, so when there's no series name,
the text field is empty—even though in most contexts we *pretend* it's
"<New?">
2024-10-18 17:13:16 -07:00
6b7c73870a Stop inferring AltStyle series name, now that it's getting more varied
They're not all Nostalgic anymore! Oh, how the times have changed!

This way, new ones will appear as "<New?>", until support staff come in
and label them (with our cool new tools!)
2024-10-18 17:07:38 -07:00
e7a0ff1234 Make deleting an AltStyle also delete its ParentSwfAssetRelationships
Not relevant in-app as such, I'm just deleting records to re-test
things in development, and it helps to keep things in a more consistent
state!
2024-10-18 17:06:13 -07:00
50c9ba53e7 Add announcement explaining alt style bugs
so people know it's fixed and can model them now lol
2024-10-18 17:04:44 -07:00
89c729ecbe Oops, fix bug preventing new alt styles from being saved
Whoops, I didn't realize this change I made to validation for the alt
style editing form, was goofing up alt style modeling!

The trick is, the validation was happening before the `before_create`
hook. Now I've reformulated these as `before_validation` hooks, so
we're not rejecting new alt styles for having no thumbnail!
2024-10-18 17:04:26 -07:00
bb83f6fd36 add redirect from /alt-styles to /rainbow-pool/styles
In a recent change, I changed the path for this page—but forgot to
keep the old URL available as a redirect, for older links! Fixed.
2024-10-18 14:25:03 -07:00
7891acd3b1 Oops, missed a spot for Modeling -> Rainbow Pool link updates
Whoops, clicking the *paint brush image* still linked to the Modeling
Hub, instead of the Rainbow Pool. Fixed!
2024-10-12 14:17:49 -07:00
16deee94e4 Remove now-unused pet state show page 2024-10-11 17:48:23 -07:00
2cc0c5b031 Link pet states to outfit editor instead of their not-useful show page
I was considering doing more with the show page at one point, but ehh,
I think the outfit editor is the better place for that stuff anyway.
2024-10-11 17:44:56 -07:00
381a892af8 Add a bit more space around Rainbow Pool filter forms 2024-10-11 17:39:35 -07:00
1a0fb68b1c Add more explanation copy to Rainbow Pool pages 2024-10-11 17:38:34 -07:00
4f9fbc1ac0 Improve pet type "Added" timestamp styles 2024-10-11 16:24:47 -07:00
ad51690617 Sort pet types alphabetically when filtering the Rainbow Pool
The default is latest first, but when you're searching a color or
species, you probably just want a consistent alphabetical order.
2024-10-11 16:24:24 -07:00
5648f55d2c Add date to pet types in Rainbow Pool 2024-10-11 16:00:07 -07:00
6934b636fb Update Pet Styles copy 2024-10-11 15:50:31 -07:00
83fe0d20e0 Add Rainbow Pool breadcrumbs to Pet Styles page 2024-10-11 15:42:19 -07:00
be5ad31a1d Link alt styles to the outfit editor, rather than to the big image URL 2024-10-11 15:40:20 -07:00
1626f0706c Move Alt Styles into /rainbow-pool URLs, say "Pet Styles" more in text 2024-10-11 15:37:37 -07:00
7fad6abfed Add homepage link to Rainbow Pool, and move Modeling Hub to footer 2024-10-11 15:27:36 -07:00
c985c50a1b Add special styles for the placeholder option in select tags
Noticed this for the Rainbow Pool filters!
2024-10-11 15:23:35 -07:00
bba863b94b When filtering to a specific pet type, redirect right to it!
That is, if you filter to "Candy Chia", we just redirect straight to
it, if it exists; rather than showing you a results page.
2024-10-11 15:13:59 -07:00
7c1b3ca447 Add "no results" output for Rainbow Pool filters with no results 2024-10-11 15:13:22 -07:00
71f0aa4908 Oops, fix modeling logic
Oh huh, I guess most of the new items we had when I rewrote this were
Maraquan, and I didn't test enough on standard species-specific items.

Before this change, partially-modeled items for standard pets would
appear as fully modeled, because the presence of the "nonstandard"
color Orange (because of the Orange Chia) meant that the "standard" key
didn't actually have any unique bodies (it was all `["standard", 47]`).

Here, I take my own comments' advice and move away from the standard
label as part of the logic. Instead, we look first for nonstandard
colors with unique bodies, which we'll call out as modelable; and then
check whether there are any basic bodies *not* covered by those special
colors.

That way, compatibility with the Maraquan Acara (a unique body) means
we'll treat Maraquan as a modelable color; and then we'll ignore the
basic bodies, even though it *does* fit the basic Mynci, because there
aren't any compatible basic bodies that aren't *also* Maraquan bodies.

This also means that compatibility with both the Blue Acara and Orange
Acara does *not* preclude a normal item from needing basic pets for
models: because, while Orange is a slightly nonstandard color in the
case of Chia, it doesn't have its own unique body for this item, so we
ignore it when predicting compatibility with basic colors.
2024-10-08 22:46:11 -07:00
13a0362e6d Use PetState#updated_at for the supported pose cache key, not latest ID
This is because labeling poses with the Support tools *should*
invalidate the `PetState.all_supported_poses` cache! But the previous
cache key would only invalidate when a new pet state is *added*, not
when one is *edited*.
2024-10-07 17:56:42 -07:00
fe67211fdf Add created_at/updated_at to PetState
This has just been absent for too long! We've lost a lot of data about
when poses were first modeled, which is a shame.

But I want this in now, because I was just doing caching on
/rainbow-pool.json, and realized that _labeling_ poses is another way
pet states can update rather than just being created!

So we need an `updated_at` field, to be able to quickly detect edits
that require us to invalidate the cache on
`PetState.all_supported_poses`. I'll do that next!
2024-10-07 17:54:54 -07:00
0244653cb0 Add /rainbow-pool.json for all species, colors, and poses
This clocks in a bit bigger than what Impress 2020 does in terms of
binary encoding (with gzip it's at 11K instead of 4K), but I'm okay
with that for the simplicity win.

Gonna try to swap this in for where we're still using Impress 2020 for
the species/color picker in the outfit editor!
2024-10-07 17:38:53 -07:00
2c0d55edd1 Remove unused code related to no-longer-present asset downloads 2024-10-07 17:06:14 -07:00
be0faaa36e Improve top nav support on mobile for responsive design pages
Before this change, pages that opt in with `use_responsive_design`
would often have the top nav be real cluttered for logged-in users. (I
think I happened to first test this responsive design without being
logged in on my dev box, oops!) Because the home link and `#userbar`
were absolutely positioned on the page, they would frequently overlap.

Here, I stop doing our old tricks to make the top nav load last on the
page. (This was to get "main content" loading faster, which I think is
a. not as relevant today with more commonly faster connections, and b.
was a bit naive to think that it'd be helpful to have to wait a long
time to _navigate_ if a page is unexpectedly large.)

These tricks used to leave some padding at the top of the `#container`,
which these elements would then visually fill via `position: absolute`
once they load.

Next, I update the CSS (for the responsive design pages only) to use
the new `#main-nav` container to lay them out in Flexbox: all in one
row if possible, or wrapped if needed.

Some designs hide stuff like this into a hamburger menu or such when
the screen gets small. I haven't done that here! No specific reason,
I'm just not sold that it's that much better, or worth the trouble.

I tested this on the following combinations:
1. Logged out, homepage
2. Logged in, homepage
3. Logged out, `/items`
4. Logged in, `/items`
5. Logged out, `/items/89487-Dyeworks-Purple-Haunted-Sky-Background`
6. Logged in, `/items/89487-Dyeworks-Purple-Haunted-Sky-Background`

Hope it's solid! 🤞
2024-10-05 17:52:38 -07:00
f87f4e61b3 Add extra support info to Rainbow Pool pet types
Easy-to-notice hints for which pet types need more labeling!
2024-10-04 19:24:40 -07:00
dfca88bed3 Oops, use the Rainbow Pool list styles under "Other" show/hide 2024-10-04 18:46:53 -07:00
bd001e643e Oops, avoid scooping up weird Chia bodies in predicted_body_ids
Before this change, a fully-modeled item (Dyeworks Burgundy: Gown of
the Night) was displaying as still needing the Chia. This was because
looking for "standard" body IDs like this caught up some of the weird
Chia bodies.

I think there's probably something here where we need to like, relabel
certain colors? But honestly, the better version of this logic would
probably be to lean more into the `basic` label in this logic.

But hey, that's a refactor for another time. I gotta go eat!
2024-10-03 15:39:35 -07:00
1d51e28144 Post perf upgrades announcement (and job-hunting ask 💖️) 2024-10-03 15:09:38 -07:00
fe4db1b605 Improve prediction for what pets need modeling for an item
Noticing a lot of Maraquan items on the homepage today, and they're
doing that thing of expecting standard body types to be relevant too,
because I think we wrote this logic before the Maraquan Mynci ended up
having the same standard Mynci body? (Maybe? Or maybe I'm being
ahistorical and I just wrote this wrong to begin with lol)

In any case, this is more accurate, and I think I'm also maybe
incidentally noticing that it's running faster, at least in my brief
before/after production testing? (There's *more* queries, like 100! But
many of them are *very* fast lookups, coming in at under 1ms—and also a
lot of them are dupes being served by Rails's request-scoped query
cache.)
2024-10-03 13:49:15 -07:00
860b8eef72 Remove not-very-useful caching for homepage modeling
Huh, I hadn't realized that like, we'd already set up the controller to
always *run* basically all of the modeling logic, and the caching in
the view layer wasn't saving us any queries anymore. Kinda silly!

Remove the caching call, just to simplify the codebase (I like to avoid
caching things that don't specifically need it!).

And hey, love that the modeling code in the controller is now *way*
faster to run! You love to see it!
2024-10-02 18:26:49 -07:00
61e22e3943 Oops, remove no-longer-true comment about a code block I just deleted! 2024-10-02 18:20:22 -07:00
03e4233f67 Use cached compatible body IDs on homepage modeling code
This should make it load way faster! Maybe don't even need to mess with
caching the resulting HTML anymore, like we currently do?
2024-10-02 17:55:20 -07:00
b6bddb14be Oops, fix new bug in homepage modeling code
Missed a spot on `Item#basic_body_ids`!
2024-10-02 17:54:14 -07:00
e52838ba70 Use Rails serialize method to save/load cached fields in Item
Just packing some serialization complexity away into its own thing, so
the model code doesn't need to sweat it!
2024-10-02 17:50:42 -07:00
7ba68c52d4 Simplify homepage modeling code a bit
I have some other changes planned too, but these are some easy ones. I
also turn back on this stuff in development, in hopes that my changes
can make these queries fast enough to not be a big deal anymore!
2024-10-02 17:26:32 -07:00
26add4577c Use cached fields for item searches, instead of big joins
This is the second part of the previous change `efda6d74`, in which we
switch out the item search query conditions!

This was a two-parter to ease deployment: first deploy the change with
the migration, then *run* the migration (because it's an unusually slow
one), then deploy this change that actually uses it.

Another way to approach this would've been to deploy it all in one
commit, but not set it as `current` until we had run the migration.
That would have been a reasonable approach too!
2024-09-30 23:16:03 -07:00
efda6d74ab Add cached fields to Item model for searching, but don't use them yet
This is the first part of a change to improve search performance, by
caching occupied zone IDs and supported body IDs onto the Item record
itself, instead of always doing joins with `SwfAsset`.

It's unfortunate, because part of the power of SQL is joins! But doing
joins with big tables, in ways that can't take advantage of indexes in
the same ways as we often want to, is… slow.

It's possible there's something I'm misunderstanding about SQL
optimization, and this _could_ be done with query optimization or
indexes instead of duplicating data like this? This complexity carries
the risk of data getting out of sync in unforeseen ways. But this is
what I know how to do, and it seems to be working, so! Okay!
2024-09-30 23:10:44 -07:00
4a431a4ae8 Oops, omit the commit field from Rainbow Pool filter URLs
By default, Rails gives this button the name `commit`, so it appears in
the URL the form sends to. By setting the name to `nil`, Rails doesn't
set a `name` attribute on the HTML element, so it's *not* included.
2024-09-30 18:05:05 -07:00
4bcc3aaebb Limit Rainbow Pool filter dropdown size
cuz the "Prismatic Pink: Nostalgic" stuff is gonna get pretty long if
we just do the default behavior of letting it grow to max content size!
2024-09-30 17:42:52 -07:00
5890e52e53 Use full name when showing Alt Styles in the list 2024-09-30 17:41:21 -07:00
dd8426fefd Paginate Alt Styles, sort by most recent first-seen date 2024-09-30 17:35:18 -07:00
2a9818b2d1 Add series name filter to Alt Styles filter form
Right now this just is Nostalgic, but I'll label the rest soon!
2024-09-30 17:34:31 -07:00
0b72b5568c Add edit form for Alt Styles, for Support staff only
We'll need this to fix up the series names and thumbnails for the new
prismatic styles!
2024-09-30 17:21:45 -07:00
86e1f31231 Only show *relevant* colors in Alt Styles filters
That is, there is no 8-bit alt style, so don't bother including it in
the filter form; same for most other colors.
2024-09-30 16:35:58 -07:00
a99fb3ec02 Use Rainbow Pool styles for Alt Styles lists 2024-09-30 16:24:43 -07:00
d11c18129d Refactor Rainbow Pool to use shared styles for the list elements
The lists of pet types and pet states had very similar styles, which I
mostly copy-pasted. Now that I want to use them for Alt Styles too, I'm
refactoring!
2024-09-30 16:21:47 -07:00
0958111341 Share styles between pet types and alt styles as "rainbow-pool" CSS 2024-09-30 16:10:26 -07:00
775baa250b Add filter form to alt styles page
Oh wow, alt styles are getting some real work! I'll improve both the
user-facing and Support-facing tooling, to better handle the complexity.
2024-09-30 16:06:22 -07:00
2bd8afd486 Fix whitespace around "(X species)" in item page zone info 2024-09-29 15:05:57 -07:00
1f1c6d92b1 Oops, fix item page's "Customize More" not animating after color change
Ah whoops, I didn't notice that, when Turbo morphs the
`<measured-container>` into what the server HTML returns, it deletes
the `style` attribute we were using.

In this change, I refactor for `MeasuredContainer` to be the component
rather than `MeasuredContent`, so that it can also be responsible for
listening for changes to its own `style` prop, and remeasuring when
they happen.

We're also careful to avoid infinite loops, by only doing this when the
property is missing! (Otherwise, setting `--natural-width` triggers the
callback again, oops!)
2024-09-29 14:59:52 -07:00
e4a640ccee Oops, fix minor breadcrumbs display bug on pet_types#show 2024-09-27 22:39:22 -07:00
d465f4125e For support staff, Rainbow Pool links directly to edit page, not show
Just on the assumption that like. We're mostly here to edit things
2024-09-27 22:35:13 -07:00
946a6326ac Use radio buttons for poses in Rainbow Pool form, instead of dropdown
Just a bit easier to find what you want! especially with the grid layout
2024-09-27 22:34:52 -07:00
d5a901b917 Add edit form to Rainbow Pool for pet states, for support staff only 2024-09-27 22:14:00 -07:00
39e5ca59c4 Add breadcrumbs to Rainbow Pool pages 2024-09-27 20:01:07 -07:00
4fa80d33cc Merge branch 'main' into rainbow-pool 2024-09-27 19:43:31 -07:00
d66f81c96b Remove support for old "Nebula (fake)" April Fools color
This hasn't worked for a while anyway! Let's remove the bits of code
where we deal with it, and the database field that signals it. (We also
make a corresponding change in Impress 2020, so it doesn't crash trying
to query based on the `prank` column.)

I also ran this snippet to clear out all the Nebula stuff in the db:

```rb
Color.transaction do
	nebula = Color.where(prank: true).find_by_name("Nebula")
	nebula.pet_types.includes(pet_states: :swf_assets).each do |pet_type|
		pet_type.pet_states.each do |pet_state|
			pet_state.parent_swf_asset_relationships.each do |psa|
				psa.swf_asset.destroy!
				psa.destroy!
			end
			pet_state.destroy!
		end
		pet_type.destroy!
	end
	nebula.destroy!
end
```
2024-09-27 19:38:53 -07:00
f8a5ce4490 Improve Rainbow Pool filter form styles 2024-09-27 19:10:37 -07:00
81f0845d4a Improve Rainbow Pool link styles 2024-09-27 18:45:45 -07:00
f0257ba2d3 Merge branch 'main' into rainbow-pool 2024-09-27 18:32:04 -07:00
d056a5e766 Oops, don't show not-directly-for-sale items as being "0 NC"
"Fall Woodland Leaves Filter" is an example, it's part of the two-item
*pack* named "Fall Woodland Minitheus Petpet Foreground". The NC Mall
page for it will include the secondary items in `object_data`, but it's
not part of the storefront itself—and the only thing indicating that is
the `render` list.

Theoretically, we could use this to construct more data about like,
packs and stuff, automatically? But also, I don't want to backfill it
for everything historically, so like. Whatever.
2024-09-27 18:27:12 -07:00
5214a14990 Rescue from ActiveRecord::ConnectionTimeoutError
Just to stop filling the crash logs with it… if they spike, we'll be
alerted by the downtime monitor anyway.
2024-09-27 17:50:35 -07:00
06a89689d8 Oops, fix crash when modeling Patchwork Staff (AMFPHP string encoding!)
See comment for details! I wonder if other items have been affected by
this in the past. I think probably what happened before was that we
successfully created this item, but failed to create the *translation*,
so when migrating over the Patchwork Staff all its translated fields
were empty? (That's what I found looking in the database today.)

But yeah, thankfully our crash logging at health.openneo.net gave me
the name of a pet someone was trying to model, and so I was able to
find the bug and fix it!
2024-09-27 15:18:43 -07:00
a08fb89d59 Oops, don't crash when an item has no previews
A weird state to get into, one would expect impossible! But something
funny is going on with the Kiko Lake Team Popcorn item (85598)!
2024-09-27 15:18:43 -07:00
80307f21f7 Add Rainbow Pool homepage, with basic filter form 2024-09-26 21:10:25 -07:00
75040ffbf3 Add pages for the Rainbow Pool pet states 2024-09-26 20:24:31 -07:00
6f45cd0485 Add a bit more info to Rainbow Pool glitched label 2024-09-26 19:34:30 -07:00
4e33477c65 Hide unconverted below the "Other" list for Rainbow Pool poses 2024-09-26 19:33:16 -07:00
b28255cafd WIP: Better styles for Rainbow Pool pet type page 2024-09-26 18:39:32 -07:00
99e8b46157 Oops, fix bug parsing "8-Bit-Chia" in Rainbow Pool URLs 2024-09-26 18:36:49 -07:00
734b7fba1d WIP: Outfit viewers on pet type Rainbow Pool page
Now that we have such a convenient lil outfit viewer component we built
for the item page preview, it's easy peasy to drop it in here too! And
it's all nice and lightweight, since in this case it's basically just.
image tags, with some supporting enhancements.

Anyway, this page has no actual useful styles of its own yet. Gonna
make it look nice and such!
2024-09-26 18:20:05 -07:00
a1d6961249 WIP: Placeholder page for Rainbow Pool pet type
I'm experimenting with a Rainbow Pool ish UI, mainly as a support tool
for exploring and labeling poses—but one we can probably just show to
real users too!

Right now, I just use pet type images as a placeholder, and I polished
up some of the `pet_type_image` API. But we're probably gonna drop
these for a full outfit viewer, now that I think of it.
2024-09-26 14:56:45 -07:00
e7148ffae3 Oops, finish removing record_tag_helper gem
My bad!!
2024-09-26 12:53:16 -07:00
64b1d11faa Remove old record_tag_helper gem
This is a transitional gem to help with upgrading from old versions of
Rails: it provides a deprecated feature that Rails removed.

I audited and I *think* we only used it in one place, and that this one
place doesn't even use any of its functionality for styling or
scripting? So, begone!
2024-09-26 12:50:47 -07:00
e63f4df25b Run bundle update 2024-09-26 12:42:18 -07:00
535a0029f9 Replace some JS with the @starting-style CSS directive
Oh sweet, I learned about a new CSS feature with good-enough support!
This lets you use CSS transitions for an element as it enters the page,
or becomes visible.

Firefox only has partial support for this feature rn, but its partial
support covers our case, I tested to make sure! (Specifically, it
doesn't handle transitioning from `display: none` yet, which isn't what
we're doing.)
2024-09-24 19:33:06 -07:00
c0e4291745 Remove FragmentLocalization and localized_cache helper
We replace the `localized_cache` helper with just simple keys provided
to the `cache` helper, with `locale=#{I18n.locale}` inlined. End of an
era!
2024-09-20 20:10:04 -07:00
d27c03606f Delete unused images
Whew, quite a history here! I didn't _extensively_ audit for these, but
I scanned with pretty good searches and hit major pages and they didn't
crash, so. Good enough for me!
2024-09-20 19:38:52 -07:00
40a3f5bf68 Don't show the list filter for petpage exports if you have no lists 2024-09-20 19:30:23 -07:00
4bc38db5aa Replace closet_hangers/petpage.js with modern CSS
We use jQuery to basically simulate the `:has()` pseudoselector. Let's
just, use `:has()` now!
2024-09-20 19:27:39 -07:00
2ab1951e68 Move closet_hangers/petpage stylesheet into its own CSS file 2024-09-20 19:26:06 -07:00
cae2f3ca74 Serve jquery and jquery.tmpl from our own codebase, instead of a CDN
Right, yeah, we've been depending on an external CDN for a long time
for jQuery and the jQuery Template library, and I don't like that kind
of external dependency! Let's put it in with the rest of our libs.
2024-09-20 19:23:53 -07:00
31619071af Remove ajax_auth.js lib, by merging it in where needed
It's only actually used in two JS files, so rather than doing a weird
global `$.ajaxSetup` call, let's just inline it into the small handful
of AJAX calls that actually care.
2024-09-20 19:10:26 -07:00
f20a1b5398 Oops, fix locale form with Turbo pageloads
Before this change, this would only work on the first pageload, and
fail after doing a Turbo page navigation. Now, it works all the time!
2024-09-20 18:55:08 -07:00
3bd6f09a54 Remove "About NeoPass" page, now that it's on the blog 2024-09-20 18:43:38 -07:00
38474d19d7 Oops, fix broken strings on Neopets page import wizard
Uhh I guess when I half-removed a feature from the translations list (I
don't remember when?), it left two different dictionaries labeled
`neopets_page_import_tasks.new`, and the second one overwrote the
first. Oops! Yikes!

By removing these, the translations *above* them actually get to apply
to the page correctly. Before this change, the page just showed the
translation keys as placeholders, womp womp.
2024-09-20 18:16:06 -07:00
73e0b3bb3c Remove some silly view template caching calls
When I was trying to debug slow view code one time long long ago, I was
like "let's cache any part of the template that's static!"

And like. no that's silly, I don't trust that this speeds anything up,
but it _definitely_ adds complexity. Let's just not.
2024-09-20 18:08:11 -07:00
1f53615654 Add "State of DTI: 2024" blog post announcement 2024-09-20 18:02:58 -07:00
7f55456454 Explicitly disable the unused ActionCable Rails feature
Just for consistency with the other features we're not using, we turn
off ActionCable when loading the app. I just removed
`config/cable.yml`, so I figure, let's not load a feature without the
config file it expects! (even though that didn't seem to bother it)
2024-09-20 13:14:00 -07:00
f23bebb607 Remove unused config/cable.yml and config/store.yml files
We're not using the ActionCable or ActiveStorage Rails features in this
app, so we can clear out these default config files. If we need them
later, it's not hard to re-find / re-generate them!
2024-09-20 13:12:47 -07:00
cf2cd41531 Remove unused config/basic_type_hashes.yml file
Our production data now contains basic hashes for all species/color
combinations, and it's easy enough for a dev copy of the site to get
them too by running `rails public_data:pull`. So, I think it's time to
retire this hardcoded set, and get one more file out of our codebase!
2024-09-20 13:10:15 -07:00
d45162897d Upgrade to Rails 7.2.1
No pressing reason, I'm just doing upgrades today, and noticed a new
version is out, and scrolled the patch notes and there's no obvious
breaking changes for my purposes, so. Up we go!
2024-09-20 12:57:59 -07:00
02b510bb3f Upgrade to Yarn 4.5.0 2024-09-20 12:47:54 -07:00
9ebc498888 Upgrade to Ruby 3.3.5, and improve the mechanisms for it a bit
I move `ruby_version` into an Ansible variable, to make it easier to
update in the future!
2024-09-20 12:47:35 -07:00
5bf2ef42a0 Move JS libraries to vendor/javascript
The silly motivation is that I wanted to remove `.prettierignore`,
which just exists to omit that one folder from `npm run format`. But it
also seems like this is the standard place to put them—a standard
created long after we first set this up lol
2024-09-13 21:16:46 -07:00
0a5d369735 Remove i18n locale config complexity we don't use anymore
I forget what this was for, I think part of it was for managing item
names in different languages, and the "private" locale thing was
probably for WIP locales? But yeah, not used, delete!
2024-09-13 20:55:09 -07:00
ebd400369a Remove misc unused files 2024-09-13 20:43:32 -07:00
81e4d16816 Remove unused Delicious-Heavy.otf font
I don't think we've uhh ever used this? Idk?
2024-09-13 20:39:22 -07:00
95ae669549 Remove Noto fonts and just use system-ui
Yeah, I don't remember why So Many Years Ago I felt it was important to
use the Droid fonts; I adapted this choice into the Noto fonts when
modernizing the other day, but, tbh, the default system fonts are
probably just a better fit for like. everything we do, and then *not*
downloading MB of font files.

I also feel like a lot of the contexts where we used serif fonts were
like, frankly incidental, based on where we chose `<p>` for semantic
reasons? I don't think any of them actually are made much better by
serifs, I'm okay with just simplifying and dropping that, instead of
looking for a better serif font stack to replace it.
2024-09-13 20:07:12 -07:00
989c96fd2b Oops, fix pb_item for "Royal Girl Elephante Gold Bracelets" and similar
There's some funny bugs we had here, like "Relic Elephante Jewellery"
and "Royal Girl Skeith Bodice" getting assigned "Ice", and
"Tyrannian Meerca Spear" being "Pea" lmao

I went and checked all the assignments now and they look good to me!

```ruby
Item.is_pb.order(:name).
  map { |i| [i.pb_color&.human_name, i.name] }
```
2024-09-13 19:56:41 -07:00
fdf1f31867 Add pets:find task to look up pets of a given color/species 2024-09-13 18:59:17 -07:00
c7b0ec71ef Add pet_types:guess task to guess poses for Invisible etc pets 2024-09-13 18:12:28 -07:00
287d7af1b9 Fix minor whitespace issue on item page "Occupies" zone list
Ahh right, when you indent stuff underneath a tag in HAML, it does the
same indented form in the output HTML, which adds whitespace that
creates a problem for how we're doing this list.

Before this change, the "Engulfed in Flames Effect" item showed below
the preview: `Occupies: Background Item , Lower Foreground Item`, with
an extra space before the comma.

After this change, it now shows
`Occupies: Background Item, Lower Foreground Item`, as intended.
2024-09-12 16:03:10 -07:00
58d7c38523 Simplify CSP header for SWF asset embeds, to fix 502 for some assets
Fun little bug: viewing the "Engulfed in Flames Effect" item was
showing our "502 Bad Gateway" custom error page in the embed. This is
because the Rails app was providing a `Content-Security-Policy` header
value that was longer than nginx is configured by default to allow, so
it was refusing the response, and showing the same 502 error as if the
app hadn't responded at all. (We discovered this by opening
`/var/log/nginx/error.log`, which explained this very clearly, ty~!)

In this change, we no longer list every `images.neopets.com` asset,
instead marking the entire domain as a valid image source for the
SWF asset embed iframe. I don't _love_ this solution, I liked the
property of specifying literally exactly the assets we allow! But I
don't think there's any practical danger here, and it helps a *lot* for
making this more reliable.

(If we could have solved this reliably by increasing nginx's allowed
response header size, I probably would've done that? But I researched a
bit, and ultimately concluded that I don't trust other intermediary
software like firewalls not to have the same issue. Let's not be
pushing the limits of HTTP headers of all things!)
2024-09-12 15:59:18 -07:00
68b6f46939 Oops, fix typo blocking non-bold-or-italic Delicious font from loading 2024-09-09 21:45:52 -07:00
cf6a19a7fc Use Noto Sans as a fallback if Delicious fails to load
This shouldn't ever be an issue in practice? I just noticed it because
something funny is going on with the `#userbar` element specifically
not using the Delicious font, and so I figured, hey, this simulates a
very real possible scenario, I'd rather use our consistent sans font
in this case!
2024-09-09 21:44:39 -07:00
9e052789db fix hash in Thanks for showing us banner 2024-09-09 21:37:56 -07:00
30f211caf3 Remove some now-unused homepage styles
These must be from long ago! Shrug!
2024-09-09 21:35:06 -07:00
dab865689f Refactor module sections on homepage, to handle font change
Huh okay, moving to my other machine, the change to Noto Sans subtly
broke the homepage layout a bit, wrapping the form buttons to the next
line in the three module sections.

Here, I refactor to more modern grid/flexbox sensibilities. Btw, there
was a Flexbox thing that didn't work quite how I expected? I commented
on my confusion, but checked in Chrome and Firefox and it seems to work
in both, so, ok!
2024-09-09 21:33:05 -07:00
249 changed files with 5311 additions and 1957 deletions

2
.gitignore vendored
View file

@ -4,6 +4,8 @@ log/*.log
tmp/**/* tmp/**/*
.env .env
.env.* .env.*
/spec/examples.txt
/.yardoc
/app/assets/builds/* /app/assets/builds/*
!/app/assets/builds/.keep !/app/assets/builds/.keep

View file

@ -1,4 +1,5 @@
#!/usr/bin/env sh #!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh" . "$(dirname -- "$0")/_/husky.sh"
yarn lint --max-warnings=0 --fix # Run the linter, and all our tests.
yarn lint --max-warnings=0 --fix && bin/rake test spec

View file

@ -1 +0,0 @@
/app/assets/javascripts/lib

1
.rspec Normal file
View file

@ -0,0 +1 @@
--require spec_helper

View file

@ -1 +1 @@
3.3.4 3.3.5

26
Gemfile
View file

@ -1,7 +1,7 @@
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '3.3.4' ruby '3.3.5'
gem 'rails', '~> 7.1', '>= 7.1.3.4' gem 'rails', '~> 7.2', '>= 7.2.1'
# The HTTP server running the Rails instance. # The HTTP server running the Rails instance.
gem 'falcon', '~> 0.48.0' gem 'falcon', '~> 0.48.0'
@ -19,7 +19,7 @@ gem 'haml', '~> 6.1', '>= 6.1.1'
gem 'sass-rails', '~> 6.0' gem 'sass-rails', '~> 6.0'
gem 'terser', '~> 1.1', '>= 1.1.17' gem 'terser', '~> 1.1', '>= 1.1.17'
gem 'react-rails', '~> 2.7', '>= 2.7.1' gem 'react-rails', '~> 2.7', '>= 2.7.1'
gem 'jsbundling-rails', '~> 1.1' gem 'jsbundling-rails', '~> 1.3'
gem 'turbo-rails', '~> 2.0' gem 'turbo-rails', '~> 2.0'
# For authentication. # For authentication.
@ -66,10 +66,10 @@ gem "async-http", "~> 0.75.0", require: false
gem "thread-local", "~> 1.1", require: false gem "thread-local", "~> 1.1", require: false
# For debugging. # For debugging.
gem 'web-console', '~> 4.2', group: :development group :development do
gem 'debug', '~> 1.9.2'
# TODO: Review our use of content_tag_for etc and uninstall this! gem 'web-console', '~> 4.2'
gem 'record_tag_helper', '~> 1.0', '>= 1.0.1' end
# Reduces boot times through caching; required in config/boot.rb # Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '~> 1.16', require: false gem 'bootsnap', '~> 1.16', require: false
@ -87,5 +87,13 @@ gem "sentry-rails", "~> 5.12"
gem "shell", "~> 0.8.1" gem "shell", "~> 0.8.1"
# For workspace autocomplete. # For workspace autocomplete.
gem "solargraph", "~> 0.50.0", group: :development group :development do
gem "solargraph-rails", "~> 1.1", group: :development gem "solargraph", "~> 0.50.0"
gem "solargraph-rails", "~> 1.1"
end
# For automated tests.
group :development, :test do
gem "rspec-rails", "~> 7.0"
gem "webmock", "~> 3.24", group: :test
end

View file

@ -128,9 +128,15 @@ GEM
fiber-annotation fiber-annotation
fiber-local (~> 1.1) fiber-local (~> 1.1)
json json
crack (1.0.0)
bigdecimal
rexml
crass (1.0.6) crass (1.0.6)
csv (3.3.0) csv (3.3.0)
date (3.3.4) date (3.3.4)
debug (1.9.2)
irb (~> 1.10)
reline (>= 0.3.8)
devise (4.9.4) devise (4.9.4)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
@ -150,7 +156,7 @@ GEM
activemodel activemodel
erubi (1.13.0) erubi (1.13.0)
execjs (2.9.1) execjs (2.9.1)
falcon (0.48.0) falcon (0.48.2)
async async
async-container (~> 0.18) async-container (~> 0.18)
async-http (~> 0.75) async-http (~> 0.75)
@ -163,8 +169,9 @@ GEM
protocol-http (~> 0.31) protocol-http (~> 0.31)
protocol-rack (~> 0.7) protocol-rack (~> 0.7)
samovar (~> 2.3) samovar (~> 2.3)
faraday (2.11.0) faraday (2.12.0)
faraday-net_http (>= 2.0, < 3.4) faraday-net_http (>= 2.0, < 3.4)
json
logger logger
faraday-follow_redirects (0.3.0) faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3) faraday (>= 1, < 3)
@ -181,19 +188,20 @@ GEM
temple (>= 0.8.2) temple (>= 0.8.2)
thor thor
tilt tilt
hashdiff (1.1.2)
hashie (5.0.0) hashie (5.0.0)
http_accept_language (2.1.1) http_accept_language (2.1.1)
httparty (0.22.0) httparty (0.22.0)
csv csv
mini_mime (>= 1.0.0) mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
i18n (1.14.5) i18n (1.14.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
io-console (0.7.2) io-console (0.7.2)
io-endpoint (0.13.1) io-endpoint (0.13.1)
io-event (1.6.5) io-event (1.6.5)
io-stream (0.4.0) io-stream (0.4.1)
irb (1.14.0) irb (1.14.1)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jaro_winkler (1.6.0) jaro_winkler (1.6.0)
@ -218,7 +226,7 @@ GEM
letter_opener (1.10.0) letter_opener (1.10.0)
launchy (>= 2.2, < 4) launchy (>= 2.2, < 4)
localhost (1.3.1) localhost (1.3.1)
logger (1.6.0) logger (1.6.1)
loofah (2.22.0) loofah (2.22.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
@ -229,7 +237,7 @@ GEM
net-smtp net-smtp
mapping (1.1.1) mapping (1.1.1)
marcel (1.0.4) marcel (1.0.4)
memory_profiler (1.0.2) memory_profiler (1.1.0)
metrics (0.10.2) metrics (0.10.2)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.7) mini_portile2 (2.8.7)
@ -240,7 +248,7 @@ GEM
mysql2 (0.5.6) mysql2 (0.5.6)
net-http (0.4.1) net-http (0.4.1)
uri uri
net-imap (0.4.14) net-imap (0.4.16)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
@ -279,22 +287,22 @@ GEM
openssl (3.2.0) openssl (3.2.0)
orm_adapter (0.5.0) orm_adapter (0.5.0)
parallel (1.26.3) parallel (1.26.3)
parser (3.3.4.2) parser (3.3.5.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
process-metrics (0.3.0) process-metrics (0.3.0)
console (~> 1.8) console (~> 1.8)
json (~> 2) json (~> 2)
samovar (~> 2.1) samovar (~> 2.1)
protocol-hpack (1.5.0) protocol-hpack (1.5.1)
protocol-http (0.33.0) protocol-http (0.37.0)
protocol-http1 (0.22.0) protocol-http1 (0.27.0)
protocol-http (~> 0.22) protocol-http (~> 0.22)
protocol-http2 (0.18.0) protocol-http2 (0.19.1)
protocol-hpack (~> 1.4) protocol-hpack (~> 1.4)
protocol-http (~> 0.18) protocol-http (~> 0.18)
protocol-rack (0.7.0) protocol-rack (0.10.0)
protocol-http (~> 0.27) protocol-http (~> 0.37)
rack (>= 1.0) rack (>= 1.0)
psych (5.1.2) psych (5.1.2)
stringio stringio
@ -366,30 +374,43 @@ GEM
execjs execjs
railties (>= 3.2) railties (>= 3.2)
tilt tilt
record_tag_helper (1.0.1)
actionview (>= 5)
regexp_parser (2.9.2) regexp_parser (2.9.2)
reline (0.5.9) reline (0.5.10)
io-console (~> 0.5) io-console (~> 0.5)
responders (3.1.1) responders (3.1.1)
actionpack (>= 5.2) actionpack (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
reverse_markdown (2.1.1) reverse_markdown (2.1.1)
nokogiri nokogiri
rexml (3.3.6) rexml (3.3.7)
strscan rspec-core (3.13.2)
rubocop (1.65.1) rspec-support (~> 3.13.0)
rspec-expectations (3.13.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (7.0.1)
actionpack (>= 7.0)
activesupport (>= 7.0)
railties (>= 7.0)
rspec-core (~> 3.13)
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.1)
rubocop (1.66.1)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.3.0.2) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.4, < 3.0) regexp_parser (>= 2.4, < 3.0)
rexml (>= 3.2.5, < 4.0) rubocop-ast (>= 1.32.2, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.32.1) rubocop-ast (1.32.3)
parser (>= 3.3.1.0) parser (>= 3.3.1.0)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
samovar (2.3.0) samovar (2.3.0)
@ -446,7 +467,6 @@ GEM
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
stackprof (0.2.26) stackprof (0.2.26)
stringio (3.1.1) stringio (3.1.1)
strscan (3.1.0)
swd (2.0.3) swd (2.0.3)
activesupport (>= 3) activesupport (>= 3)
attr_required (>= 0.0.5) attr_required (>= 0.0.5)
@ -456,18 +476,17 @@ GEM
temple (0.10.3) temple (0.10.3)
terser (1.2.3) terser (1.2.3)
execjs (>= 0.3.0, < 3) execjs (>= 0.3.0, < 3)
thor (1.3.1) thor (1.3.2)
thread-local (1.1.0) thread-local (1.1.0)
tilt (2.4.0) tilt (2.4.0)
timeout (0.4.1) timeout (0.4.1)
traces (0.13.1) traces (0.13.1)
turbo-rails (2.0.6) turbo-rails (2.0.10)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
activejob (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
tzinfo (2.0.6) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0) unicode-display_width (2.6.0)
uri (0.13.1) uri (0.13.1)
useragent (0.16.10) useragent (0.16.10)
validate_url (1.0.15) validate_url (1.0.15)
@ -484,13 +503,17 @@ GEM
activesupport activesupport
faraday (~> 2.0) faraday (~> 2.0)
faraday-follow_redirects faraday-follow_redirects
webrick (1.8.1) webmock (3.24.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.8.2)
websocket-driver (0.7.6) websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
will_paginate (4.0.1) will_paginate (4.0.1)
yard (0.9.36) yard (0.9.37)
zeitwerk (2.6.17) zeitwerk (2.6.18)
PLATFORMS PLATFORMS
ruby ruby
@ -501,6 +524,7 @@ DEPENDENCIES
async (~> 2.17) async (~> 2.17)
async-http (~> 0.75.0) async-http (~> 0.75.0)
bootsnap (~> 1.16) bootsnap (~> 1.16)
debug (~> 1.9.2)
devise (~> 4.9, >= 4.9.2) devise (~> 4.9, >= 4.9.2)
devise-encryptable (~> 0.2.0) devise-encryptable (~> 0.2.0)
dotenv-rails (~> 2.8, >= 2.8.1) dotenv-rails (~> 2.8, >= 2.8.1)
@ -508,7 +532,7 @@ DEPENDENCIES
haml (~> 6.1, >= 6.1.1) haml (~> 6.1, >= 6.1.1)
http_accept_language (~> 2.1, >= 2.1.1) http_accept_language (~> 2.1, >= 2.1.1)
httparty (~> 0.22.0) httparty (~> 0.22.0)
jsbundling-rails (~> 1.1) jsbundling-rails (~> 1.3)
letter_opener (~> 1.8, >= 1.8.1) letter_opener (~> 1.8, >= 1.8.1)
memory_profiler (~> 1.0) memory_profiler (~> 1.0)
mysql2 (~> 0.5.5) mysql2 (~> 0.5.5)
@ -519,11 +543,11 @@ DEPENDENCIES
parallel (~> 1.23) parallel (~> 1.23)
rack-attack (~> 6.7) rack-attack (~> 6.7)
rack-mini-profiler (~> 3.1) rack-mini-profiler (~> 3.1)
rails (~> 7.1, >= 7.1.3.4) rails (~> 7.2, >= 7.2.1)
rails-i18n (~> 7.0, >= 7.0.7) rails-i18n (~> 7.0, >= 7.0.7)
rdiscount (~> 2.2, >= 2.2.7.1) rdiscount (~> 2.2, >= 2.2.7.1)
react-rails (~> 2.7, >= 2.7.1) react-rails (~> 2.7, >= 2.7.1)
record_tag_helper (~> 1.0, >= 1.0.1) rspec-rails (~> 7.0)
sanitize (~> 6.0, >= 6.0.2) sanitize (~> 6.0, >= 6.0.2)
sass-rails (~> 6.0) sass-rails (~> 6.0)
sentry-rails (~> 5.12) sentry-rails (~> 5.12)
@ -537,10 +561,11 @@ DEPENDENCIES
thread-local (~> 1.1) thread-local (~> 1.1)
turbo-rails (~> 2.0) turbo-rails (~> 2.0)
web-console (~> 4.2) web-console (~> 4.2)
webmock (~> 3.24)
will_paginate (~> 4.0) will_paginate (~> 4.0)
RUBY VERSION RUBY VERSION
ruby 3.3.4p94 ruby 3.3.5p100
BUNDLED WITH BUNDLED WITH
2.5.18 2.5.18

View file

@ -1,5 +1,6 @@
//= link_tree ../images //= link_tree ../images
//= link_tree ../javascripts .js //= link_tree ../javascripts .js
//= link_tree ../../../vendor/javascript .js
//= link_tree ../stylesheets .css //= link_tree ../stylesheets .css
//= link_directory ../fonts .otf //= link_directory ../fonts .otf
//= link_tree ../builds //= link_tree ../builds

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 585 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 516 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -1,20 +0,0 @@
(function () {
var CSRFProtection;
var token = $('meta[name="csrf-token"]').attr("content");
if (token) {
CSRFProtection = function (xhr, settings) {
var sendToken =
typeof settings.useCSRFProtection === "undefined" || // default to true
settings.useCSRFProtection;
if (sendToken) {
xhr.setRequestHeader("X-CSRF-Token", token);
}
};
} else {
CSRFProtection = $.noop;
}
$.ajaxSetup({
beforeSend: CSRFProtection,
});
})();

View file

@ -1,4 +1,11 @@
(function () { (function () {
function addCSRFToken(xhr) {
const token = document
.querySelector('meta[name="csrf-token"]')
?.getAttribute("content");
xhr.setRequestHeader("X-CSRF-Token", token);
}
var hangersInitCallbacks = []; var hangersInitCallbacks = [];
function onHangersInit(callback) { function onHangersInit(callback) {
@ -285,6 +292,7 @@
type: "post", type: "post",
data: data, data: data,
dataType: "json", dataType: "json",
beforeSend: addCSRFToken,
complete: function (data) { complete: function (data) {
if (quantityEl.val() == 0) { if (quantityEl.val() == 0) {
objectRemoved(objectWrapper); objectRemoved(objectWrapper);
@ -389,6 +397,7 @@
type: "post", type: "post",
data: data, data: data,
dataType: "json", dataType: "json",
beforeSend: addCSRFToken,
complete: function () { complete: function () {
button.val("Remove"); button.val("Remove");
}, },
@ -465,6 +474,7 @@
url: form.attr("action"), url: form.attr("action"),
type: form.attr("method"), type: form.attr("method"),
data: data, data: data,
beforeSend: addCSRFToken,
success: function (html) { success: function (html) {
var doc = $(html); var doc = $(html);
hangersEl.html(doc.find("#closet-hangers").html()); hangersEl.html(doc.find("#closet-hangers").html());
@ -501,6 +511,7 @@
url: form.attr("action") + ".json?" + $.param({ ids: hangerIds }), url: form.attr("action") + ".json?" + $.param({ ids: hangerIds }),
type: "delete", type: "delete",
dataType: "json", dataType: "json",
beforeSend: addCSRFToken,
success: function () { success: function () {
objectRemoved(hangerEls); objectRemoved(hangerEls);
}, },
@ -567,6 +578,7 @@
closet_hanger: closetHanger, closet_hanger: closetHanger,
return_to: window.location.pathname + window.location.search, return_to: window.location.pathname + window.location.search,
}, },
beforeSend: addCSRFToken,
complete: function () { complete: function () {
itemsSearchField.removeClass("loading"); itemsSearchField.removeClass("loading");
}, },
@ -711,6 +723,7 @@
type: "post", type: "post",
data: data, data: data,
dataType: "json", dataType: "json",
beforeSend: addCSRFToken,
complete: function () { complete: function () {
contactForm.enableForms(); contactForm.enableForms();
}, },
@ -731,6 +744,7 @@
type: "POST", type: "POST",
data: { neopets_connection: { neopets_username: newUsername } }, data: { neopets_connection: { neopets_username: newUsername } },
dataType: "json", dataType: "json",
beforeSend: addCSRFToken,
success: function (connection) { success: function (connection) {
var newOption = $("<option/>", { var newOption = $("<option/>", {
text: newUsername, text: newUsername,

View file

@ -1,8 +0,0 @@
(function () {
function setChecked() {
var el = $(this);
el.closest("li").toggleClass("checked", el.is(":checked"));
}
$("#petpage-closet-lists input").click(setChecked).each(setChecked);
})();

View file

@ -81,23 +81,35 @@ class SpeciesFacePickerOptions extends HTMLElement {
} }
} }
class MeasuredContent extends HTMLElement { // TODO: If it ever gets wide support, remove this in favor of the CSS rule
// `interpolate-size: allow-keywords`, to animate directly from `auto`.
// https://drafts.csswg.org/css-values-5/#valdef-interpolate-size-allow-keywords
class MeasuredContainer extends HTMLElement {
static observedAttributes = ["style"];
connectedCallback() { connectedCallback() {
setTimeout(() => this.#measure(), 0); setTimeout(() => this.#measure(), 0);
} }
#measure() { attributeChangedCallback() {
// Find our `<measured-container>` parent, and set our natural width // When `--natural-width` gets morphed away by Turbo, measure it again!
// as `var(--natural-width)` in the context of its CSS styles. if (this.style.getPropertyValue("--natural-width") === "") {
const container = this.closest("measured-container"); this.#measure();
if (container == null) {
throw new Error(`<measured-content> must be in a <measured-container>`);
} }
container.style.setProperty("--natural-width", this.offsetWidth + "px"); }
#measure() {
// Find our `<measured-content>` child, and set our natural width as
// `var(--natural-width)` in the context of our CSS styles.
const content = this.querySelector("measured-content");
if (content == null) {
throw new Error(`<measured-container> must contain a <measured-content>`);
}
this.style.setProperty("--natural-width", content.offsetWidth + "px");
} }
} }
customElements.define("species-color-picker", SpeciesColorPicker); customElements.define("species-color-picker", SpeciesColorPicker);
customElements.define("species-face-picker", SpeciesFacePicker); customElements.define("species-face-picker", SpeciesFacePicker);
customElements.define("species-face-picker-options", SpeciesFacePickerOptions); customElements.define("species-face-picker-options", SpeciesFacePickerOptions);
customElements.define("measured-content", MeasuredContent); customElements.define("measured-container", MeasuredContainer);

View file

@ -21,10 +21,6 @@ class OutfitViewer extends HTMLElement {
this.#setIsPlaying(playPauseToggle.checked); this.#setIsPlaying(playPauseToggle.checked);
this.#setIsPlayingCookie(playPauseToggle.checked); this.#setIsPlayingCookie(playPauseToggle.checked);
}); });
// Tell the CSS our first frame has rendered, which we use for loading
// state transitions.
this.#internals.states.add("after-first-frame");
} }
#setIsPlaying(isPlaying) { #setIsPlaying(isPlaying) {

View file

@ -12,10 +12,14 @@
if (PetQuery.name) { if (PetQuery.name) {
if (PetQuery.species && PetQuery.color) { if (PetQuery.species && PetQuery.color) {
var image_url = petImage("cpn/" + PetQuery.name, 1);
if (PetQuery.name.startsWith("@")) {
image_url = petImage("cp/" + PetQuery.name.substr(1), 1);
}
$("#pet-query-notice-template") $("#pet-query-notice-template")
.tmpl({ .tmpl({
pet_name: PetQuery.name, pet_name: PetQuery.name,
pet_image_url: petImage("cpn/" + PetQuery.name, 1), pet_image_url: image_url,
}) })
.prependTo("#container"); .prependTo("#container");
} }

View file

@ -37,6 +37,12 @@
pets.shift(); pets.shift();
loading = true; loading = true;
$.ajax({ $.ajax({
beforeSend: (xhr) => {
const token = document
.querySelector('meta[name="csrf-token"]')
?.getAttribute("content");
xhr.setRequestHeader("X-CSRF-Token", token);
},
complete: function (data) { complete: function (data) {
loading = false; loading = false;
loadNextIfReady(); loadNextIfReady();

View file

@ -32,9 +32,6 @@ body
a[href] a[href]
color: $link-color color: $link-color
p
font-family: $text-font
input, button, select input, button, select
font: font:
family: inherit family: inherit
@ -77,7 +74,7 @@ $container_width: 800px
input, button, select, label input, button, select, label
cursor: pointer cursor: pointer
input[type=text], input[type=password], input[type=search], input[type=number], input[type=email], select, textarea input[type=text], input[type=password], input[type=search], input[type=number], input[type=email], input[type=url], select, textarea
border-radius: 3px border-radius: 3px
background: #fff background: #fff
border: 1px solid $input-border-color border: 1px solid $input-border-color
@ -86,6 +83,15 @@ input[type=text], input[type=password], input[type=search], input[type=number],
&:focus, &:active &:focus, &:active
color: inherit color: inherit
select:has(option[value='']:checked)
color: #666
option[value='']
color: #666
option:not([value=''])
color: $text-color
textarea textarea
font: inherit font: inherit

View file

@ -3,10 +3,20 @@ body.use-responsive-design
max-width: 100% max-width: 100%
padding-inline: 1rem padding-inline: 1rem
box-sizing: border-box box-sizing: border-box
padding-top: 0
#main-nav
display: flex
flex-wrap: wrap
#home-link, #userbar
position: static
#home-link #home-link
margin-left: 1rem padding-inline: .5rem
padding-inline: 0 margin-inline: -.5rem
margin-right: auto
#userbar #userbar
margin-right: 1rem margin-left: auto
text-align: right

View file

@ -1,18 +0,0 @@
body.alt_styles-index
.alt-styles-header
margin-top: 1em
margin-bottom: .5em
.alt-styles-list
list-style: none
display: flex
flex-wrap: wrap
gap: 1.5em
.alt-style
text-align: center
width: 80px
.alt-style-thumbnail
width: 80px
height: 80px

View file

@ -0,0 +1,4 @@
.alt-style-preview
width: 300px
height: 300px
margin: 0 auto

View file

@ -0,0 +1,3 @@
.rainbow-pool-list
.name span
display: inline-block

View file

@ -8,9 +8,7 @@
@import partials/jquery.jgrowl @import partials/jquery.jgrowl
@import alt_styles/index
@import closet_hangers/index @import closet_hangers/index
@import closet_hangers/petpage
@import closet_lists/form @import closet_lists/form
@import neopets_page_import_tasks/new @import neopets_page_import_tasks/new
@import contributions/index @import contributions/index

View file

@ -0,0 +1,23 @@
#title:has(+ .breadcrumbs)
margin-bottom: .125em
.breadcrumbs
list-style-type: none
display: flex
flex-direction: row
margin-block: .5em
font-size: .85em
li
display: flex
li:not(:first-child)
&::before
margin-inline: .35em
content: ""
&[data-relation-to-prev=sibling]::before
content: "+"
&[data-relation-to-prev=menu]::before
content: "-"

View file

@ -0,0 +1,110 @@
@import "../partials/clean/constants"
// When loading, fade in the loading spinner after a brief delay. We only apply
// the delay here, not on the base styles, because fading *out* on load should
// be instant.
//
// This is implemented as a mixin, so that the item page can leverage the same
// loading state when loading a new preview altogether. Once CSS container
// style queries gain wider support, maybe use that instead.
=outfit-viewer-loading
cursor: wait
.loading-indicator
opacity: 1
transition-delay: 2s
// If the outfit *starts* in loading state, still delay the fade-in.
@starting-style
opacity: 0
outfit-viewer
display: block
position: relative
overflow: hidden
// These are default widths, expected to often be overridden.
width: 300px
height: 300px
// There's no useful text in here, but double-clicking the play/pause
// button can cause a weird selection state. Disable text selection.
user-select: none
-webkit-user-select: none
outfit-layer
display: block
position: absolute
inset: 0
// We disable pointer-events most importantly for the iframes, which
// will ignore our `cursor: wait` and show a plain cursor for the
// inside of its own document. But also, the context menus for these
// elements are kinda actively misleading, too!
pointer-events: none
img, iframe
width: 100%
height: 100%
.loading-indicator
position: absolute
z-index: 1000
bottom: 0px
right: 4px
padding: 8px
background: radial-gradient(circle closest-side, white 45%, #ffffff00)
opacity: 0
.play-pause-button
position: absolute
z-index: 1001
left: 8px
bottom: 8px
display: none
align-items: center
justify-content: center
color: white
background: rgba(0, 0, 0, 0.64)
width: 2.5em
height: 2.5em
border-radius: 100%
border: 2px solid transparent
transition: all .25s
.playing-label, .paused-label
display: none
width: 1em
height: 1em
.play-pause-toggle
// Visually hidden
clip: rect(0 0 0 0)
clip-path: inset(50%)
height: 1px
overflow: hidden
position: absolute
white-space: nowrap
width: 1px
&:checked ~ .playing-label
display: block
&:not(:checked) ~ .paused-label
display: block
&:hover, &:has(.play-pause-toggle:focus)
border: 2px solid $module-border-color
background: $module-bg-color
color: $text-color
&:has(.play-pause-toggle:active)
transform: translateY(2px)
&:has(outfit-layer:state(has-animations))
.play-pause-button
display: flex
&:has(outfit-layer:state(loading))
+outfit-viewer-loading

View file

@ -0,0 +1,74 @@
@import "../partials/clean/constants"
.rainbow-pool-filters
margin-block: .5em
fieldset
display: flex
flex-direction: row
align-items: center
justify-content: center
gap: .5em
legend
display: contents
font-weight: bold
select
width: 16ch
.rainbow-pool-list
list-style-type: none
display: flex
flex-wrap: wrap
justify-content: center
gap: .5em
--preview-base-width: 150px
> li
width: var(--preview-base-width)
max-width: calc(50% - .25em)
min-width: 150px
box-sizing: border-box
text-align: center
a
display: block
border-radius: 1em
padding: .5em
text-decoration: none
background: white
&:hover
outline: 1px solid $module-border-color
background: $module-bg-color
.preview
width: 100%
height: auto
aspect-ratio: 1 / 1
margin-bottom: -1em
.name
background: inherit
padding: .25em .5em
border-radius: .5em
margin: 0 auto
position: relative
z-index: 1
.info
font-size: .85em
p
margin-block: .25em
.rainbow-pool-pagination
margin-block: .5em
display: flex
justify-content: center
gap: 1em
.rainbow-pool-no-results
margin-block: 1em
text-align: center
font-style: italic

View file

@ -0,0 +1,102 @@
@import "../partials/clean/constants"
.support-form
display: flex
flex-direction: column
gap: 1em
align-items: flex-start
.fields
list-style-type: none
display: flex
flex-direction: column
gap: .75em
width: 100%
> li
display: flex
flex-direction: column
gap: .25em
max-width: 60ch
> label, > .field_with_errors label
display: block
font-weight: bold
.field_with_errors
> label
color: $error-color
input[type=text], input[type=url]
border-color: $error-border-color
color: $error-color
&[data-type=radio]
ul
list-style-type: none
&[data-type=radio-grid] // Set the `--num-columns` property to configure!
max-width: none
ul
list-style-type: none
display: grid
grid-template-columns: repeat(var(--num-columns, 1), 1fr)
gap: .25em
li
display: flex
align-items: stretch // Give the bubbles equal heights!
label
display: flex
align-items: center
gap: .5em
padding: .5em 1em
border: 1px solid $soft-border-color
border-radius: 1em
flex: 1 1 auto
input
margin: 0
&:has(:checked)
background: $module-bg-color
border-color: $module-border-color
input[type=text], input[type=url]
width: 100%
min-width: 10ch
box-sizing: border-box
.thumbnail-input
display: flex
align-items: center
gap: .25em
img
width: 40px
height: 40px
fieldset
display: flex
flex-direction: column
gap: .25em
legend
font-weight: bold
.field_with_errors
display: contents
.actions
display: flex
align-items: center
gap: 1em
.go-to-next
display: flex
align-items: center
gap: .25em
font-size: .85em
font-style: italic

View file

@ -1,58 +0,0 @@
@import "../partials/clean/constants"
@import "../partials/clean/mixins"
@import "../partials/secondary_nav"
body.closet_hangers-petpage
+secondary-nav
#intro
clear: both
#petpage-closet-lists
+clearfix
border-radius: 10px
border: 1px solid $soft-border-color
margin-bottom: 1.5em
padding: .5em 1.5em
> div
margin: .25em 0
h4
display: inline-block
vertical-align: middle
&::after
content: ":"
ul
list-style: none
margin: 0
padding: 0
li
display: inline-block
font-size: 85%
margin: .25em .5em
padding: 1px
label
padding: .25em .75em .25em .25em
&.checked
background: $module-bg-color
border-radius: 3px
border: 1px solid $module-border-color
padding: 0
&.unlisted
font-style: italic
input[type=submit]
float: right
#petpage-output
display: block
height: 30em
margin: 0 auto
width: 50%

View file

@ -0,0 +1,57 @@
@import "../partials/clean/constants"
@import "../partials/clean/mixins"
@import "../partials/secondary_nav"
+secondary-nav
#intro
clear: both
#petpage-closet-lists
+clearfix
border-radius: 10px
border: 1px solid $soft-border-color
margin-bottom: 1.5em
padding: .5em 1.5em
> div
margin: .25em 0
h4
display: inline-block
vertical-align: middle
&::after
content: ":"
ul
list-style: none
margin: 0
padding: 0
li
display: inline-block
font-size: 85%
margin: .25em .5em
padding: 1px
label
padding: .25em .75em .25em .25em
&:has(:checked)
background: $module-bg-color
border-radius: 3px
border: 1px solid $module-border-color
padding: 0
&.unlisted
font-style: italic
input[type=submit]
float: right
#petpage-output
display: block
height: 30em
margin: 0 auto
width: 50%

View file

@ -1,7 +1,7 @@
/* A font by Jos Buivenga (exljbris) -> www.exljbris.nl */ /* A font by Jos Buivenga (exljbris) -> www.exljbris.nl */
@font-face { @font-face {
font-family: Delicious; font-family: Delicious;
src: local("Delicious"), url("<%= font_path "Delicious-Roman.otf" %>)"); src: local("Delicious"), url("<%= font_path "Delicious-Roman.otf" %>");
} }
@font-face { @font-face {
@ -15,25 +15,3 @@
font-style: italic; font-style: italic;
src: local("Delicious"), url("<%= font_path "Delicious-Italic.otf" %>"); src: local("Delicious"), url("<%= font_path "Delicious-Italic.otf" %>");
} }
@font-face {
font-family: "Noto Sans";
src: local("Noto Sans"), url("<%= font_path "NotoSans-Variable.ttf" %>");
}
@font-face {
font-family: "Noto Sans";
font-style: italic;
src: local("Noto Sans"), url("<%= font_path "NotoSans-Italic-Variable.ttf" %>");
}
@font-face {
font-family: "Noto Serif";
src: local("Noto Serif"), url("<%= font_path "NotoSerif-Variable.ttf" %>");
}
@font-face {
font-family: "Noto Serif";
font-style: italic;
src: local("Noto Serif"), url("<%= font_path "NotoSerif-Italic-Variable.ttf" %>");
}

View file

@ -2,6 +2,8 @@
@import "../partials/clean/mixins" @import "../partials/clean/mixins"
@import "../partials/item_header" @import "../partials/item_header"
@import "../application/outfit-viewer"
#container #container
width: 900px // A bit more generous to the preview area! width: 900px // A bit more generous to the preview area!
@ -78,93 +80,10 @@
width: var(--natural-width) width: var(--natural-width)
outfit-viewer outfit-viewer
display: block
position: relative
width: 300px width: 300px
height: 300px height: 300px
border: 1px solid $module-border-color border: 1px solid $module-border-color
border-radius: 1em border-radius: 1em
overflow: hidden
// There's no useful text in here, but double-clicking the play/pause
// button can cause a weird selection state. Disable text selection.
user-select: none
-webkit-user-select: none
outfit-layer
display: block
position: absolute
inset: 0
// We disable pointer-events most importantly for the iframes, which
// will ignore our `cursor: wait` and show a plain cursor for the
// inside of its own document. But also, the context menus for these
// elements are kinda actively misleading, too!
pointer-events: none
img, iframe
width: 100%
height: 100%
.loading-indicator
position: absolute
z-index: 1000
bottom: 0px
right: 4px
padding: 8px
background: radial-gradient(circle closest-side, white 45%, #ffffff00)
opacity: 0
transition: opacity .5s
.play-pause-button
position: absolute
z-index: 1001
left: 8px
bottom: 8px
display: none
align-items: center
justify-content: center
color: white
background: rgba(0, 0, 0, 0.64)
width: 2.5em
height: 2.5em
border-radius: 100%
border: 2px solid transparent
transition: all .25s
.playing-label, .paused-label
display: none
width: 1em
height: 1em
.play-pause-toggle
// Visually hidden
clip: rect(0 0 0 0)
clip-path: inset(50%)
height: 1px
overflow: hidden
position: absolute
white-space: nowrap
width: 1px
&:checked ~ .playing-label
display: block
&:not(:checked) ~ .paused-label
display: block
&:hover, &:has(.play-pause-toggle:focus)
border: 2px solid $module-border-color
background: $module-bg-color
color: $text-color
&:has(.play-pause-toggle:active)
transform: translateY(2px)
&:has(outfit-layer:state(has-animations))
.play-pause-button
display: flex
.error-indicator .error-indicator
font-size: 85% font-size: 85%
@ -178,19 +97,9 @@ outfit-viewer
// is loading. // is loading.
// //
// We only apply the delay here, not on the base styles, because fading // We only apply the delay here, not on the base styles, because fading
// *out* on load should be instant. We also wait for the outfit-viewer to // *out* on load should be instant.
// execute a `setTimeout(0)`, to make sure we always *start* in the #item-preview[busy] outfit-viewer
// non-loading state. This is because it's sometimes possible for the page to +outfit-viewer-loading
// start with the web component already in `state(loading)`, and we need to
// make sure we *start* in *non-loading* state for the transition delay to
// happen. (This can happen when you Turbo-navigate between multiple items.)
#item-preview[busy] outfit-viewer, outfit-viewer:has(outfit-layer:state(loading))
cursor: wait
&:state(after-first-frame)
.loading-indicator
opacity: 1
transition-delay: 2s
#item-preview:has(outfit-layer:state(error)) #item-preview:has(outfit-layer:state(error))
outfit-viewer outfit-viewer

View file

@ -78,85 +78,57 @@ body.outfits-new
font-size: 175% font-size: 175%
select select
font-size: 120% font-size: 120%
#description, #top-contributors
float: left
#description
margin-right: 2%
width: 64%
#top-contributors
border: 1px solid $input-border-color
margin-top: 1em
padding: 1%
width: 30%
ol
margin-left: 2em
padding-left: 1em
> a
font-size: 80%
display: block
text-align: right
#how-can-i-help, #i-found-something
+module
float: left
padding: 1%
width: 46%
h2
font-style: italic
input, button
font-size: 115%
input[type=text]
border-color: $module-border-color
width: 12em
#how-can-i-help
margin-right: 1%
#i-found-something
margin-left: 1%
a
float: right
font-size: 87.5%
margin-top: 1em
$section-count: 3
$section-border-width: 1px
$section-padding: 0.5em
$section-width: 100% / $section-count
// (A - (B-1)*C) / B
#sections #sections
+clearfix display: grid
display: table grid-template-columns: 1fr 1fr 1fr
list-style: none list-style: none
margin-top: 1em margin-top: 1em
h3
margin-bottom: .25em
li li
border-left: display: grid
color: $module-border-color grid-template-areas: "header image" "info image" "form form"
style: solid grid-template-rows: auto auto auto
width: $section-border-width row-gap: .5em
display: table-cell padding: 0.5em
padding: $section-padding &:not(:first-child)
position: relative border-left: 1px solid $module-border-color
width: $section-width h3
&:first-child grid-area: header
border-left: 0 margin-bottom: 0
div div
grid-area: info
color: $soft-text-color color: $soft-text-color
font-size: 75% font-size: 75%
margin-left: 1em margin-left: 1em
z-index: 2 z-index: 2
h4, input strong
font-size: 116% font-size: 116%
h4, input[type=text] a:has(img)
color: inherit grid-area: image
h4 a
background: #ffffc0
img img
+opacity(0.75) opacity: 0.75
float: right float: right
margin-left: .5em margin-left: .5em
&:hover &:hover
+opacity(1) opacity: 1
p p
line-height: 1.5
min-height: 4.5em min-height: 4.5em
margin-bottom: 0
form
grid-area: form
display: flex
align-items: center
gap: .5em
font-size: .85em
margin-left: 1em
margin-right: .5em
input[type=text], input[type=search]
// TODO: It doesn't make sense to me that this is the right style? I
// expected `flex: 1 0 0` to be right, but that grew *too* large, and
// forced the sections to grow wider too. I also tried `flex: 0 1 100%`,
// which I would have *thought* is the same as this, but isn't! Idk!
width: 100%
#whats-new #whats-new
margin-bottom: 1em margin-bottom: 1em
@ -325,4 +297,3 @@ body.outfits-new
#latest-contribution-created-at #latest-contribution-created-at
color: $soft-text-color color: $soft-text-color
margin-left: .5em margin-left: .5em

View file

@ -67,14 +67,21 @@
background: #FEEBC8 background: #FEEBC8
color: #7B341E color: #7B341E
.support-form
grid-area: support
font-size: 85%
text-align: left
.user-lists-info .user-lists-info
grid-area: lists grid-area: lists
font-size: 85% font-size: 85%
text-align: left text-align: left
.user-lists-form-opener display: flex
&::after gap: 1em
content: " "
a::after
content: " "
.user-lists-form .user-lists-form
background: $background-color background: $background-color

View file

@ -18,9 +18,8 @@ $error-color: #8a1f11
$error-bg-color: #fbe3e4 $error-bg-color: #fbe3e4
$error-border-color: #fbc2c4 $error-border-color: #fbc2c4
$header-font: Delicious, Helvetica, Arial, Verdana, sans-serif $header-font: Delicious, system-ui, sans-serif
$main-font: "Noto Sans", Helvetica, Arial, Verdana, sans-serif $main-font: system-ui, sans-serif
$text-font: "Noto Serif", Georgia, "Times New Roman", Times, serif
$object-img-size: 80px $object-img-size: 80px
$object-width: 100px $object-width: 100px

View file

@ -0,0 +1,15 @@
outfit-viewer
margin: 0 auto
.fields li[data-type=radio-grid]
--num-columns: 3
.reference-link
display: flex
align-items: center
gap: .5em
padding-inline: .5em
img
height: 2em
width: auto

View file

@ -0,0 +1,8 @@
@import "../partials/clean/constants"
.rainbow-pool-list
--preview-base-width: 200px
margin-bottom: 2em
.glitched
cursor: help

View file

@ -1,21 +1,40 @@
class AltStylesController < ApplicationController class AltStylesController < ApplicationController
before_action :support_staff_only, except: [:index]
def index def index
@alt_styles = AltStyle.includes(:species, :color, :swf_assets). @all_alt_styles = AltStyle.includes(:species, :color)
order(:species_id, :color_id)
if params[:species_id] @all_colors = @all_alt_styles.map(&:color).uniq.sort_by(&:name)
@species = Species.find(params[:species_id]) @all_species = @all_alt_styles.map(&:species).uniq.sort_by(&:name)
@alt_styles = @alt_styles.merge(@species.alt_styles)
end
# We're going to link to the HTML5 image URL, so make sure we have all the @all_series_names = @all_alt_styles.map(&:series_name).uniq.sort
@all_color_names = @all_colors.map(&:human_name)
@all_species_names = @all_species.map(&:human_name)
@series_name = params[:series]
@color = find_color
@species = find_species
@alt_styles = @all_alt_styles.includes(:swf_assets)
@alt_styles.where!(series_name: @series_name) if @series_name.present?
@alt_styles.merge!(@color.alt_styles) if @color
@alt_styles.merge!(@species.alt_styles) if @species
# We're using the HTML5 image for our preview, so make sure we have all the
# manifests ready! # manifests ready!
SwfAsset.preload_manifests @alt_styles.map(&:swf_assets).flatten SwfAsset.preload_manifests @alt_styles.map(&:swf_assets).flatten
respond_to do |format| respond_to do |format|
format.html { render } format.html {
@alt_styles = @alt_styles.
by_creation_date.order(:color_id, :species_id, :series_name).
paginate(page: params[:page], per_page: 30)
render
}
format.json { format.json {
render json: @alt_styles.includes(swf_assets: [:zone]).as_json( @alt_styles = @alt_styles.includes(swf_assets: [:zone]).
sort_by(&:full_name)
render json: @alt_styles.as_json(
only: [:id, :species_id, :color_id, :body_id, :series_name, only: [:id, :species_id, :color_id, :body_id, :series_name,
:adjective_name, :thumbnail_url], :adjective_name, :thumbnail_url],
include: { include: {
@ -30,4 +49,56 @@ class AltStylesController < ApplicationController
} }
end end
end end
def edit
@alt_style = AltStyle.find params[:id]
end
def update
@alt_style = AltStyle.find params[:id]
if @alt_style.update(alt_style_params)
flash[:notice] = "\"#{@alt_style.full_name}\" successfully saved!"
redirect_to destination_after_save
else
render action: :edit, status: :bad_request
end
end
protected
def alt_style_params
params.require(:alt_style).permit(:real_series_name, :thumbnail_url)
end
def find_color
if params[:color]
Color.find_by(name: params[:color])
end
end
def find_species
if params[:species_id]
Species.find_by(id: params[:species_id])
elsif params[:species]
Species.find_by(name: params[:species])
end
end
def destination_after_save
if params[:next] == "unlabeled-style"
next_unlabeled_style_path
else
alt_styles_path
end
end
def next_unlabeled_style_path
unlabeled_style = AltStyle.unlabeled.newest.first
if unlabeled_style
edit_alt_style_path(unlabeled_style, next: "unlabeled-style")
else
alt_styles_path
end
end
end end

View file

@ -2,12 +2,10 @@ require 'async'
require 'async/container' require 'async/container'
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
include FragmentLocalization
protect_from_forgery protect_from_forgery
helper_method :current_user, :user_signed_in? helper_method :current_user, :support_staff?, :user_signed_in?
before_action :set_locale before_action :set_locale
before_action :configure_permitted_parameters, if: :devise_controller? before_action :configure_permitted_parameters, if: :devise_controller?
@ -23,9 +21,12 @@ class ApplicationController < ActionController::Base
class AccessDenied < StandardError; end class AccessDenied < StandardError; end
rescue_from AccessDenied, with: :on_access_denied rescue_from AccessDenied, with: :on_access_denied
rescue_from Async::Stop, Async::Container::Terminate, rescue_from Async::Stop, Async::Container::Terminate,
with: :on_request_stopped with: :on_request_stopped
rescue_from ActiveRecord::ConnectionTimeoutError, with: :on_db_timeout
def authenticate_user! def authenticate_user!
redirect_to(new_auth_user_session_path) unless user_signed_in? redirect_to(new_auth_user_session_path) unless user_signed_in?
end end
@ -45,15 +46,15 @@ class ApplicationController < ActionController::Base
def user_signed_in? def user_signed_in?
auth_user_signed_in? auth_user_signed_in?
end end
def infer_locale def infer_locale
return params[:locale] if valid_locale?(params[:locale]) return params[:locale] if valid_locale?(params[:locale])
return cookies[:locale] if valid_locale?(cookies[:locale]) return cookies[:locale] if valid_locale?(cookies[:locale])
Rails.logger.debug "Preferred languages: #{http_accept_language.user_preferred_languages}" Rails.logger.debug "Preferred languages: #{http_accept_language.user_preferred_languages}"
http_accept_language.language_region_compatible_from(I18n.public_locales.map(&:to_s)) || http_accept_language.language_region_compatible_from(I18n.available_locales.map(&:to_s)) ||
I18n.default_locale I18n.default_locale
end end
def not_found(record_name='record') def not_found(record_name='record')
raise ActionController::RoutingError.new("#{record_name} not found") raise ActionController::RoutingError.new("#{record_name} not found")
end end
@ -67,6 +68,11 @@ class ApplicationController < ActionController::Base
status: :internal_server_error status: :internal_server_error
end end
def on_db_timeout
render file: 'public/503.html', layout: false,
status: :service_unavailable
end
def redirect_back!(default=:back) def redirect_back!(default=:back)
redirect_to(params[:return_to] || default) redirect_to(params[:return_to] || default)
end end
@ -76,7 +82,7 @@ class ApplicationController < ActionController::Base
end end
def valid_locale?(locale) def valid_locale?(locale)
locale && I18n.usable_locales.include?(locale.to_sym) locale && I18n.available_locales.include?(locale.to_sym)
end end
def configure_permitted_parameters def configure_permitted_parameters
@ -104,5 +110,13 @@ class ApplicationController < ActionController::Base
Rails.logger.debug "Using return_to path: #{return_to.inspect}" Rails.logger.debug "Using return_to path: #{return_to.inspect}"
return_to || root_path return_to || root_path
end end
def support_staff?
current_user&.support_staff?
end
def support_staff_only
raise AccessDenied, "Support staff only" unless support_staff?
end
end end

View file

@ -1,5 +1,6 @@
class ItemsController < ApplicationController class ItemsController < ApplicationController
before_action :set_query before_action :set_query
before_action :support_staff_only, except: [:index, :show, :sources]
rescue_from Item::Search::Error, :with => :search_error rescue_from Item::Search::Error, :with => :search_error
def index def index
@ -112,6 +113,21 @@ class ItemsController < ApplicationController
end end
end end
def edit
@item = Item.find params[:id]
render layout: "application"
end
def update
@item = Item.find params[:id]
if @item.update(item_params)
flash[:notice] = "\"#{@item.name}\" successfully saved!"
redirect_to @item
else
render action: "edit", layout: "application", status: :bad_request
end
end
def sources def sources
# Load all the items, then group them by source. # Load all the items, then group them by source.
item_ids = params[:ids].split(",") item_ids = params[:ids].split(",")
@ -164,6 +180,15 @@ class ItemsController < ApplicationController
protected protected
def item_params
params.require(:item).permit(
:name, :thumbnail_url, :description, :modeling_status_hint,
:is_manually_nc, :explicitly_body_specific,
).tap do |p|
p[:modeling_status_hint] = nil if p[:modeling_status_hint] == ""
end
end
def assign_closeted!(items) def assign_closeted!(items)
current_user.assign_closeted_to_items!(items) if user_signed_in? current_user.assign_closeted_to_items!(items) if user_signed_in?
end end
@ -215,7 +240,8 @@ class ItemsController < ApplicationController
@item.compatible_pet_types. @item.compatible_pet_types.
preferring_species(cookies["preferred-preview-species-id"] || "<ignore>"). preferring_species(cookies["preferred-preview-species-id"] || "<ignore>").
preferring_color(cookies["preferred-preview-color-id"] || "<ignore>"). preferring_color(cookies["preferred-preview-color-id"] || "<ignore>").
preferring_simple.first preferring_simple.first ||
PetType.matching_name("Blue", "Acara").first!
end end
def validate_preview def validate_preview

View file

@ -47,29 +47,24 @@ class OutfitsController < ApplicationController
end end
def new def new
@colors = Color.funny.alphabetical @colors = Color.alphabetical
@species = Species.alphabetical @species = Species.alphabetical
# HACK: Skip this in development, because it's slow! newest_items = Item.newest.limit(18)
unless Rails.env.development? @newest_modeled_items, @newest_unmodeled_items =
newest_items = Item.newest. newest_items.partition(&:predicted_fully_modeled?)
select(:id, :name, :updated_at, :thumbnail_url, :rarity_index, :is_manually_nc)
.limit(18)
@newest_modeled_items, @newest_unmodeled_items =
newest_items.partition(&:predicted_fully_modeled?)
@newest_unmodeled_items_predicted_missing_species_by_color = {} @newest_unmodeled_items_predicted_missing_species_by_color = {}
@newest_unmodeled_items_predicted_modeled_ratio = {} @newest_unmodeled_items_predicted_modeled_ratio = {}
@newest_unmodeled_items.each do |item| @newest_unmodeled_items.each do |item|
h = item.predicted_missing_nonstandard_body_ids_by_species_by_color h = item.predicted_missing_nonstandard_body_ids_by_species_by_color
standard_body_ids_by_species = item. standard_body_ids_by_species = item.
predicted_missing_standard_body_ids_by_species predicted_missing_standard_body_ids_by_species
if standard_body_ids_by_species.present? if standard_body_ids_by_species.present?
h[:standard] = standard_body_ids_by_species h[:standard] = standard_body_ids_by_species
end
@newest_unmodeled_items_predicted_missing_species_by_color[item] = h
@newest_unmodeled_items_predicted_modeled_ratio[item] = item.predicted_modeled_ratio
end end
@newest_unmodeled_items_predicted_missing_species_by_color[item] = h
@newest_unmodeled_items_predicted_modeled_ratio[item] = item.predicted_modeled_ratio
end end
@species_count = Species.count @species_count = Species.count

View file

@ -0,0 +1,50 @@
class PetStatesController < ApplicationController
before_action :find_pet_state
before_action :support_staff_only
def edit
end
def update
if @pet_state.update(pet_state_params)
flash[:notice] = "Pet appearance \##{@pet_state.id} successfully saved!"
redirect_to destination_after_save
else
render action: :edit, status: :bad_request
end
end
protected
def find_pet_state
@pet_type = PetType.find_by_param!(params[:pet_type_name])
@pet_state = @pet_type.pet_states.find(params[:id])
@reference_pet_type = @pet_type.reference
end
def pet_state_params
params.require(:pet_state).permit(:pose, :glitched)
end
def destination_after_save
if params[:next] == "unlabeled-appearance"
next_unlabeled_appearance_path
else
@pet_type
end
end
def next_unlabeled_appearance_path
unlabeled_appearance = PetState.next_unlabeled_appearance
if unlabeled_appearance
edit_pet_type_pet_state_path(
unlabeled_appearance.pet_type,
unlabeled_appearance,
next: "unlabeled-appearance"
)
else
@pet_type
end
end
end

View file

@ -1,10 +1,111 @@
class PetTypesController < ApplicationController class PetTypesController < ApplicationController
def show def index
@pet_type = PetType. respond_to do |format|
where(species_id: params[:species_id]). format.html {
where(color_id: params[:color_id]). @species_names = Species.order(:name).map(&:human_name)
first @color_names = Color.order(:name).map(&:human_name)
render json: @pet_type if params[:species].present?
@selected_species = Species.find_by!(name: params[:species])
@selected_species_name = @selected_species.human_name
end
if params[:color].present?
@selected_color = Color.find_by!(name: params[:color])
@selected_color_name = @selected_color.human_name
end
@selected_order =
if @selected_species.present? || @selected_color.present?
:alphabetical
else
:newest
end
@pet_types = PetType.
includes(:color, :species, :pet_states).
paginate(page: params[:page], per_page: 30)
@pet_types.where!(species_id: @selected_species) if @selected_species
@pet_types.where!(color_id: @selected_color) if @selected_color
if @selected_order == :newest
@pet_types.order!(created_at: :desc)
elsif @selected_order == :alphabetical
@pet_types.merge!(Color.alphabetical).merge!(Species.alphabetical)
end
if @selected_species && @selected_color && @pet_types.size == 1
redirect_to @pet_types.first
end
if support_staff?
@counts = {
total: PetState.count,
glitched: PetState.glitched.count,
needs_labeling: PetState.needs_labeling.count,
usable: PetState.usable.count,
}
@unlabeled_appearance = PetState.next_unlabeled_appearance
end
}
format.json {
if stale?(etag: PetState.last_updated_key)
render json: {
species: Species.order(:name).all,
colors: Color.order(:name).all,
supported_poses: PetState.all_supported_poses,
}
end
}
end
end
def show
@pet_type = find_pet_type
respond_to do |format|
format.html do
@pet_states = group_pet_states @pet_type.pet_states
end
format.json { render json: @pet_type }
end
end
protected
# The API-ish route uses IDs, but the human-facing route uses names.
def find_pet_type
if params[:species_id] && params[:color_id]
PetType.find_by!(
species_id: params[:species_id],
color_id: params[:color_id],
)
elsif params[:name]
PetType.find_by_param!(params[:name])
else
raise "expected params: species_id and color_id, or name"
end
end
# The `canonical` pet states are the main ones we want to show: the most
# canonical state for each pose. The `other` pet states are, the others!
#
# If no main poses are available, then we just make all the poses
# "canonical", and show the whole mish-mash!
def group_pet_states(pet_states)
pose_groups = pet_states.emotion_order.group_by(&:pose)
main_groups =
pose_groups.select { |k| PetState::MAIN_POSES.include?(k) }.values
other_groups =
pose_groups.reject { |k| PetState::MAIN_POSES.include?(k) }.values
if main_groups.empty?
return {canonical: other_groups.flatten(1).sort_by(&:pose), other: []}
end
canonical = main_groups.map(&:first).sort_by(&:pose)
main_others = main_groups.map { |l| l.drop(1) }.flatten(1)
other = (main_others + other_groups.flatten(1)).sort_by(&:pose)
{canonical:, other:}
end end
end end

View file

@ -1,14 +1,11 @@
class PetsController < ApplicationController class PetsController < ApplicationController
rescue_from Pet::PetNotFound, with: :pet_not_found rescue_from Neopets::CustomPets::PetNotFound, with: :pet_not_found
rescue_from PetType::DownloadError, SwfAsset::DownloadError, with: :asset_download_error rescue_from Neopets::CustomPets::DownloadError, with: :pet_download_error
rescue_from Pet::DownloadError, with: :pet_download_error rescue_from Pet::ModelingDisabled, with: :modeling_disabled
rescue_from Pet::UnexpectedDataFormat, with: :unexpected_data_format rescue_from Pet::UnexpectedDataFormat, with: :unexpected_data_format
def load def load
# Uncomment this to temporarily disable modeling for most users. raise Neopets::CustomPets::PetNotFound unless params[:name]
# return modeling_disabled unless user_signed_in? && current_user.admin?
raise Pet::PetNotFound unless params[:name]
@pet = Pet.load(params[:name]) @pet = Pet.load(params[:name])
points = contribute(current_user, @pet) points = contribute(current_user, @pet)
@ -48,12 +45,6 @@ class PetsController < ApplicationController
:status => :not_found :status => :not_found
end end
def asset_download_error(e)
Rails.logger.warn e.message
pet_load_error :long_message => t('pets.load.asset_download_error'),
:status => :gateway_timeout
end
def pet_download_error(e) def pet_download_error(e)
Rails.logger.warn e.message Rails.logger.warn e.message
Rails.logger.warn e.backtrace.join("\n") Rails.logger.warn e.backtrace.join("\n")

View file

@ -12,13 +12,20 @@ class SwfAssetsController < ApplicationController
helpers.image_url("favicon.png"), helpers.image_url("favicon.png"),
@swf_asset.image_url, @swf_asset.image_url,
*@swf_asset.canvas_movie_sprite_urls, *@swf_asset.canvas_movie_sprite_urls,
# For images, `images.neopets.com` is a generally safe host to load
# from (shouldn't be a vulnerable site or exfiltration vector), and
# doing this can help make this header a *lot* shorter, which helps
# our nginx reverse proxy (and probably some clients) handle it. (For
# example, see asset `667993` for "Engulfed in Flames Effect".)
hosts: ["https://images.neopets.com"],
) )
} }
policy.script_src -> { policy.script_src -> {
src_list( src_list(
helpers.javascript_url("lib/easeljs.min"), helpers.javascript_url("easeljs.min"),
helpers.javascript_url("lib/tweenjs.min"), helpers.javascript_url("tweenjs.min"),
helpers.javascript_url("swf_assets/show"), helpers.javascript_url("swf_assets/show"),
@swf_asset.canvas_movie_library_url, @swf_asset.canvas_movie_library_url,
) )
@ -38,7 +45,14 @@ class SwfAssetsController < ApplicationController
private private
def src_list(*urls) def src_list(*urls, hosts: [])
urls.filter(&:present?).map { |url| url.sub(/\?.*\z/, "") }.join(" ") urls.
# Ignore any `nil`s that might arise
filter(&:present?).
# Remove query strings from URLs (they're invalid in CSPs)
map { |url| url.sub(/\?.*\z/, "") }.
# For the given `hosts`, remove all their specific URLs, and just list
# the host itself.
reject { |url| hosts.any? { |h| url.start_with? h } } + hosts
end end
end end

View file

@ -0,0 +1,13 @@
module AltStylesHelper
def view_or_edit_alt_style_url(alt_style)
if support_staff?
edit_alt_style_path alt_style
else
wardrobe_path(
species: alt_style.species_id,
color: alt_style.color_id,
style: alt_style.id,
)
end
end
end

View file

@ -1,6 +1,4 @@
module ApplicationHelper module ApplicationHelper
include FragmentLocalization
def absolute_url(path_or_url) def absolute_url(path_or_url)
if path_or_url.include?('://') # already an absolute URL if path_or_url.include?('://') # already an absolute URL
path_or_url path_or_url
@ -129,10 +127,6 @@ module ApplicationHelper
!@hide_home_link !@hide_home_link
end end
def support_staff?
user_signed_in? && current_user.support_staff?
end
def impress_2020_meta_tags def impress_2020_meta_tags
origin = Rails.configuration.impress_2020_origin origin = Rails.configuration.impress_2020_origin
support_secret = Rails.application.credentials.dig( support_secret = Rails.application.credentials.dig(
@ -148,20 +142,9 @@ module ApplicationHelper
end end
end end
JAVASCRIPT_LIBRARIES = {
:jquery => 'https://ajax.googleapis.com/ajax/libs/jquery/1.4.3/jquery.min.js',
:jquery_tmpl => 'https://ajax.microsoft.com/ajax/jquery.templates/beta1/jquery.tmpl.min.js',
}
def include_javascript_libraries(*library_names)
raw(library_names.inject('') do |html, name|
html + javascript_include_tag(JAVASCRIPT_LIBRARIES[name], defer: true)
end)
end
def locale_options def locale_options
current_locale_is_public = false current_locale_is_public = false
options = I18n.public_locales.map do |available_locale| options = I18n.available_locales.map do |available_locale|
current_locale_is_public = true if I18n.locale == available_locale current_locale_is_public = true if I18n.locale == available_locale
# Include fallbacks data on the tag. Right now it's used in blog # Include fallbacks data on the tag. Right now it's used in blog
# localization, but may conceivably be used for something else later. # localization, but may conceivably be used for something else later.
@ -176,13 +159,6 @@ module ApplicationHelper
options options
end end
def localized_cache(key={}, &block)
localized_key = localize_fragment_key(key, locale)
# TODO: The digest feature is handy, but it's not compatible with how we
# check for fragments existence in the controller, so skip it for now.
cache(localized_key, skip_digest: true, &block)
end
def auth_user_sign_in_path_with_return_to def auth_user_sign_in_path_with_return_to
new_auth_user_session_path :return_to => request.fullpath new_auth_user_session_path :return_to => request.fullpath
@ -237,6 +213,10 @@ module ApplicationHelper
@hide_title_header = true @hide_title_header = true
end end
def hide_after(last_day, &block)
yield if Date.today <= last_day
end
def use_responsive_design def use_responsive_design
@use_responsive_design = true @use_responsive_design = true
add_body_class "use-responsive-design" add_body_class "use-responsive-design"

View file

@ -14,19 +14,30 @@ module ItemsHelper
} }
Sizes = { Sizes = {
face: 1, face: 1, # 50x50
thumb: 2, face_3x: 6, # 150x150
zoom: 3,
full: 4, thumb: 2, # 150x150
face_2x: 6, full: 4, # 300x300
large: 5, # 500x500
xlarge: 7, # 640x640
zoom: 3, # 80x80
autocrop: 9, # <varies>
}
SizeUpgrades = {
face: :face_3x,
thumb: :full,
full: :xlarge,
} }
end end
def pet_type_image_url(pet_type, emotion: :happy, size: :face) def pet_type_image_url(pet_type, emotion: :happy, size: :face)
PetTypeImage::Template.expand( PetTypeImage::Template.expand(
hash: pet_type.basic_image_hash || pet_type.image_hash, hash: pet_type.basic_image_hash || pet_type.image_hash,
emotion: PetTypeImage::Emotions[emotion], emotion: PetTypeImage::Emotions.fetch(emotion),
size: PetTypeImage::Sizes[size], size: PetTypeImage::Sizes.fetch(size),
).to_s ).to_s
end end
@ -246,8 +257,10 @@ module ItemsHelper
def pet_type_image(pet_type, emotion, size, **options) def pet_type_image(pet_type, emotion, size, **options)
src = pet_type_image_url(pet_type, emotion:, size:) src = pet_type_image_url(pet_type, emotion:, size:)
srcset = if size == :face
[[pet_type_image_url(pet_type, emotion:, size: :face_2x), "2x"]] size_2x = PetTypeImage::SizeUpgrades[size]
srcset = if size_2x
[[pet_type_image_url(pet_type, emotion:, size: size_2x), "2x"]]
end end
image_tag(src, srcset:, **options) image_tag(src, srcset:, **options)

View file

@ -1,9 +1,4 @@
module OutfitsHelper module OutfitsHelper
LAST_DAY_OF_ANNOUNCEMENT = Date.parse("2024-09-13")
def show_announcement?
Date.today <= LAST_DAY_OF_ANNOUNCEMENT
end
def destination_tag(value) def destination_tag(value)
hidden_field_tag 'destination', value, :id => nil hidden_field_tag 'destination', value, :id => nil
end end
@ -69,5 +64,12 @@ module OutfitsHelper
options = {:spellcheck => false, :id => nil}.merge(options) options = {:spellcheck => false, :id => nil}.merge(options)
text_field_tag 'name', nil, options text_field_tag 'name', nil, options
end end
def outfit_viewer(outfit=nil, pet_state: nil, **html_options)
outfit = Outfit.new(pet_state:) if outfit.nil? && pet_state.present?
raise "outfit_viewer must have outfit or pet state" if outfit.nil?
render partial: "outfit_viewer", locals: {outfit:, html_options:}
end
end end

View file

@ -0,0 +1,41 @@
module PetStatesHelper
def pose_name(pose)
case pose
when "HAPPY_FEM"
"Happy (Feminine)"
when "HAPPY_MASC"
"Happy (Masculine)"
when "SAD_FEM"
"Sad (Feminine)"
when "SAD_MASC"
"Sad (Masculine)"
when "SICK_FEM"
"Sick (Feminine)"
when "SICK_MASC"
"Sick (Masculine)"
when "UNCONVERTED"
"Unconverted"
else
"Not labeled yet"
end
end
POSE_OPTIONS = %w(HAPPY_FEM SAD_FEM SICK_FEM HAPPY_MASC SAD_MASC SICK_MASC
UNCONVERTED UNKNOWN)
def pose_options
POSE_OPTIONS
end
def useful_pet_state_path(pet_type, pet_state)
if support_staff?
edit_pet_type_pet_state_path(pet_type, pet_state)
else
wardrobe_path(
color: pet_type.color_id,
species: pet_type.species_id,
pose: pet_state.pose,
state: pet_state.id,
)
end
end
end

View file

@ -0,0 +1,16 @@
module PetTypesHelper
def moon_progress(num, total)
nearest_quarter = (4.0 * num / total).round / 4.0
if nearest_quarter >= 1
"🌕️"
elsif nearest_quarter >= 0.75
"🌔"
elsif nearest_quarter >= 0.5
"🌓"
elsif nearest_quarter >= 0.25
"🌒"
else
"🌑"
end
end
end

View file

@ -0,0 +1,60 @@
module SupportFormHelper
class SupportFormBuilder < ActionView::Helpers::FormBuilder
attr_reader :template
delegate :capture, :check_box_tag, :content_tag, :params, :render,
to: :template, private: true
def errors
render partial: "application/support_form/errors", locals: {form: self}
end
def fields(&block)
content_tag(:ul, class: "fields", &block)
end
def field(**options, &block)
content_tag(:li, **options, &block)
end
def radio_fieldset(legend, **options, &block)
render partial: "application/support_form/radio_fieldset",
locals: {form: self, legend:, options:, content: capture(&block)}
end
def radio_field(**options, &block)
content_tag(:li) do
content_tag(:label, **options, &block)
end
end
def radio_grid_fieldset(*args, &block)
radio_fieldset(*args, "data-type": "radio-grid", &block)
end
def thumbnail_input(method)
render partial: "application/support_form/thumbnail_input",
locals: {form: self, method:}
end
def actions(&block)
content_tag(:section, class: "actions", &block)
end
def go_to_next_field(**options, &block)
content_tag(:label, class: "go-to-next", **options, &block)
end
def go_to_next_check_box(value)
check_box_tag "next", value, checked: params[:next] == value
end
end
def support_form_with(**options, &block)
form_with(
builder: SupportFormBuilder,
**options,
class: ["support-form", options[:class]],
&block
)
end
end

View file

@ -1,5 +1,6 @@
import "@hotwired/turbo-rails"; import "@hotwired/turbo-rails";
document.getElementById("locale").addEventListener("change", function () { document.addEventListener("change", (e) => {
if (!e.target.matches("#locale")) return;
document.getElementById("locale-form").submit(); document.getElementById("locale-form").submit();
}); });

View file

@ -777,8 +777,13 @@ function StyleExplanation() {
opacity="0.7" opacity="0.7"
marginTop="2" marginTop="2"
> >
<Box as="a" href="/alt-styles" target="_blank" textDecoration="underline"> <Box
Alt Styles as="a"
href="/rainbow-pool/styles"
target="_blank"
textDecoration="underline"
>
Pet Styles
</Box>{" "} </Box>{" "}
are NC items that override the pet's appearance via the{" "} are NC items that override the pet's appearance via the{" "}
<Box <Box
@ -789,7 +794,7 @@ function StyleExplanation() {
> >
Styling Chamber Styling Chamber
</Box> </Box>
. Not all items fit Alt Style pets. The pet's color doesn't have to match. . Not all items fit all Pet Styles. The pet's color doesn't have to match.
</Box> </Box>
); );
} }

View file

@ -4,49 +4,68 @@ class AltStyle < ApplicationRecord
belongs_to :species belongs_to :species
belongs_to :color belongs_to :color
has_many :parent_swf_asset_relationships, as: :parent has_many :parent_swf_asset_relationships, as: :parent, dependent: :destroy
has_many :swf_assets, through: :parent_swf_asset_relationships has_many :swf_assets, through: :parent_swf_asset_relationships
has_many :contributions, as: :contributed, inverse_of: :contributed has_many :contributions, as: :contributed, inverse_of: :contributed
validates :body_id, presence: true validates :body_id, presence: true
validates :series_name, presence: true, allow_nil: true
validates :thumbnail_url, presence: true
before_create :infer_series_name before_validation :infer_thumbnail_url, unless: :thumbnail_url?
before_create :infer_thumbnail_url
scope :matching_name, ->(series_name, color_name, species_name) { scope :matching_name, ->(series_name, color_name, species_name) {
color = Color.find_by_name!(color_name) color = Color.find_by_name!(color_name)
species = Species.find_by_name!(species_name) species = Species.find_by_name!(species_name)
where(series_name:, color_id: color.id, species_id: species.id) where(series_name:, color_id: color.id, species_id: species.id)
} }
scope :by_creation_date, -> {
order("DATE(created_at) DESC")
}
scope :unlabeled, -> { where(series_name: nil) }
scope :newest, -> { order(created_at: :desc) }
def name def pet_name
I18n.translate('pet_types.human_name', color_human_name: color.human_name, I18n.translate('pet_types.human_name', color_human_name: color.human_name,
species_human_name: species.human_name) species_human_name: species.human_name)
end end
alias_method :name, :pet_name
# If the series_name hasn't yet been set manually by support staff, show the # If the series_name hasn't yet been set manually by support staff, show the
# string "<New?>" instead. But it won't be searchable by that string—that is, # string "<New?>" instead. But it won't be searchable by that string—that is,
# `fits:<New?>-faerie-draik` intentionally will not work, and the canonical # `fits:<New?>-faerie-draik` intentionally will not work, and the canonical
# filter name will be `fits:alt-style-IDNUMBER`, instead. # filter name will be `fits:alt-style-IDNUMBER`, instead.
def series_name def series_name
self[:series_name] || "<New?>" real_series_name || AltStyle.placeholder_name
end
def real_series_name=(new_series_name)
self[:series_name] = new_series_name
end
def real_series_name
self[:series_name]
end end
# You can use this to check whether `series_name` is returning the actual # You can use this to check whether `series_name` is returning the actual
# value or its placeholder value. # value or its placeholder value.
def has_real_series_name? def real_series_name?
self[:series_name].present? real_series_name.present?
end end
def adjective_name def adjective_name
"#{series_name} #{color.human_name}" "#{series_name} #{color.human_name}"
end end
def preview_image_url def full_name
swf_asset = swf_assets.first "#{series_name} #{name}"
return nil if swf_asset.nil? end
swf_asset.image_url EMPTY_IMAGE_URL = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
def preview_image_url
# Use the image URL for the first asset. Or, fall back to an empty image.
swf_assets.first&.image_url || EMPTY_IMAGE_URL
end end
# Given a list of items, return how they look on this alt style. # Given a list of items, return how they look on this alt style.
@ -54,28 +73,6 @@ class AltStyle < ApplicationRecord
Item.appearances_for(items, self, ...) Item.appearances_for(items, self, ...)
end end
def biology=(biology)
# TODO: This is very similar to what `PetState` does, but like… much much
# more compact? Idk if I'm missing something, or if I was just that much
# more clueless back when I wrote it, lol 😅
self.swf_assets = biology.values.map do |asset_data|
SwfAsset.from_biology_data(self.body_id, asset_data)
end
end
# Until the end of 2024, assume new alt styles are from the "Nostalgic"
# series. That way, we can stop having to manually label them all as they
# come out and get modeled (TNT is prolific rn!), but we aren't gonna get too
# greedy and forget about this and use Nostalgic for some far-future thing,
# in ways that will certainly be fixable but would also be confusing and
# embarrassing.
NOSTALGIC_FINAL_DAY = Date.new(2024, 12, 31)
def infer_series_name
if !has_real_series_name? && Date.today <= NOSTALGIC_FINAL_DAY
self.series_name = "Nostalgic"
end
end
# At time of writing, most batches of Alt Styles thumbnails used a simple # At time of writing, most batches of Alt Styles thumbnails used a simple
# pattern for the item thumbnail URL, but that's not always the case anymore. # pattern for the item thumbnail URL, but that's not always the case anymore.
# For now, let's keep using this format as the default value when creating a # For now, let's keep using this format as the default value when creating a
@ -85,7 +82,7 @@ class AltStyle < ApplicationRecord
) )
DEFAULT_THUMBNAIL_URL = "https://images.neopets.com/items/mall_bg_circle.gif" DEFAULT_THUMBNAIL_URL = "https://images.neopets.com/items/mall_bg_circle.gif"
def infer_thumbnail_url def infer_thumbnail_url
if has_real_series_name? if real_series_name?
self.thumbnail_url = THUMBNAIL_URL_TEMPLATE.expand( self.thumbnail_url = THUMBNAIL_URL_TEMPLATE.expand(
series: series_name.gsub(/\s+/, '').downcase, series: series_name.gsub(/\s+/, '').downcase,
color: color.name.gsub(/\s+/, '').downcase, color: color.name.gsub(/\s+/, '').downcase,
@ -96,6 +93,14 @@ class AltStyle < ApplicationRecord
end end
end end
def real_thumbnail_url?
thumbnail_url != DEFAULT_THUMBNAIL_URL
end
def self.placeholder_name
"<New?>"
end
# For convenience in the console! # For convenience in the console!
def self.find_by_name(color_name, species_name) def self.find_by_name(color_name, species_name)
color = Color.find_by_name(color_name) color = Color.find_by_name(color_name)

View file

@ -161,7 +161,7 @@ class AuthUser < AuthRecord
# means we can wrap it in a `with_timeout` block!) # means we can wrap it in a `with_timeout` block!)
neopets_username = Sync do |task| neopets_username = Sync do |task|
task.with_timeout(5) do task.with_timeout(5) do
NeoPass.load_main_neopets_username(auth.credentials.token) Neopets::NeoPass.load_main_neopets_username(auth.credentials.token)
end end
rescue Async::TimeoutError rescue Async::TimeoutError
nil # If the request times out, just move on! nil # If the request times out, just move on!

View file

@ -1,11 +1,11 @@
class Color < ApplicationRecord class Color < ApplicationRecord
has_many :pet_types has_many :pet_types
has_many :alt_styles
scope :alphabetical, -> { order(:name) } scope :alphabetical, -> { order(:name) }
scope :basic, -> { where(basic: true) } scope :basic, -> { where(basic: true) }
scope :standard, -> { where(standard: true) } scope :standard, -> { where(standard: true) }
scope :nonstandard, -> { where(standard: false) } scope :nonstandard, -> { where(standard: false) }
scope :funny, -> { order(:prank) unless pranks_funny? }
validates :name, presence: true validates :name, presence: true
@ -14,27 +14,23 @@ class Color < ApplicationRecord
end end
def human_name def human_name
if prank? && !Color.pranks_funny? if name
unfunny_human_name + ' ' + I18n.translate('colors.prank_suffix') name.split(' ').map { |word| word.capitalize }.join(' ')
else else
unfunny_human_name I18n.translate('colors.default_human_name')
end end
end end
def to_param
name? ? human_name : id.to_s
end
def example_pet_type(preferred_species: nil) def example_pet_type(preferred_species: nil)
preferred_species ||= Species.first preferred_species ||= Species.first
pet_types.order([Arel.sql("species_id = ? DESC"), preferred_species.id], pet_types.order([Arel.sql("species_id = ? DESC"), preferred_species.id],
"species_id ASC").first "species_id ASC").first
end end
def unfunny_human_name
if name
name.split(' ').map { |word| word.capitalize }.join(' ')
else
I18n.translate('colors.default_human_name')
end
end
def default_gender_presentation def default_gender_presentation
if name.downcase.ends_with? "boy" if name.downcase.ends_with? "boy"
:masc :masc
@ -45,8 +41,7 @@ class Color < ApplicationRecord
end end
end end
def self.pranks_funny? def self.param_to_id(param)
now = Time.now.in_time_zone('Pacific Time (US & Canada)') param.match?(/\A\d+\Z/) ? param.to_i : find_by_name!(param).id
now.month == 4 && now.day == 1
end end
end end

View file

@ -10,16 +10,29 @@ class Item < ApplicationRecord
SwfAssetType = 'object' SwfAssetType = 'object'
serialize :cached_compatible_body_ids, coder: Serializers::IntegerSet
serialize :cached_occupied_zone_ids, coder: Serializers::IntegerSet
has_many :closet_hangers has_many :closet_hangers
has_one :contribution, :as => :contributed, :inverse_of => :contributed has_one :contribution, as: :contributed, inverse_of: :contributed
has_one :nc_mall_record has_one :nc_mall_record
has_many :parent_swf_asset_relationships, :as => :parent has_many :parent_swf_asset_relationships, as: :parent
has_many :swf_assets, :through => :parent_swf_asset_relationships has_many :swf_assets, through: :parent_swf_asset_relationships
belongs_to :dyeworks_base_item, class_name: "Item", belongs_to :dyeworks_base_item, class_name: "Item",
default: -> { inferred_dyeworks_base_item }, optional: true default: -> { inferred_dyeworks_base_item }, optional: true
has_many :dyeworks_variants, class_name: "Item", has_many :dyeworks_variants, class_name: "Item",
inverse_of: :dyeworks_base_item inverse_of: :dyeworks_base_item
# We require a name field. A number of other fields must be *specified*: they
# can't be nil, to help ensure we aren't forgetting any fields when importing
# items. But sometimes they happen to be blank (e.g. when TNT leaves an item
# description empty, oops), in which case we want to accept that reality!
validates_presence_of :name
validates :description, :thumbnail_url, :rarity, :price, :zones_restrict,
exclusion: {in: [nil], message: "must be specified"}
after_save :update_cached_fields,
if: :modeling_status_hint_previously_changed?
attr_writer :current_body_id, :owned, :wanted attr_writer :current_body_id, :owned, :wanted
@ -60,39 +73,25 @@ class Item < ApplicationRecord
where('description NOT LIKE ?', where('description NOT LIKE ?',
'%' + sanitize_sql_like(PAINTBRUSH_SET_DESCRIPTION) + '%') '%' + sanitize_sql_like(PAINTBRUSH_SET_DESCRIPTION) + '%')
} }
scope :is_modeled, -> {
where(cached_predicted_fully_modeled: true)
}
scope :is_not_modeled, -> {
where(cached_predicted_fully_modeled: false)
}
scope :occupies, ->(zone_label) { scope :occupies, ->(zone_label) {
zone_ids = Zone.matching_label(zone_label).map(&:id) Zone.matching_label(zone_label).
map { |z| occupies_zone_id(z.id) }.reduce(none, &:or)
# NOTE: In searches, this query performs much better using a subquery
# instead of joins! This is because, in the joins case, filtering by an
# `swf_assets` field but sorting by an `items` field causes the query
# planner to only be able to use an index for *one* of them. In this case,
# MySQL can use the `swf_assets`.`zone_id` index to get the item IDs for
# the subquery, then use the `items`.`name` index to sort them.
i = arel_table
psa = ParentSwfAssetRelationship.arel_table
sa = SwfAsset.arel_table
where(
ParentSwfAssetRelationship.joins(:swf_asset).
where(sa[:zone_id].in(zone_ids)).
where(psa[:parent_type].eq("Item")).
where(psa[:parent_id].eq(i[:id])).
arel.exists
)
} }
scope :not_occupies, ->(zone_label) { scope :not_occupies, ->(zone_label) {
zone_ids = Zone.matching_label(zone_label).map(&:id) Zone.matching_label(zone_label).
i = Item.arel_table map { |z| not_occupies_zone_id(z.id) }.reduce(all, &:and)
sa = SwfAsset.arel_table }
# Querying for "has NO swf_assets matching these zone IDs" is trickier than scope :occupies_zone_id, ->(zone_id) {
# the positive case! To do it, we GROUP_CONCAT the zone_ids together for where("FIND_IN_SET(?, cached_occupied_zone_ids) > 0", zone_id)
# each item, then use FIND_IN_SET to search the result for each zone ID, }
# and assert that it must not find a match. (This is uhh, not exactly fast, scope :not_occupies_zone_id, ->(zone_id) {
# so it helps to have other tighter conditions applied first!) where.not("FIND_IN_SET(?, cached_occupied_zone_ids) > 0", zone_id)
# TODO: I feel like this could also be solved with a LEFT JOIN, idk if that
# performs any better? In Rails 5+ `left_outer_joins` is built in so!
condition = zone_ids.map { 'FIND_IN_SET(?, GROUP_CONCAT(zone_id)) = 0' }.join(' AND ')
joins(:swf_assets).group(i[:id]).having(condition, *zone_ids).distinct
} }
scope :restricts, ->(zone_label) { scope :restricts, ->(zone_label) {
zone_ids = Zone.matching_label(zone_label).map(&:id) zone_ids = Zone.matching_label(zone_label).map(&:id)
@ -105,31 +104,12 @@ class Item < ApplicationRecord
where("NOT (#{condition})", *zone_ids) where("NOT (#{condition})", *zone_ids)
} }
scope :fits, ->(body_id) { scope :fits, ->(body_id) {
joins(:swf_assets).where(swf_assets: {body_id: [body_id, 0]}).distinct where("FIND_IN_SET(?, cached_compatible_body_ids) > 0", body_id).
or(where("FIND_IN_SET('0', cached_compatible_body_ids) > 0"))
} }
scope :not_fits, ->(body_id) { scope :not_fits, ->(body_id) {
i = Item.arel_table where.not("FIND_IN_SET(?, cached_compatible_body_ids) > 0", body_id).
sa = SwfAsset.arel_table and(where.not("FIND_IN_SET('0', cached_compatible_body_ids) > 0"))
# Querying for "has NO swf_assets matching these body IDs" is trickier than
# the positive case! To do it, we GROUP_CONCAT the body_ids together for
# each item, then use FIND_IN_SET to search the result for the body ID,
# and assert that it must not find a match. (This is uhh, not exactly fast,
# so it helps to have other tighter conditions applied first!)
#
# TODO: I feel like this could also be solved with a LEFT JOIN, idk if that
# performs any better? In Rails 5+ `left_outer_joins` is built in so!
#
# NOTE: The `fits` and `not_fits` counts don't perfectly add up to the
# total number of items, 5 items aren't accounted for? I'm not going to
# bother looking into this, but one thing I notice is items with no assets
# somehow would not match either scope in this impl (but LEFT JOIN would!)
joins(:swf_assets).group(i[:id]).
having(
"FIND_IN_SET(?, GROUP_CONCAT(body_id)) = 0 AND " +
"FIND_IN_SET(0, GROUP_CONCAT(body_id)) = 0",
body_id
).
distinct
} }
def nc_trade_value def nc_trade_value
@ -243,8 +223,14 @@ class Item < ApplicationRecord
normalized_name = name.downcase.gsub("female", "girl").gsub("male", "boy"). normalized_name = name.downcase.gsub("female", "girl").gsub("male", "boy").
gsub(/\s/, "") gsub(/\s/, "")
Color.order(:name). # For each color, normalize its name, look for it in the item name, and
find { |c| normalized_name.include?(c.name.downcase.gsub(/\s/, "")) } # return the matching color that appears earliest. (This is important for
# items that contain multiple color names, like the "Royal Girl Elephante
# Gold Bracelets".)
Color.all.to_h { |c| [c, c.name.downcase.gsub(/\s/, "")] }.
transform_values { |n| normalized_name.index(n) }.
filter { |c, n| n.present? }.
min_by { |c, i| i }&.first
end end
# If this is a PB item, return the corresponding Species, inferred from the # If this is a PB item, return the corresponding Species, inferred from the
@ -290,6 +276,23 @@ class Item < ApplicationRecord
restricted_zones + occupied_zones restricted_zones + occupied_zones
end end
def update_cached_fields
# First, clear out some cached instance variables we use for performance,
# to ensure we recompute the latest values.
@predicted_body_ids = nil
@predicted_missing_body_ids = nil
# We also need to reload our associations, so they include any new records.
swf_assets.reload
# Finally, compute and save our cached fields.
self.cached_occupied_zone_ids = occupied_zone_ids
self.cached_compatible_body_ids = compatible_body_ids(use_cached: false)
self.cached_predicted_fully_modeled =
predicted_fully_modeled?(use_cached: false)
self.save!
end
def species_support_ids def species_support_ids
@species_support_ids_array ||= read_attribute('species_support_ids').split(',').map(&:to_i) rescue nil @species_support_ids_array ||= read_attribute('species_support_ids').split(',').map(&:to_i) rescue nil
end end
@ -299,70 +302,83 @@ class Item < ApplicationRecord
replacement = replacement.join(',') if replacement.is_a?(Array) replacement = replacement.join(',') if replacement.is_a?(Array)
write_attribute('species_support_ids', replacement) write_attribute('species_support_ids', replacement)
end end
def support_species?(species)
species_support_ids.blank? || species_support_ids.include?(species.id)
end
def modeled_body_ids def modeling_hinted_done?
@modeled_body_ids ||= swf_assets.select('DISTINCT body_id').map(&:body_id) modeling_status_hint == "done" || modeling_status_hint == "glitchy"
end
def modeled_color_ids
# Might be empty if modeled_body_ids is 0. But it's currently not called
# in that scenario, so, whatever.
@modeled_color_ids ||= PetType.select('DISTINCT color_id').
where(body_id: modeled_body_ids).
map(&:color_id)
end
def basic_body_ids
@basic_body_ids ||= begin
basic_color_ids ||= Color.select([:id]).basic.map(&:id)
PetType.select('DISTINCT body_id').
where(color_id: basic_color_ids).map(&:body_id)
end
end end
def predicted_body_ids def predicted_body_ids
@predicted_body_ids ||= if modeled_body_ids.include?(0) @predicted_body_ids ||= if modeling_hinted_done?
# If we've manually set this item to no longer report as needing modeling,
# predict that the current bodies are all of the compatible bodies.
compatible_body_ids
elsif compatible_body_ids.include?(0)
# Oh, look, it's already known to fit everybody! Sweet. We're done. (This # Oh, look, it's already known to fit everybody! Sweet. We're done. (This
# isn't folded into the case below, in case this item somehow got a # isn't folded into the case below, in case this item somehow got a
# body-specific and non-body-specific asset. In all the cases I've seen # body-specific and non-body-specific asset. In all the cases I've seen
# it, that indicates a glitched item, but this method chooses to reflect # it, that indicates a glitched item, but this method chooses to reflect
# behavior elsewhere in the app by saying that we can put this item on # behavior elsewhere in the app by saying that we can put this item on
# anybody. (Heh. Any body.)) # anybody. (Heh. Any body.))
modeled_body_ids compatible_body_ids
elsif modeled_body_ids.size == 1 elsif compatible_body_ids.size == 1
# This might just be a species-specific item. Let's be conservative in # This might just be a species-specific item. Let's be conservative in
# our prediction, though we'll revise it if we see another body ID. # our prediction, though we'll revise it if we see another body ID.
modeled_body_ids compatible_body_ids
elsif compatible_body_ids.size == 0
# If somehow we have this item, but not any modeling data for it (weird!),
# consider it to fit all standard pet types until shown otherwise.
PetType.basic.released_before(released_at_estimate).
distinct.pluck(:body_id).sort
else else
# If an item is worn by more than one body, then it must be wearable by # First, find our compatible pet types, then pair each body ID with its
# all bodies of the same color. (To my knowledge, anyway. I'm not aware # color. (As an optimization, we omit standard colors, other than the
# of any exceptions.) So, let's find those bodies by first finding those # basic colors. We also flatten the basic colors into the single color
# colors. # ID "basic", so we can treat them specially.)
basic_modeled_body_ids, nonbasic_modeled_body_ids = modeled_body_ids. compatible_pairs = compatible_pet_types.joins(:color).
partition { |bi| basic_body_ids.include?(bi) } merge(Color.nonstandard.or(Color.basic)).
distinct.pluck(
Arel.sql("IF(colors.basic, 'basic', colors.id)"), :body_id)
output = [] # Group colors by body, to help us find bodies unique to certain colors.
if basic_modeled_body_ids.present? compatible_color_ids_by_body_id = {}.tap do |h|
output += basic_body_ids compatible_pairs.each do |(color_id, body_id)|
h[body_id] ||= []
h[body_id] << color_id
end
end end
if nonbasic_modeled_body_ids.present?
nonbasic_modeled_color_ids = PetType.select('DISTINCT color_id'). # Find non-basic colors with at least one unique compatible body. (This
where(body_id: nonbasic_modeled_body_ids). # means we'll ignore e.g. the Maraquan Mynci, which has the same body as
map(&:color_id) # the Blue Mynci, as not indicating Maraquan compatibility in general.)
output += PetType.select('DISTINCT body_id'). modelable_color_ids =
where(color_id: nonbasic_modeled_color_ids). compatible_color_ids_by_body_id.
map(&:body_id) filter { |k, v| v.size == 1 && v.first != "basic" }.
end values.map(&:first).uniq
output
# We can model on basic pets (perhaps in addition to the above) if we
# find at least one compatible basic body that doesn't *also* fit any of
# the modelable colors we identified above.
basic_is_modelable =
compatible_color_ids_by_body_id.values.
any? { |v| v.include?("basic") && (v & modelable_color_ids).empty? }
# Filter to pet types that match the colors that seem compatible.
predicted_pet_types =
(basic_is_modelable ? PetType.basic : PetType.none).
or(PetType.where(color_id: modelable_color_ids))
# Only include species that were released when this item was. If we don't
# know our creation date (we don't have it for some old records), assume
# it's pretty old.
predicted_pet_types.merge! PetType.released_before(released_at_estimate)
# Get all body IDs for the pet types we decided are modelable.
predicted_pet_types.distinct.pluck(:body_id).sort
end end
end end
def predicted_missing_body_ids def predicted_missing_body_ids
@predicted_missing_body_ids ||= predicted_body_ids - modeled_body_ids @predicted_missing_body_ids ||= predicted_body_ids - compatible_body_ids
end end
def predicted_missing_standard_body_ids_by_species_id def predicted_missing_standard_body_ids_by_species_id
@ -382,9 +398,8 @@ class Item < ApplicationRecord
end end
def predicted_missing_nonstandard_body_pet_types def predicted_missing_nonstandard_body_pet_types
PetType.joins(:color). body_ids = predicted_missing_body_ids - PetType.basic_body_ids
where(body_id: predicted_missing_body_ids - basic_body_ids, PetType.joins(:color).where(body_id: body_ids, colors: {standard: false})
colors: {standard: false})
end end
def predicted_missing_nonstandard_body_ids_by_species_by_color def predicted_missing_nonstandard_body_ids_by_species_by_color
@ -409,12 +424,19 @@ class Item < ApplicationRecord
body_ids_by_species_by_color body_ids_by_species_by_color
end end
def predicted_fully_modeled? def predicted_fully_modeled?(use_cached: true)
return cached_predicted_fully_modeled? if use_cached
predicted_missing_body_ids.empty? predicted_missing_body_ids.empty?
end end
def predicted_modeled_ratio def predicted_modeled_ratio
modeled_body_ids.size.to_f / predicted_body_ids.size compatible_body_ids.size.to_f / predicted_body_ids.size
end
# We estimate the item's release time as either when we first saw it, or 2010
# if it's so old that we don't have a record.
def released_at_estimate
created_at || Time.new(2010)
end end
def as_json(options={}) def as_json(options={})
@ -424,7 +446,9 @@ class Item < ApplicationRecord
}.merge(options)) }.merge(options))
end end
def compatible_body_ids def compatible_body_ids(use_cached: true)
return cached_compatible_body_ids if use_cached
swf_assets.map(&:body_id).uniq swf_assets.map(&:body_id).uniq
end end

View file

@ -117,7 +117,7 @@ class Item
)\z )\z
}x }x
def inferred_dyeworks_base_item def inferred_dyeworks_base_item
name_match = name.match(DYEWORKS_NAME_PATTERN) name_match = (name || "").match(DYEWORKS_NAME_PATTERN)
return nil if name_match.nil? return nil if name_match.nil?
Item.find_by_name(name_match["base"]) Item.find_by_name(name_match["base"])

View file

@ -132,6 +132,8 @@ class Item
is_positive ? Filter.is_np : Filter.is_not_np is_positive ? Filter.is_np : Filter.is_not_np
when 'pb' when 'pb'
is_positive ? Filter.is_pb : Filter.is_not_pb is_positive ? Filter.is_pb : Filter.is_not_pb
when 'modeled'
is_positive ? Filter.is_modeled : Filter.is_not_modeled
else else
raise_search_error "not_found.label", label: "is:#{value}" raise_search_error "not_found.label", label: "is:#{value}"
end end
@ -346,6 +348,14 @@ class Item
self.new Item.is_not_pb, '-is:pb' self.new Item.is_not_pb, '-is:pb'
end end
def self.is_modeled
self.new Item.is_modeled, 'is:modeled'
end
def self.is_not_modeled
self.new Item.is_not_modeled, '-is:modeled'
end
private private
# Add quotes around the value, if needed. # Add quotes around the value, if needed.
@ -367,7 +377,7 @@ class Item
# If the real series name has been set in the database by support # If the real series name has been set in the database by support
# staff, use that for the canonical filter text for this alt style. # staff, use that for the canonical filter text for this alt style.
# Otherwise, represent this alt style by ID. # Otherwise, represent this alt style by ID.
if alt_style.has_real_series_name? if alt_style.real_series_name?
series_name = alt_style.series_name.downcase series_name = alt_style.series_name.downcase
color_name = alt_style.color.name.downcase color_name = alt_style.color.name.downcase
species_name = alt_style.species.name.downcase species_name = alt_style.species.name.downcase

View file

@ -4,6 +4,9 @@ class ParentSwfAssetRelationship < ApplicationRecord
belongs_to :parent, :polymorphic => true belongs_to :parent, :polymorphic => true
belongs_to :swf_asset belongs_to :swf_asset
after_save :update_parent_cached_fields
after_destroy :update_parent_cached_fields
def item=(replacement) def item=(replacement)
self.parent = replacement self.parent = replacement
@ -16,4 +19,8 @@ class ParentSwfAssetRelationship < ApplicationRecord
def pet_state=(replacement) def pet_state=(replacement)
self.parent = replacement self.parent = replacement
end end
def update_parent_cached_fields
parent.try(:update_cached_fields)
end
end end

View file

@ -1,82 +1,20 @@
require 'rocketamf_extensions/remote_gateway'
require 'ostruct'
class Pet < ApplicationRecord class Pet < ApplicationRecord
NEOPETS_URL_ORIGIN = ENV['NEOPETS_URL_ORIGIN'] || 'https://www.neopets.com'
GATEWAY_URL = NEOPETS_URL_ORIGIN + '/amfphp/gateway.php'
GATEWAY = RocketAMFExtensions::RemoteGateway.new(GATEWAY_URL)
CUSTOM_PET_SERVICE = GATEWAY.service('CustomPetService')
PET_SERVICE = GATEWAY.service('PetService')
belongs_to :pet_type belongs_to :pet_type
attr_reader :items, :pet_state, :alt_style attr_reader :items, :pet_state, :alt_style
scope :with_pet_type_color_ids, ->(color_ids) {
joins(:pet_type).where(PetType.arel_table[:id].in(color_ids))
}
def load!(timeout: nil) def load!(timeout: nil)
viewer_data = self.class.fetch_viewer_data(name, timeout:) raise ModelingDisabled unless Rails.configuration.modeling_enabled
use_viewer_data(viewer_data)
viewer_data_hash = Neopets::CustomPets.fetch_viewer_data(name, timeout:)
use_modeling_snapshot(ModelingSnapshot.new(viewer_data_hash))
end end
def use_viewer_data(viewer_data) def use_modeling_snapshot(snapshot)
pet_data = viewer_data[:custom_pet] self.pet_type = snapshot.pet_type
@pet_state = snapshot.pet_state
raise UnexpectedDataFormat unless pet_data[:species_id] @alt_style = snapshot.alt_style
raise UnexpectedDataFormat unless pet_data[:color_id] @items = snapshot.items
raise UnexpectedDataFormat unless pet_data[:body_id]
has_alt_style = pet_data[:alt_style].present?
self.pet_type = PetType.find_or_initialize_by(
species_id: pet_data[:species_id].to_i,
color_id: pet_data[:color_id].to_i
)
begin
new_image_hash = Pet.fetch_image_hash(self.name)
rescue => error
Rails.logger.warn "Failed to load image hash: #{error.full_message}"
end
self.pet_type.image_hash = new_image_hash if new_image_hash.present?
# With an alt style, `body_id` in the biology data refers to the body ID of
# the *alt* style, not the usual pet type. (We have `original_biology` for
# *some* of the pet type's situation, but not it's body ID!)
#
# So, in the alt style case, don't update `body_id` - but if this is our
# first time seeing this pet type and it doesn't *have* a `body_id` yet,
# let's not be creating it without one. We'll need to model it without the
# alt style first. (I don't bother with a clear error message though 😅)
self.pet_type.body_id = pet_data[:body_id] unless has_alt_style
if self.pet_type.body_id.nil?
raise UnexpectedDataFormat,
"can't process alt style on first occurrence of pet type"
end
pet_state_biology = has_alt_style ? pet_data[:original_biology] :
pet_data[:biology_by_zone]
raise UnexpectedDataFormat if pet_state_biology.empty?
pet_state_biology[0] = nil # remove effects if present
@pet_state = self.pet_type.add_pet_state_from_biology! pet_state_biology
if has_alt_style
raise UnexpectedDataFormat unless pet_data[:alt_color]
raise UnexpectedDataFormat if pet_data[:biology_by_zone].empty?
@alt_style = AltStyle.find_or_initialize_by(id: pet_data[:alt_style].to_i)
@alt_style.assign_attributes(
color_id: pet_data[:alt_color].to_i,
species_id: pet_data[:species_id].to_i,
body_id: pet_data[:body_id].to_i,
biology: pet_data[:biology_by_zone],
)
end
@items = Item.collection_from_pet_type_and_registries(self.pet_type,
viewer_data[:object_info_registry], viewer_data[:object_asset_registry])
end end
def wardrobe_query def wardrobe_query
@ -87,6 +25,7 @@ class Pet < ApplicationRecord
pose: self.pet_state.pose, pose: self.pet_state.pose,
state: self.pet_state.id, state: self.pet_state.id,
objects: self.items.map(&:id), objects: self.items.map(&:id),
style: self.alt_style ? self.alt_style.id : nil,
}.to_query }.to_query
end end
@ -101,11 +40,8 @@ class Pet < ApplicationRecord
before_validation do before_validation do
pet_type.save! pet_type.save!
if @pet_state @pet_state.save! if @pet_state
@pet_state.save!
@pet_state.handle_assets!
end
if @items if @items
@items.each do |item| @items.each do |item|
item.save! if item.changed? item.save! if item.changed?
@ -124,60 +60,6 @@ class Pet < ApplicationRecord
pet pet
end end
# NOTE: Ideally pet requests shouldn't take this long, but Neopets can be
# slow sometimes! Since we're on the Falcon server, long timeouts shouldn't
# slow down the rest of the request queue, like it used to be in the past.
def self.fetch_viewer_data(name, timeout: 10)
request = CUSTOM_PET_SERVICE.action('getViewerData').request([name])
send_amfphp_request(request).tap do |data|
if data[:custom_pet][:name].blank?
raise PetNotFound, "Pet #{name.inspect} does not exist"
end
end
end
def self.fetch_metadata(name, timeout: 10)
# If this is an image hash "pet name", it has no metadata.
return nil if name.start_with?("@")
request = PET_SERVICE.action('getPet').request([name])
send_amfphp_request(request).tap do |data|
if data[:name].blank?
raise PetNotFound, "Pet #{name.inspect} does not exist"
end
end
end
# Given a pet's name, load its image hash, for use in `pets.neopets.com`
# image URLs. (This corresponds to its current biology and items.)
def self.fetch_image_hash(name, timeout: 10)
# If this is an image hash "pet name", just take off the `@`!
return name[1..] if name.start_with?("@")
metadata = fetch_metadata(name, timeout:)
metadata[:hash]
end
class PetNotFound < RuntimeError;end
class DownloadError < RuntimeError;end
class UnexpectedDataFormat < RuntimeError;end class UnexpectedDataFormat < RuntimeError;end
class ModelingDisabled < RuntimeError;end
private
# Send an AMFPHP request, re-raising errors as `Pet::DownloadError`.
# Return the response body as a `HashWithIndifferentAccess`.
def self.send_amfphp_request(request, timeout: 10)
begin
response = request.post(timeout: timeout, headers: {
"User-Agent" => Rails.configuration.user_agent_for_neopets,
})
rescue RocketAMFExtensions::RemoteGateway::AMFError => e
raise DownloadError, e.message
rescue RocketAMFExtensions::RemoteGateway::ConnectionError => e
raise DownloadError, e.message, e.backtrace
end
HashWithIndifferentAccess.new(response.messages[0].data.body)
end
end end

View file

@ -0,0 +1,104 @@
# A representation of a Neopets::CustomPets viewer data response, translated
# to DTI's database models!
class Pet::ModelingSnapshot
def initialize(viewer_data_hash)
@custom_pet = viewer_data_hash[:custom_pet]
@object_info_registry = viewer_data_hash[:object_info_registry]
@object_asset_registry = viewer_data_hash[:object_asset_registry]
end
def pet_type
@pet_type ||= begin
raise Pet::UnexpectedDataFormat unless @custom_pet[:species_id]
raise Pet::UnexpectedDataFormat unless @custom_pet[:color_id]
raise Pet::UnexpectedDataFormat unless @custom_pet[:body_id]
@custom_pet => {species_id:, color_id:}
PetType.find_or_initialize_by(species_id:, color_id:).tap do |pet_type|
# Apply the pet's body ID to the pet type, unless it's wearing an alt
# style, in which case ignore it, because it's the *alt style*'s body ID.
# (This can theoretically cause a problem saving a new pet type when
# there's an alt style too!)
pet_type.body_id = @custom_pet[:body_id] unless @custom_pet[:alt_style]
if pet_type.body_id.nil?
raise Pet::UnexpectedDataFormat,
"can't process alt style on first occurrence of pet type"
end
# Try using this pet for the pet type's thumbnail, but don't worry
# if it fails.
begin
pet_type.consider_pet_image(@custom_pet[:name])
rescue => error
Rails.logger.warn "Failed to load pet image: #{error.full_message}"
end
end
end
end
def pet_state
@pet_state ||= begin
swf_asset_ids = biology_assets.map(&:remote_id)
pet_type.pet_states.find_or_initialize_by(swf_asset_ids:).tap do |pet_state|
pet_state.swf_assets = biology_assets
end
end
end
def alt_style
@alt_style ||= begin
return nil unless @custom_pet[:alt_style]
raise Pet::UnexpectedDataFormat unless @custom_pet[:alt_color]
id = @custom_pet[:alt_style].to_i
AltStyle.find_or_initialize_by(id:).tap do |alt_style|
alt_style.assign_attributes(
color_id: @custom_pet[:alt_color].to_i,
species_id: @custom_pet[:species_id].to_i,
body_id: @custom_pet[:body_id].to_i,
swf_assets: alt_style_assets,
)
end
end
end
def items
@items ||= Item.collection_from_pet_type_and_registries(
pet_type, @object_info_registry, @object_asset_registry
)
end
private
def biology_assets
@biology_assets ||= begin
biology = @custom_pet[:alt_style].present? ?
@custom_pet[:original_biology] :
@custom_pet[:biology_by_zone]
assets_from_biology(biology)
end
end
def item_assets_for(item_id)
all_infos = @object_asset_registry.values
infos = all_infos.select { |a| a[:obj_info_id].to_i == item_id.to_i }
infos.map do |asset_data|
remote_id = asset_data[:asset_id].to_i
SwfAsset.find_or_initialize_by(type: "object", remote_id:).tap do |swf_asset|
swf_asset.origin_pet_type = pet_type
swf_asset.origin_object_data = asset_data
end
end
end
def alt_style_assets
raise Pet::UnexpectedDataFormat if @custom_pet[:biology_by_zone].empty?
assets_from_biology(@custom_pet[:biology_by_zone])
end
def assets_from_biology(biology)
raise Pet::UnexpectedDataFormat if biology.empty?
body_id = @custom_pet[:body_id].to_i
biology.values.map { |b| SwfAsset.from_biology_data(body_id, b) }
end
end

View file

@ -1,20 +1,29 @@
class PetState < ApplicationRecord class PetState < ApplicationRecord
SwfAssetType = 'biology' SwfAssetType = 'biology'
MAIN_POSES = %w(HAPPY_FEM HAPPY_MASC SAD_FEM SAD_MASC SICK_FEM SICK_MASC)
has_many :contributions, :as => :contributed, has_many :contributions, :as => :contributed,
:inverse_of => :contributed # in case of duplicates being merged :inverse_of => :contributed # in case of duplicates being merged
has_many :outfits has_many :outfits
has_many :parent_swf_asset_relationships, :as => :parent, has_many :parent_swf_asset_relationships, :as => :parent
:autosave => false
has_many :swf_assets, :through => :parent_swf_asset_relationships has_many :swf_assets, :through => :parent_swf_asset_relationships
serialize :swf_asset_ids, coder: Serializers::IntegerSet, type: Array
belongs_to :pet_type belongs_to :pet_type
delegate :color, to: :pet_type delegate :species_id, :species, :color_id, :color, to: :pet_type
alias_method :swf_asset_ids_from_association, :swf_asset_ids alias_method :swf_asset_ids_from_association, :swf_asset_ids
attr_writer :parent_swf_asset_relationships_to_update scope :glitched, -> { where(glitched: true) }
scope :needs_labeling, -> { unlabeled.where(glitched: false) }
scope :unlabeled, -> { with_pose("UNKNOWN") }
scope :usable, -> { where(labeled: true, glitched: false) }
scope :newest, -> { order(created_at: :desc) }
scope :newest_pet_type, -> { joins(:pet_type).merge(PetType.newest) }
# A simple ordering that tries to bring reliable pet states to the front. # A simple ordering that tries to bring reliable pet states to the front.
scope :emotion_order, -> { scope :emotion_order, -> {
@ -71,105 +80,73 @@ class PetState < ApplicationRecord
end end
end end
def reassign_children_to!(main_pet_state) # TODO: More and more, wanting to refactor poses…
self.contributions.each do |contribution| def pose=(pose)
contribution.contributed = main_pet_state case pose
contribution.save when "UNKNOWN"
end label_pose nil, nil, unconverted: nil, labeled: false
self.outfits.each do |outfit| when "HAPPY_MASC"
outfit.pet_state = main_pet_state label_pose 1, false
outfit.save when "HAPPY_FEM"
end label_pose 1, true
ParentSwfAssetRelationship.where(ParentSwfAssetRelationship.arel_table[:parent_id].eq(self.id)).delete_all when "SAD_MASC"
end label_pose 2, false
when "SAD_FEM"
def reassign_duplicates! label_pose 2, true
raise "This may only be applied to pet states that represent many duplicate entries" unless duplicate_ids when "SICK_MASC"
pet_states = duplicate_ids.split(',').map do |id| label_pose 4, false
PetState.find(id.to_i) when "SICK_FEM"
end label_pose 4, true
main_pet_state = pet_states.shift when "UNCONVERTED"
pet_states.each do |pet_state| label_pose nil, nil, unconverted: true
pet_state.reassign_children_to!(main_pet_state)
pet_state.destroy
end end
end end
def sort_swf_asset_ids! def to_param
self.swf_asset_ids = swf_asset_ids_array.sort.join(',') "#{id}-#{pose.split('_').map(&:capitalize).join('-')}"
end end
def swf_asset_ids # Because our column is named `swf_asset_ids`, we need to ensure writes to
self['swf_asset_ids'] # it go to the attribute, and not the thing ActiveRecord does of finding the
# relevant `swf_assets`.
# TODO: Consider renaming the column to `cached_swf_asset_ids`?
def swf_asset_ids=(new_swf_asset_ids)
write_attribute(:swf_asset_ids, new_swf_asset_ids)
end end
def swf_asset_ids_array private
swf_asset_ids.split(',').map(&:to_i)
# A helper for the `pose=` method.
def label_pose(mood_id, female, unconverted: false, labeled: true)
self.labeled = labeled
self.mood_id = mood_id
self.female = female
self.unconverted = unconverted
end end
def swf_asset_ids=(ids) def self.last_updated_key
self['swf_asset_ids'] = ids PetState.maximum(:updated_at)
end
def handle_assets!
@parent_swf_asset_relationships_to_update.each do |rel|
rel.swf_asset.save!
rel.save!
end
end end
def self.from_pet_type_and_biology_info(pet_type, info) def self.all_supported_poses
swf_asset_ids = [] Rails.cache.fetch("PetState.all_supported_poses #{last_updated_key}") do
info.each do |zone_id, asset_info| {}.tap do |h|
if zone_id.present? && asset_info includes(:pet_type).find_each do |pet_state|
swf_asset_ids << asset_info[:part_id].to_i h[pet_state.species_id] ||= {}
end h[pet_state.species_id][pet_state.color_id] ||= []
end h[pet_state.species_id][pet_state.color_id] << pet_state.pose
swf_asset_ids_str = swf_asset_ids.sort.join(',')
if pet_type.new_record?
pet_state = self.new :swf_asset_ids => swf_asset_ids_str
else
pet_state = self.find_or_initialize_by(
pet_type_id: pet_type.id,
swf_asset_ids: swf_asset_ids_str
)
end
existing_swf_assets = SwfAsset.biology_assets.includes(:zone).
where(remote_id: swf_asset_ids)
existing_swf_assets_by_id = {}
existing_swf_assets.each do |swf_asset|
existing_swf_assets_by_id[swf_asset.remote_id] = swf_asset
end
existing_relationships_by_swf_asset_id = {}
unless pet_state.new_record?
pet_state.parent_swf_asset_relationships.each do |relationship|
existing_relationships_by_swf_asset_id[relationship.swf_asset_id] = relationship
end
end
pet_state.pet_type = pet_type # save the second case from having to look it up by ID
relationships = []
info.each do |zone_id, asset_info|
if zone_id.present? && asset_info
swf_asset_id = asset_info[:part_id].to_i
swf_asset = existing_swf_assets_by_id[swf_asset_id]
unless swf_asset
swf_asset = SwfAsset.new
swf_asset.remote_id = swf_asset_id
end end
swf_asset.origin_biology_data = asset_info
swf_asset.origin_pet_type = pet_type h.values.map(&:values).flatten(1).each(&:uniq!).each(&:sort!)
relationship = existing_relationships_by_swf_asset_id[swf_asset.id]
unless relationship
relationship ||= ParentSwfAssetRelationship.new
relationship.parent = pet_state
relationship.swf_asset_id = swf_asset.id
end
relationship.swf_asset = swf_asset
relationships << relationship
end end
end end
pet_state.parent_swf_asset_relationships_to_update = relationships end
pet_state
def self.next_unlabeled_appearance
# Rather than just getting the newest unlabeled pet state, prioritize the
# newest *pet type*. This better matches the user's perception of what the
# newest state is, because the Rainbow Pool UI is grouped by pet type!
needs_labeling.newest_pet_type.newest.first
end end
end end

View file

@ -9,14 +9,13 @@ class PetType < ApplicationRecord
has_many :pet_states has_many :pet_states
has_many :pets has_many :pets
BasicHashes = YAML::load_file(Rails.root.join('config', 'basic_type_hashes.yml'))
scope :basic, -> { joins(:color).merge(Color.basic) } scope :basic, -> { joins(:color).merge(Color.basic) }
scope :matching_name, ->(color_name, species_name) { scope :matching_name, ->(color_name, species_name) {
color = Color.find_by_name!(color_name) color = Color.find_by_name!(color_name)
species = Species.find_by_name!(species_name) species = Species.find_by_name!(species_name)
where(color_id: color.id, species_id: species.id) where(color_id: color.id, species_id: species.id)
} }
scope :newest, -> { order(created_at: :desc) }
scope :preferring_species, ->(species_id) { scope :preferring_species, ->(species_id) {
joins(:species).order([Arel.sql("species_id = ? DESC"), species_id]) joins(:species).order([Arel.sql("species_id = ? DESC"), species_id])
} }
@ -28,6 +27,10 @@ class PetType < ApplicationRecord
merge(Species.order(name: :asc)). merge(Species.order(name: :asc)).
merge(Color.order(basic: :desc, standard: :desc, name: :asc)) merge(Color.order(basic: :desc, standard: :desc, name: :asc))
} }
scope :released_before, ->(time) {
# We use DTI's creation timestamp as an estimate of when it was released.
where('created_at <= ?', time)
}
def self.random_basic_per_species(species_ids) def self.random_basic_per_species(species_ids)
random_pet_types = [] random_pet_types = []
@ -52,17 +55,15 @@ class PetType < ApplicationRecord
# Otherwise, refer to the fallback YAML file (though, if we have our # Otherwise, refer to the fallback YAML file (though, if we have our
# basic image hashes set correctly, the fallbacks should just be an old # basic image hashes set correctly, the fallbacks should just be an old
# subset of the basic image hashes in the database.) # subset of the basic image hashes in the database.)
basic_image_hash || self['image_hash'] || fallback_image_hash basic_image_hash || self['image_hash'] || 'deadbeef'
end end
def fallback_image_hash def consider_pet_image(pet_name)
I18n.with_locale(I18n.default_locale) do # If we already have a basic image hash, don't worry about it!
if species && color && BasicHashes[species.name] && BasicHashes[species.name][color.name] return if basic_image_hash?
BasicHashes[species.name][color.name]
else # Otherwise, use this as the new image hash for this pet type.
return 'deadbeef' self.image_hash = Neopets::CustomPets.fetch_image_hash(pet_name)
end
end
end end
def possibly_new_color def possibly_new_color
@ -79,11 +80,6 @@ class PetType < ApplicationRecord
species_human_name: possibly_new_species.human_name) species_human_name: possibly_new_species.human_name)
end end
def add_pet_state_from_biology!(biology)
pet_state = PetState.from_pet_type_and_biology_info(self, biology)
pet_state
end
def canonical_pet_state def canonical_pet_state
# For consistency (randomness is always scary!), we use the PetType ID to # For consistency (randomness is always scary!), we use the PetType ID to
# determine which gender to prefer, if it's not built into the color. That # determine which gender to prefer, if it's not built into the color. That
@ -120,6 +116,44 @@ class PetType < ApplicationRecord
Item.appearances_for(item, self, ...) Item.appearances_for(item, self, ...)
end end
def to_param
"#{possibly_new_color.to_param}-#{possibly_new_species.to_param}"
end
def fully_labeled?
num_missing_poses == 0
end
def num_poses
all_poses = pet_states.map(&:pose)
PetState::MAIN_POSES.count { |pose| all_poses.include? pose }
end
def num_missing_poses
PetState::MAIN_POSES.count - num_poses
end
def num_unlabeled_states
pet_states.count { |ps| ps.pose == "UNKNOWN" }
end
def reference
PetType.where(species_id: species).basic.merge(Color.alphabetical).first
end
def self.find_by_param!(param)
raise ActiveRecord::RecordNotFound unless param.include?("-")
color_param, _, species_param = param.rpartition("-")
where(
color_id: Color.param_to_id(color_param),
species_id: Species.param_to_id(species_param),
).first!
end
def self.basic_body_ids
PetType.basic.distinct.pluck(:body_id)
end
def self.all_by_ids_or_children(ids, pet_states) def self.all_by_ids_or_children(ids, pet_states)
pet_states_by_pet_type_id = {} pet_states_by_pet_type_id = {}
pet_states.each do |pet_state| pet_states.each do |pet_state|
@ -139,7 +173,5 @@ class PetType < ApplicationRecord
end end
end end
end end
class DownloadError < Exception;end
end end

View file

@ -16,6 +16,10 @@ class Species < ApplicationRecord
end end
end end
def to_param
name? ? human_name : id.to_s
end
# Given a list of body IDs, return a hash from body ID to Species. # Given a list of body IDs, return a hash from body ID to Species.
# (We assume that each body ID belongs to just one species; if not, which # (We assume that each body ID belongs to just one species; if not, which
# species we return for that body ID is undefined.) # species we return for that body ID is undefined.)
@ -26,4 +30,8 @@ class Species < ApplicationRecord
to_h { |s| [s.id, s] } to_h { |s| [s.id, s] }
species_ids_by_body_id.transform_values { |id| species_by_id[id] } species_ids_by_body_id.transform_values { |id| species_by_id[id] }
end end
def self.param_to_id(param)
param.match?(/\A\d+\Z/) ? param.to_i : find_by_name!(param).id
end
end end

View file

@ -2,8 +2,6 @@ require 'addressable/template'
require 'async' require 'async'
require 'async/barrier' require 'async/barrier'
require 'async/semaphore' require 'async/semaphore'
require 'fileutils'
require 'uri'
class SwfAsset < ApplicationRecord class SwfAsset < ApplicationRecord
# We use the `type` column to mean something other than what Rails means! # We use the `type` column to mean something other than what Rails means!
@ -322,14 +320,6 @@ class SwfAsset < ApplicationRecord
swf_asset swf_asset
end end
def self.from_wardrobe_link_params(ids)
where((
arel_table[:remote_id].in(ids[:biology]).and(arel_table[:type].eq('biology'))
).or(
arel_table[:remote_id].in(ids[:object]).and(arel_table[:type].eq('object'))
))
end
# Given a list of SWF assets, ensure all of their manifests are loaded, with # Given a list of SWF assets, ensure all of their manifests are loaded, with
# fast concurrent execution! # fast concurrent execution!
def self.preload_manifests(swf_assets) def self.preload_manifests(swf_assets)
@ -373,6 +363,4 @@ class SwfAsset < ApplicationRecord
# linked to it, meaning that it's probably wearable by all bodies. # linked to it, meaning that it's probably wearable by all bodies.
self.body_id = 0 if !@body_id_overridden && (!self.body_specific? || (!self.new_record? && self.body_id_changed?)) self.body_id = 0 if !@body_id_overridden && (!self.body_specific? || (!self.new_record? && self.body_id_changed?))
end end
class DownloadError < Exception;end
end end

View file

@ -0,0 +1,67 @@
require 'rocketamf_extensions/remote_gateway'
module Neopets::CustomPets
GATEWAY_URL =
Addressable::URI.parse(Rails.configuration.neopets_origin) +
'/amfphp/gateway.php'
GATEWAY = RocketAMFExtensions::RemoteGateway.new(GATEWAY_URL)
CUSTOM_PET_SERVICE = GATEWAY.service('CustomPetService')
PET_SERVICE = GATEWAY.service('PetService')
class << self
# NOTE: Ideally pet requests shouldn't take this long, but Neopets can be
# slow sometimes! Since we're on the Falcon server, long timeouts shouldn't
# slow down the rest of the request queue, like it used to be in the past.
def fetch_viewer_data(name, timeout: 10)
request = CUSTOM_PET_SERVICE.action('getViewerData').request([name])
send_amfphp_request(request).tap do |data|
if data[:custom_pet][:name].blank?
raise PetNotFound, "Pet #{name.inspect} does not exist"
end
end
end
def fetch_metadata(name, timeout: 10)
# If this is an image hash "pet name", it has no metadata.
return nil if name.start_with?("@")
request = PET_SERVICE.action('getPet').request([name])
send_amfphp_request(request).tap do |data|
if data[:name].blank?
raise PetNotFound, "Pet #{name.inspect} does not exist"
end
end
end
# Given a pet's name, load its image hash, for use in `pets.neopets.com`
# image URLs. (This corresponds to its current biology and items.)
def fetch_image_hash(name, timeout: 10)
# If this is an image hash "pet name", just take off the `@`!
return name[1..] if name.start_with?("@")
metadata = fetch_metadata(name, timeout:)
metadata[:hash]
end
private
# Send an AMFPHP request, re-raising errors as `DownloadError`.
# Return the response body as a `HashWithIndifferentAccess`.
def send_amfphp_request(request, timeout: 10)
begin
response_data = request.post(timeout: timeout, headers: {
"User-Agent" => Rails.configuration.user_agent_for_neopets,
})
rescue RocketAMFExtensions::RemoteGateway::AMFError => e
raise DownloadError, e.message
rescue RocketAMFExtensions::RemoteGateway::ConnectionError => e
raise DownloadError, e.message, e.backtrace
end
HashWithIndifferentAccess.new(response_data)
end
end
class PetNotFound < RuntimeError;end
class DownloadError < RuntimeError;end
end

View file

@ -1,7 +1,7 @@
require "addressable/template" require "addressable/template"
require "async/http/internet/instance" require "async/http/internet/instance"
module NCMall module Neopets::NCMall
# Share a pool of persistent connections, rather than reconnecting on # Share a pool of persistent connections, rather than reconnecting on
# each request. (This library does that automatically!) # each request. (This library does that automatically!)
INTERNET = Async::HTTP::Internet.instance INTERNET = Async::HTTP::Internet.instance
@ -45,6 +45,37 @@ module NCMall
uniq uniq
end end
STYLING_STUDIO_URL = "https://www.neopets.com/np-templates/ajax/stylingstudio/studio.php"
def self.load_styles(species_id:, neologin:)
Sync do
INTERNET.post(
STYLING_STUDIO_URL,
headers: [
["User-Agent", Rails.configuration.user_agent_for_neopets],
["Content-Type", "application/x-www-form-urlencoded"],
["Cookie", "neologin=#{neologin}"],
["X-Requested-With", "XMLHttpRequest"],
],
body: {tab: 1, mode: "getStyles", species: species_id}.to_query,
) do |response|
if response.status != 200
raise ResponseNotOK.new(response.status),
"expected status 200 but got #{response.status} (#{STYLING_STUDIO_URL})"
end
begin
data = JSON.parse(response.read).deep_symbolize_keys
# HACK: styles is a hash, unless it's empty, in which case it's an
# array? Weird. Normalize this by converting to hash.
data.fetch(:styles).to_h.values
rescue JSON::ParserError, KeyError
raise UnexpectedResponseFormat
end
end
end
end
private private
def self.load_page_by_url(url) def self.load_page_by_url(url)
@ -76,11 +107,20 @@ module NCMall
raise UnexpectedResponseFormat, "missing field object_data in NC page" raise UnexpectedResponseFormat, "missing field object_data in NC page"
end end
object_data = nc_page["object_data"]
# NOTE: When there's no object data, it will be an empty array instead of # NOTE: When there's no object data, it will be an empty array instead of
# an empty hash. Weird API thing to work around! # an empty hash. Weird API thing to work around!
nc_page["object_data"] = {} if nc_page["object_data"] == [] object_data = {} if object_data == []
items = nc_page["object_data"].values.map do |item_info| # Only the items in the `render` list are actually listed as directly for
# sale in the shop. `object_data` might contain other items that provide
# supporting information about them, but aren't actually for sale.
visible_object_data = (nc_page["render"] || []).
map { |id| object_data[id.to_s] }.
filter(&:present?)
items = visible_object_data.map do |item_info|
{ {
id: item_info["id"], id: item_info["id"],
name: item_info["name"], name: item_info["name"],

View file

@ -2,7 +2,7 @@ require "async/http/internet/instance"
# While most of our NeoPass logic is built into Devise -> OmniAuth -> OIDC # While most of our NeoPass logic is built into Devise -> OmniAuth -> OIDC
# OmniAuth plugin, NeoPass also offers some supplemental APIs that we use here. # OmniAuth plugin, NeoPass also offers some supplemental APIs that we use here.
module NeoPass module Neopets::NeoPass
# Share a pool of persistent connections, rather than reconnecting on # Share a pool of persistent connections, rather than reconnecting on
# each request. (This library does that automatically!) # each request. (This library does that automatically!)
INTERNET = Async::HTTP::Internet.instance INTERNET = Async::HTTP::Internet.instance

View file

@ -1,76 +0,0 @@
- title "NeoPass for DTI"
= image_tag 'about/neopass-header.png',
alt: "Header image of three Neopets wearing NeoPass badges on lanyards",
width: 800, height: 232
:markdown
Hi, everyone! We've got big news coming up: we're partnering with Neopets to
add "Login with NeoPass" and some other integrations to bring our sites a
bit closer together! Here's what to expect and why.
_(Posted: March 13, 2024)_
## Login with NeoPass
Over time, Neopets is planning to send more users our way, and we want them
to have a smooth experience when they get here!
**So, new users will be able to click "Login with NeoPass" and use their
existing Neopets account**, instead of creating a new DTI username and
password. Existing DTI users can also link accounts if they want, too!
**All of this functionality is optional, and removable at any time!**
Usernames and passwords will still work as before—and unlike official
Neopets accounts that need long-term permanent linkage, we intend to offer
both linking and unlinking, so you can always have options.
We also know that a _lot_ of the pain points in Neopets and DTI right now come
from transferring info between our sites by hand. **It's possible this could
set us up for other smoother experiences in the future, too!** (Nothing like
that in the first release though—we've just been chatting with TNT about what
might come next!)
## Links to NC Mall
We're also planning to add **a few links from DTI to the NC Mall**, which
we'll do our best to make thoughtful and unobtrusive. There's two main
reasons for this!
First off, when Neopets sends users our way, we don't want them to get
confused and stuck here. Existing DTI users know their way around NC, but new
users probably won't, so we'll add a couple hints for how to get their
designs onto Neopets.com.
The second reason is: we believe Dress to Impress is a critical part of the
Neopets economy, and we want TNT to be able to see that, too. We'll include
**a lil referral code in the link** so TNT can know which shoppers came from
DTI, and can evaluate accordingly. (We expect this to be important for us
long-term!)
## Why now?
Dress to Impress has always been a **very small-staff volunteer project**, and
it's been clear to everyone over the past few years that we're struggling to
balance DTI with the rest of our lives 😖 Work and life and family have their
own needs, and they've been increasing!
And so… there are reasons we're being careful talking about details right
now, but the gist is: we're hoping that partnering with TNT will not only
help us fill gaps in the customization user experience, but can also be part
of **a more sustainable future for Dress to Impress long-term**. I hope we
can tell you more about it soon!
I know full well, and I'm sure you do too, that partnerships between
companies and fan projects can be complicated. I promise I'm doing my best to
represent you all, focusing on securing what's right for the community, and
keeping in mind the importance of autonomy! We'll keep DTI independent, only
do things we believe genuinely serve everyone, and keep a critical eye as we
go.
So, yeah! It's NeoPass time! We'll be working on this in the coming months,
and I'll let you know more along the way. If you have questions or thoughts,
please email me at <matchu@openneo.net>, and I'll do my best to listen and
help!
Thanks as always, everyone. We'll talk more soon! 💖
_—Matchu_

View file

@ -1,4 +1,12 @@
%li.alt-style %li
= link_to alt_style.preview_image_url do = link_to view_or_edit_alt_style_url(alt_style) do
= image_tag alt_style.thumbnail_url, class: 'alt-style-thumbnail' = image_tag alt_style.preview_image_url, class: "preview", loading: "lazy"
.alt-style-name= alt_style.name .name
%span= alt_style.series_name
%span= alt_style.pet_name
.info
%p
Added
= time_tag alt_style.created_at,
title: alt_style.created_at.to_formatted_s(:long_nst) do
= time_with_only_month_if_old alt_style.created_at

View file

@ -0,0 +1,37 @@
- title @alt_style.full_name
- use_responsive_design
%ol.breadcrumbs
%li= link_to "Alt Styles", alt_styles_path
%li
= link_to @alt_style.color.human_name,
alt_styles_path(color: @alt_style.color.human_name)
%li{"data-relation-to-prev": "sibling"}
= link_to @alt_style.species.human_name,
alt_styles_path(species: @alt_style.species.human_name)
%li= @alt_style.series_name
= image_tag @alt_style.preview_image_url, class: "alt-style-preview"
= support_form_with model: @alt_style, class: "support-form" do |f|
= f.errors
= f.fields do
= f.field do
= f.label :real_series_name, "Series"
= f.text_field :real_series_name, autofocus: !@alt_style.real_series_name?,
placeholder: AltStyle.placeholder_name
= f.field do
= f.label :thumbnail_url, "Thumbnail"
= f.thumbnail_input :thumbnail_url
= f.actions do
= f.submit "Save changes"
= f.go_to_next_field title: "If checked, takes you to the next unlabeled pet style, if any. Useful for labeling in bulk!" do
= f.go_to_next_check_box "unlabeled-style"
Then: Go to unlabeled style
- content_for :stylesheets do
= stylesheet_link_tag "application/breadcrumbs", "application/support-form"
= page_stylesheet_link_tag "alt_styles/edit"

View file

@ -1,18 +1,46 @@
- title "Styling Studio" - title "NC Pet Styles"
- use_responsive_design
%p %ul.breadcrumbs
Here's all the new NC Pet Styles we have! They're available in the app too, %li= link_to "Rainbow Pool", pet_types_path
by opening the emotion picker and clicking the "Styles" tab. %li Pet Styles
%p :markdown
If you have an Alt Style we don't, please model it by entering your pet's Pet Styles drastically change the appearance of your pet! They're [available
in the NC Mall][1], or via "NC Trading". Some of them are "Nostalgic",
meaning they're reminiscent of classic Neopets designs from long ago—and some
are brand new!
Pet Styles only fit pets of the same species—but the *color* of the pet
doesn't matter! A Blue Acara can wear the "Nostalgic Faerie Acara" Pet Style.
Only some items fit pets wearing Pet Styles: mostly Backgrounds, Foregrounds,
and other items that aren't designed to fit a specific body shape.
If you have a Pet Style we don't, please model it by entering your pet's
name on the homepage! Thank you! 💖 name on the homepage! Thank you! 💖
%p [1]: https://www.neopets.com/mall/stylingstudio/
Also, heads-up: Because our system can only collect "item data" for normal
wearable items, there's not a great way for us to get style tokens onto
tradelists… this may change someday, but probably not soon, sorry!
- @alt_styles.group_by(&:species).each do |species, species_styles| = form_with url: alt_styles_path, method: :get,
%h2.alt-styles-header= species.human_name class: "rainbow-pool-filters" do |f|
%ul.alt-styles-list= render partial: "alt_style", collection: species_styles %fieldset
%legend Filter by:
= f.select :series, @all_series_names,
selected: @series_name, include_blank: "Style…"
= f.select :color, @all_color_names,
selected: @color&.human_name, include_blank: "Color…"
= f.select :species, @all_species_names,
selected: @species&.human_name, include_blank: "Species…"
= f.submit "Go", name: nil
= will_paginate @alt_styles, class: "rainbow-pool-pagination"
%ul.rainbow-pool-list= render @alt_styles
= will_paginate @alt_styles, class: "rainbow-pool-pagination"
- content_for :stylesheets do
= stylesheet_link_tag "application/breadcrumbs"
= stylesheet_link_tag "application/rainbow-pool"
= page_stylesheet_link_tag "alt_styles/index"

View file

@ -1,4 +1,5 @@
%outfit-viewer - html_options = {} unless defined? html_options
= content_tag "outfit-viewer", **html_options do
.loading-indicator= render partial: "hanger_spinner" .loading-indicator= render partial: "hanger_spinner"
%label.play-pause-button{title: "Pause/play animations"} %label.play-pause-button{title: "Pause/play animations"}
@ -21,6 +22,6 @@
- if swf_asset.canvas_movie? - if swf_asset.canvas_movie?
%iframe{src: swf_asset_path(swf_asset, playing: outfit_viewer_is_playing ? true : nil)} %iframe{src: swf_asset_path(swf_asset, playing: outfit_viewer_is_playing ? true : nil)}
- elsif swf_asset.image_url.present? - elsif swf_asset.image_url.present?
= image_tag swf_asset.image_url, alt: "" = image_tag swf_asset.image_url, alt: "", loading: "lazy"
- else - else
/ No movie or image available for SWF asset: #{swf_asset.url} / No movie or image available for SWF asset: #{swf_asset.url}

View file

@ -0,0 +1,7 @@
- if form.object.errors.any?
%section.errors
Could not save:
%ul
- form.object.errors.each do |error|
%li= error.full_message

View file

@ -0,0 +1,4 @@
= form.field("data-type": "radio", **options) do
%fieldset
%legend= legend
%ul= content

View file

@ -0,0 +1,5 @@
- url = form.object.send(method)
.thumbnail-input
- if url.present?
= image_tag url, alt: "Thumbnail"
= form.url_field method

View file

@ -151,9 +151,8 @@
= stylesheet_link_tag 'https://ajax.googleapis.com/ajax/libs/jqueryui/1.9.0/themes/south-street/jquery-ui.css' = stylesheet_link_tag 'https://ajax.googleapis.com/ajax/libs/jqueryui/1.9.0/themes/south-street/jquery-ui.css'
- content_for :javascripts do - content_for :javascripts do
= include_javascript_libraries :jquery, :jquery_tmpl = javascript_include_tag 'jquery', 'jquery.tmpl', 'jquery.ui',
= javascript_include_tag 'ajax_auth', 'lib/jquery.ui', 'lib/jquery.jgrowl', 'jquery.jgrowl', defer: true
defer: true
- content_for :javascripts_body do - content_for :javascripts_body do
= javascript_include_tag 'closet_hangers/index', defer: true = javascript_include_tag 'closet_hangers/index', defer: true
@ -167,4 +166,4 @@
%meta{ %meta{
name: "trade-matches-wants", name: "trade-matches-wants",
value: @items.select(&:wanted?).map(&:id).join(",") value: @items.select(&:wanted?).map(&:id).join(",")
} }

View file

@ -7,27 +7,28 @@
%p= twl '.instructions', %p= twl '.instructions',
:edit_petpage_link_url => 'https://www.neopets.com/edithomepage.phtml' :edit_petpage_link_url => 'https://www.neopets.com/edithomepage.phtml'
= form_tag petpage_user_closet_hangers_path(@user), :method => :get, :id => 'petpage-closet-lists' do - unless @closet_lists_by_owned.values.all?(&:empty?)
= hidden_field_tag 'filter', '1' = form_tag petpage_user_closet_hangers_path(@user), :method => :get, :id => 'petpage-closet-lists' do
- @closet_lists_by_owned.each do |owned, closet_lists| = hidden_field_tag 'filter', '1'
%div - @closet_lists_by_owned.each do |owned, closet_lists|
%h4= closet_lists_group_name(:you, owned) %div
%ul %h4= closet_lists_group_name(:you, owned)
- closet_lists.each do |closet_list| %ul
%li - closet_lists.each do |closet_list|
%li
= label_tag do
= check_box_tag "lists[#{closet_list.id}]", '1', petpage_closet_list_checked(closet_list, owned)
= closet_list.name
%li.unlisted
= label_tag do = label_tag do
= check_box_tag "lists[#{closet_list.id}]", '1', petpage_closet_list_checked(closet_list, owned) = check_box_tag "groups[#{owned}]", '1', petpage_group_checked(owned)
= closet_list.name = t 'closet_lists.unlisted_name'
%li.unlisted = submit_tag t('.submit')
= label_tag do
= check_box_tag "groups[#{owned}]", '1', petpage_group_checked(owned)
= t 'closet_lists.unlisted_name'
= submit_tag t('.submit')
%textarea#petpage-output %textarea#petpage-output
= '' + render('petpage_content', = '' + render('petpage_content',
:lists_by_owned => @visible_closet_lists_by_owned, :lists_by_owned => @visible_closet_lists_by_owned,
:unlisted_hangers_by_owned => @visible_unlisted_closet_hangers_by_owned) :unlisted_hangers_by_owned => @visible_unlisted_closet_hangers_by_owned)
= include_javascript_libraries :jquery - content_for :stylesheets do
= javascript_include_tag 'closet_hangers/petpage' = stylesheet_link_tag 'closet_hangers/petpage'

View file

@ -1,5 +1,5 @@
- contributed = contribution.contributed - contributed = contribution.contributed
= content_tag_for :li, contribution do %li
%span.point-value= contribution.point_value %span.point-value= contribution.point_value
= t 'contributions.contribution.description_html', = t 'contributions.contribution.description_html',
:user_link => link_to(contribution.user.name, :user_link => link_to(contribution.user.name,

View file

@ -42,10 +42,7 @@
= f.submit 'Save donation details' = f.submit 'Save donation details'
- content_for :javascripts do - content_for :javascripts do
= include_javascript_libraries :jquery = javascript_include_tag 'jquery', 'fundraising/donations/show', defer: true
- content_for :javascripts_body do
= javascript_include_tag 'fundraising/donations/show', defer: true
- content_for :stylesheets do - content_for :stylesheets do
= page_stylesheet_link_tag 'fundraising/donations/show' = page_stylesheet_link_tag 'fundraising/donations/show'

View file

@ -46,6 +46,8 @@
= link_to t('items.show.closet_hangers.button'), = link_to t('items.show.closet_hangers.button'),
user_closet_hangers_path(current_user), user_closet_hangers_path(current_user),
class: 'user-lists-form-opener' class: 'user-lists-form-opener'
- if support_staff?
= link_to "Edit", edit_item_path(item)
- if user_signed_in? - if user_signed_in?
= form_tag update_quantities_user_item_closet_hangers_path(user_id: current_user, item_id: item), method: :put, class: 'user-lists-form', hidden: item_header_user_lists_form_state != "open" do = form_tag update_quantities_user_item_closet_hangers_path(user_id: current_user, item_id: item), method: :put, class: 'user-lists-form', hidden: item_header_user_lists_form_state != "open" do
@ -100,4 +102,4 @@
'data-is-current' => current_subpage == 'trades_seeking' 'data-is-current' => current_subpage == 'trades_seeking'
- content_for :javascripts do - content_for :javascripts do
= javascript_include_tag 'items/item_header', defer: true = javascript_include_tag 'items/item_header', async: true

View file

@ -0,0 +1,58 @@
- title "Editing \"#{@item.name}\""
- use_responsive_design
%h1#title Editing "#{@item.name}"
:markdown
Heads up: the modeling process controls some of these fields by default! If
you change something, but it doesn't match what we're seeing on Neopets.com,
it will probably be reverted automatically when someone models it.
= support_form_with model: @item, class: "support-form" do |f|
= f.errors
= f.fields do
= f.field do
= f.label :name
= f.text_field :name
= f.field do
= f.label :thumbnail_url, "Thumbnail"
= f.thumbnail_input :thumbnail_url
= f.field do
= f.label :description
= f.text_field :description
= f.radio_fieldset "Item kind" do
= f.radio_field title: "NC items generally have a rarity value of 500.\nPaintbrush items generally contain a special message in the description." do
= f.radio_button :is_manually_nc, false
Automatic: Based on rarity and description
= f.radio_field title: "Use this when Neopets releases an NC item, but labels the rarity as something other than 500, usually by mistake." do
= f.radio_button :is_manually_nc, true
Manually NC: From the NC Mall, but not r500
= f.radio_fieldset "Modeling status" do
= f.radio_field title: "If we fit two or more species of a standard color, assume we also fit the other standard-color pets that were released at the time.\nRepeat for special colors like Baby and Maraquan." do
= f.radio_button :modeling_status_hint, ""
Automatic: Fits 2+ species &rarr; Should fit all
= f.radio_field title: "Use this when e.g. there simply is no Acara version of the item." do
= f.radio_button :modeling_status_hint, "done"
Done: Neopets.com is missing some models
= f.radio_field title: "Use this when e.g. this fits the Blue Vandagyre even though it's a Maraquan item.\nBehaves identically to Done, but helps us remember why we did this!" do
= f.radio_button :modeling_status_hint, "glitchy"
Glitchy: Neopets.com has <em>too many</em> models
= f.radio_fieldset "Body fit" do
= f.radio_field title: "When an asset in a zone like Background is modeled, assume it fits all pets the same, and assign it body ID \#0.\nOtherwise, assume it fits only the kind of pet it was modeled on." do
= f.radio_button :explicitly_body_specific, false
Automatic: Some zones fit all species
= f.radio_field title: "Use this when an item uses a generally-universal zone like Static, but is body-specific regardless. \"Encased in Ice\" is one example.\nThis prevents these uncommon items from breaking every time they're modeled." do
= f.radio_button :explicitly_body_specific, true
Body-specific: Fits all species differently
= f.actions do
= f.submit "Save changes"
- content_for :stylesheets do
= page_stylesheet_link_tag "application/support-form"

Some files were not shown because too many files have changed in this diff Show more